diff --git a/docs/src/content/docs/en/modules/network/auth.md b/docs/src/content/docs/en/modules/network/auth.md index 2f29b49d..9f5e5c69 100644 --- a/docs/src/content/docs/en/modules/network/auth.md +++ b/docs/src/content/docs/en/modules/network/auth.md @@ -92,6 +92,355 @@ const token = jwtProvider.sign({ const payload = jwtProvider.decode(token) ``` +### Custom Provider + +You can create custom authentication providers by implementing the `IAuthProvider` interface to integrate with any authentication system (OAuth, LDAP, custom database auth, etc.). + +#### IAuthProvider Interface + +```typescript +interface IAuthProvider { + /** Provider name */ + readonly name: string; + + /** Verify credentials */ + verify(credentials: TCredentials): Promise>; + + /** Refresh token (optional) */ + refresh?(token: string): Promise>; + + /** Revoke token (optional) */ + revoke?(token: string): Promise; +} + +interface AuthResult { + success: boolean; + user?: TUser; + error?: string; + errorCode?: AuthErrorCode; + token?: string; + expiresAt?: number; +} + +type AuthErrorCode = + | 'INVALID_CREDENTIALS' + | 'EXPIRED_TOKEN' + | 'INVALID_TOKEN' + | 'USER_NOT_FOUND' + | 'ACCOUNT_DISABLED' + | 'RATE_LIMITED' + | 'INSUFFICIENT_PERMISSIONS'; +``` + +#### Custom Provider Examples + +**Example 1: Database Password Authentication** + +```typescript +import type { IAuthProvider, AuthResult } from '@esengine/server/auth' + +interface User { + id: string + username: string + roles: string[] +} + +interface PasswordCredentials { + username: string + password: string +} + +class DatabaseAuthProvider implements IAuthProvider { + readonly name = 'database' + + async verify(credentials: PasswordCredentials): Promise> { + const { username, password } = credentials + + // Query user from database + const user = await db.users.findByUsername(username) + if (!user) { + return { + success: false, + error: 'User not found', + errorCode: 'USER_NOT_FOUND' + } + } + + // Verify password (using bcrypt or similar) + const isValid = await bcrypt.compare(password, user.passwordHash) + if (!isValid) { + return { + success: false, + error: 'Invalid password', + errorCode: 'INVALID_CREDENTIALS' + } + } + + // Check account status + if (user.disabled) { + return { + success: false, + error: 'Account is disabled', + errorCode: 'ACCOUNT_DISABLED' + } + } + + return { + success: true, + user: { + id: user.id, + username: user.username, + roles: user.roles + } + } + } +} +``` + +**Example 2: OAuth/Third-party Authentication** + +```typescript +import type { IAuthProvider, AuthResult } from '@esengine/server/auth' + +interface OAuthUser { + id: string + email: string + name: string + provider: string + roles: string[] +} + +interface OAuthCredentials { + provider: 'google' | 'github' | 'discord' + accessToken: string +} + +class OAuthProvider implements IAuthProvider { + readonly name = 'oauth' + + async verify(credentials: OAuthCredentials): Promise> { + const { provider, accessToken } = credentials + + try { + // Verify token with provider + const profile = await this.fetchUserProfile(provider, accessToken) + + // Find or create local user + let user = await db.users.findByOAuth(provider, profile.id) + if (!user) { + user = await db.users.create({ + oauthProvider: provider, + oauthId: profile.id, + email: profile.email, + name: profile.name, + roles: ['player'] + }) + } + + return { + success: true, + user: { + id: user.id, + email: user.email, + name: user.name, + provider, + roles: user.roles + } + } + } catch (error) { + return { + success: false, + error: 'OAuth verification failed', + errorCode: 'INVALID_TOKEN' + } + } + } + + private async fetchUserProfile(provider: string, token: string) { + switch (provider) { + case 'google': + return fetch('https://www.googleapis.com/oauth2/v2/userinfo', { + headers: { Authorization: `Bearer ${token}` } + }).then(r => r.json()) + case 'github': + return fetch('https://api.github.com/user', { + headers: { Authorization: `Bearer ${token}` } + }).then(r => r.json()) + // Other providers... + default: + throw new Error(`Unsupported provider: ${provider}`) + } + } +} +``` + +**Example 3: API Key Authentication** + +```typescript +import type { IAuthProvider, AuthResult } from '@esengine/server/auth' + +interface ApiUser { + id: string + name: string + roles: string[] + rateLimit: number +} + +class ApiKeyAuthProvider implements IAuthProvider { + readonly name = 'api-key' + + private revokedKeys = new Set() + + async verify(apiKey: string): Promise> { + if (!apiKey || !apiKey.startsWith('sk_')) { + return { + success: false, + error: 'Invalid API Key format', + errorCode: 'INVALID_TOKEN' + } + } + + if (this.revokedKeys.has(apiKey)) { + return { + success: false, + error: 'API Key has been revoked', + errorCode: 'INVALID_TOKEN' + } + } + + // Query API Key from database + const keyData = await db.apiKeys.findByKey(apiKey) + if (!keyData) { + return { + success: false, + error: 'API Key not found', + errorCode: 'INVALID_CREDENTIALS' + } + } + + // Check expiration + if (keyData.expiresAt && keyData.expiresAt < Date.now()) { + return { + success: false, + error: 'API Key has expired', + errorCode: 'EXPIRED_TOKEN' + } + } + + return { + success: true, + user: { + id: keyData.userId, + name: keyData.name, + roles: keyData.roles, + rateLimit: keyData.rateLimit + }, + expiresAt: keyData.expiresAt + } + } + + async revoke(apiKey: string): Promise { + this.revokedKeys.add(apiKey) + await db.apiKeys.revoke(apiKey) + return true + } +} +``` + +#### Using Custom Providers + +```typescript +import { createServer } from '@esengine/server' +import { withAuth } from '@esengine/server/auth' + +// Create custom provider +const dbAuthProvider = new DatabaseAuthProvider() + +// Or use OAuth provider +const oauthProvider = new OAuthProvider() + +// Use custom provider +const server = withAuth(await createServer({ port: 3000 }), { + provider: dbAuthProvider, // or oauthProvider + + // Extract credentials from WebSocket connection request + extractCredentials: (req) => { + const url = new URL(req.url, 'http://localhost') + + // For database auth: get from query params + const username = url.searchParams.get('username') + const password = url.searchParams.get('password') + if (username && password) { + return { username, password } + } + + // For OAuth: get from token param + const provider = url.searchParams.get('provider') + const accessToken = url.searchParams.get('access_token') + if (provider && accessToken) { + return { provider, accessToken } + } + + // For API Key: get from header + const apiKey = req.headers['x-api-key'] + if (apiKey) { + return apiKey as string + } + + return null + }, + + onAuthFailure: (conn, error) => { + console.log(`Auth failed: ${error.errorCode} - ${error.error}`) + } +}) + +await server.start() +``` + +#### Combining Multiple Providers + +You can create a composite provider to support multiple authentication methods: + +```typescript +import type { IAuthProvider, AuthResult } from '@esengine/server/auth' + +interface MultiAuthCredentials { + type: 'jwt' | 'oauth' | 'apikey' | 'password' + data: unknown +} + +class MultiAuthProvider implements IAuthProvider { + readonly name = 'multi' + + constructor( + private jwtProvider: JwtAuthProvider, + private oauthProvider: OAuthProvider, + private apiKeyProvider: ApiKeyAuthProvider, + private dbProvider: DatabaseAuthProvider + ) {} + + async verify(credentials: MultiAuthCredentials): Promise> { + switch (credentials.type) { + case 'jwt': + return this.jwtProvider.verify(credentials.data as string) + case 'oauth': + return this.oauthProvider.verify(credentials.data as OAuthCredentials) + case 'apikey': + return this.apiKeyProvider.verify(credentials.data as string) + case 'password': + return this.dbProvider.verify(credentials.data as PasswordCredentials) + default: + return { + success: false, + error: 'Unsupported authentication type', + errorCode: 'INVALID_CREDENTIALS' + } + } + } +} +``` + ### Session Provider Use server-side sessions for stateful authentication: diff --git a/docs/src/content/docs/modules/network/auth.md b/docs/src/content/docs/modules/network/auth.md index 0bf00841..d2584393 100644 --- a/docs/src/content/docs/modules/network/auth.md +++ b/docs/src/content/docs/modules/network/auth.md @@ -92,6 +92,355 @@ const token = jwtProvider.sign({ const payload = jwtProvider.decode(token) ``` +### 自定义提供者 + +你可以通过实现 `IAuthProvider` 接口来创建自定义认证提供者,以集成任何认证系统(如 OAuth、LDAP、自定义数据库认证等)。 + +#### IAuthProvider 接口 + +```typescript +interface IAuthProvider { + /** 提供者名称 */ + readonly name: string; + + /** 验证凭证 */ + verify(credentials: TCredentials): Promise>; + + /** 刷新令牌(可选) */ + refresh?(token: string): Promise>; + + /** 撤销令牌(可选) */ + revoke?(token: string): Promise; +} + +interface AuthResult { + success: boolean; + user?: TUser; + error?: string; + errorCode?: AuthErrorCode; + token?: string; + expiresAt?: number; +} + +type AuthErrorCode = + | 'INVALID_CREDENTIALS' + | 'EXPIRED_TOKEN' + | 'INVALID_TOKEN' + | 'USER_NOT_FOUND' + | 'ACCOUNT_DISABLED' + | 'RATE_LIMITED' + | 'INSUFFICIENT_PERMISSIONS'; +``` + +#### 自定义提供者示例 + +**示例 1:数据库密码认证** + +```typescript +import type { IAuthProvider, AuthResult } from '@esengine/server/auth' + +interface User { + id: string + username: string + roles: string[] +} + +interface PasswordCredentials { + username: string + password: string +} + +class DatabaseAuthProvider implements IAuthProvider { + readonly name = 'database' + + async verify(credentials: PasswordCredentials): Promise> { + const { username, password } = credentials + + // 从数据库查询用户 + const user = await db.users.findByUsername(username) + if (!user) { + return { + success: false, + error: '用户不存在', + errorCode: 'USER_NOT_FOUND' + } + } + + // 验证密码(使用 bcrypt 等库) + const isValid = await bcrypt.compare(password, user.passwordHash) + if (!isValid) { + return { + success: false, + error: '密码错误', + errorCode: 'INVALID_CREDENTIALS' + } + } + + // 检查账号状态 + if (user.disabled) { + return { + success: false, + error: '账号已禁用', + errorCode: 'ACCOUNT_DISABLED' + } + } + + return { + success: true, + user: { + id: user.id, + username: user.username, + roles: user.roles + } + } + } +} +``` + +**示例 2:OAuth/第三方认证** + +```typescript +import type { IAuthProvider, AuthResult } from '@esengine/server/auth' + +interface OAuthUser { + id: string + email: string + name: string + provider: string + roles: string[] +} + +interface OAuthCredentials { + provider: 'google' | 'github' | 'discord' + accessToken: string +} + +class OAuthProvider implements IAuthProvider { + readonly name = 'oauth' + + async verify(credentials: OAuthCredentials): Promise> { + const { provider, accessToken } = credentials + + try { + // 根据提供商验证 token + const profile = await this.fetchUserProfile(provider, accessToken) + + // 查找或创建本地用户 + let user = await db.users.findByOAuth(provider, profile.id) + if (!user) { + user = await db.users.create({ + oauthProvider: provider, + oauthId: profile.id, + email: profile.email, + name: profile.name, + roles: ['player'] + }) + } + + return { + success: true, + user: { + id: user.id, + email: user.email, + name: user.name, + provider, + roles: user.roles + } + } + } catch (error) { + return { + success: false, + error: 'OAuth 验证失败', + errorCode: 'INVALID_TOKEN' + } + } + } + + private async fetchUserProfile(provider: string, token: string) { + switch (provider) { + case 'google': + return fetch('https://www.googleapis.com/oauth2/v2/userinfo', { + headers: { Authorization: `Bearer ${token}` } + }).then(r => r.json()) + case 'github': + return fetch('https://api.github.com/user', { + headers: { Authorization: `Bearer ${token}` } + }).then(r => r.json()) + // 其他提供商... + default: + throw new Error(`不支持的提供商: ${provider}`) + } + } +} +``` + +**示例 3:API Key 认证** + +```typescript +import type { IAuthProvider, AuthResult } from '@esengine/server/auth' + +interface ApiUser { + id: string + name: string + roles: string[] + rateLimit: number +} + +class ApiKeyAuthProvider implements IAuthProvider { + readonly name = 'api-key' + + private revokedKeys = new Set() + + async verify(apiKey: string): Promise> { + if (!apiKey || !apiKey.startsWith('sk_')) { + return { + success: false, + error: 'API Key 格式无效', + errorCode: 'INVALID_TOKEN' + } + } + + if (this.revokedKeys.has(apiKey)) { + return { + success: false, + error: 'API Key 已被撤销', + errorCode: 'INVALID_TOKEN' + } + } + + // 从数据库查询 API Key + const keyData = await db.apiKeys.findByKey(apiKey) + if (!keyData) { + return { + success: false, + error: 'API Key 不存在', + errorCode: 'INVALID_CREDENTIALS' + } + } + + // 检查过期 + if (keyData.expiresAt && keyData.expiresAt < Date.now()) { + return { + success: false, + error: 'API Key 已过期', + errorCode: 'EXPIRED_TOKEN' + } + } + + return { + success: true, + user: { + id: keyData.userId, + name: keyData.name, + roles: keyData.roles, + rateLimit: keyData.rateLimit + }, + expiresAt: keyData.expiresAt + } + } + + async revoke(apiKey: string): Promise { + this.revokedKeys.add(apiKey) + await db.apiKeys.revoke(apiKey) + return true + } +} +``` + +#### 使用自定义提供者 + +```typescript +import { createServer } from '@esengine/server' +import { withAuth } from '@esengine/server/auth' + +// 创建自定义提供者 +const dbAuthProvider = new DatabaseAuthProvider() + +// 或使用 OAuth 提供者 +const oauthProvider = new OAuthProvider() + +// 使用自定义提供者 +const server = withAuth(await createServer({ port: 3000 }), { + provider: dbAuthProvider, // 或 oauthProvider + + // 从 WebSocket 连接请求中提取凭证 + extractCredentials: (req) => { + const url = new URL(req.url, 'http://localhost') + + // 对于数据库认证:从查询参数获取 + const username = url.searchParams.get('username') + const password = url.searchParams.get('password') + if (username && password) { + return { username, password } + } + + // 对于 OAuth:从 token 参数获取 + const provider = url.searchParams.get('provider') + const accessToken = url.searchParams.get('access_token') + if (provider && accessToken) { + return { provider, accessToken } + } + + // 对于 API Key:从请求头获取 + const apiKey = req.headers['x-api-key'] + if (apiKey) { + return apiKey as string + } + + return null + }, + + onAuthFailure: (conn, error) => { + console.log(`认证失败: ${error.errorCode} - ${error.error}`) + } +}) + +await server.start() +``` + +#### 组合多个提供者 + +你可以创建一个复合提供者来支持多种认证方式: + +```typescript +import type { IAuthProvider, AuthResult } from '@esengine/server/auth' + +interface MultiAuthCredentials { + type: 'jwt' | 'oauth' | 'apikey' | 'password' + data: unknown +} + +class MultiAuthProvider implements IAuthProvider { + readonly name = 'multi' + + constructor( + private jwtProvider: JwtAuthProvider, + private oauthProvider: OAuthProvider, + private apiKeyProvider: ApiKeyAuthProvider, + private dbProvider: DatabaseAuthProvider + ) {} + + async verify(credentials: MultiAuthCredentials): Promise> { + switch (credentials.type) { + case 'jwt': + return this.jwtProvider.verify(credentials.data as string) + case 'oauth': + return this.oauthProvider.verify(credentials.data as OAuthCredentials) + case 'apikey': + return this.apiKeyProvider.verify(credentials.data as string) + case 'password': + return this.dbProvider.verify(credentials.data as PasswordCredentials) + default: + return { + success: false, + error: '不支持的认证类型', + errorCode: 'INVALID_CREDENTIALS' + } + } + } +} +``` + ### Session 提供者 使用服务端会话实现有状态认证: