diff --git a/src/auth/models/user.ts b/src/auth/models/user.ts index 033953e..cc182c0 100644 --- a/src/auth/models/user.ts +++ b/src/auth/models/user.ts @@ -34,6 +34,9 @@ const usersTable = cfUser; const orgsTable = cfOrgs; const userSecretsTable = cfUserSecrets; +// 常量定义 +const JWKS_TOKEN_EXPIRY = 2 * 3600; // 2 hours in seconds + export const redis = useContextKey('redis'); type TokenOptions = { @@ -77,6 +80,32 @@ export class User { * @param uid * @returns */ + /** + * 创建JWKS token的通用方法 + */ + private static async createJwksTokenResponse(user: { id: string; username: string }, opts: { expire?: number } = {}) { + const expiresIn = opts?.expire ?? JWKS_TOKEN_EXPIRY; + const accessToken = await jwksManager.sign({ + sub: 'user:' + user.id, + name: user.username, + exp: Math.floor(Date.now() / 1000) + expiresIn, + }); + await oauth.setJwksToken(accessToken, { id: user.id, expire: expiresIn }); + + const token = { + accessToken, + refreshToken: accessToken, + token: accessToken, + refreshTokenExpiresIn: expiresIn, + accessTokenExpiresIn: expiresIn, + }; + + return { + type: 'jwks', + ...token, + }; + } + async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week' | 'jwks', opts: TokenOptions = {}) { const { id, username, type } = this; const oauthUser: OauthUser = { @@ -90,30 +119,12 @@ export class User { oauthUser.orgId = id; } if (loginType === 'jwks') { - const expiresIn = opts?.expire ?? 2 * 3600; // 2 hours - const accessToken = await jwksManager.sign({ - sub: 'user:' + this.id, - name: this.username, - exp: Math.floor(Date.now() / 1000) + expiresIn, - }); - await oauth.setJwksToken(accessToken, { id: this.id, expire: expiresIn }); - return { - type: 'jwks', - accessToken: accessToken, - refreshToken: accessToken, - token: accessToken, - refreshTokenExpiresIn: expiresIn, - accessTokenExpiresIn: expiresIn - }; + return await User.createJwksTokenResponse(this, opts); } const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...opts }); return { type: 'default', - accessToken: token.accessToken, - refreshToken: token.refreshToken, - token: token.accessToken, - refreshTokenExpiresIn: token.refreshTokenExpiresIn, - accessTokenExpiresIn: token.accessTokenExpiresIn, + ...token, }; } /** @@ -129,40 +140,63 @@ export class User { * @param refreshToken * @returns */ - static async refreshToken(refreshToken: string) { - if (refreshToken?.includes?.('.')) { + static async refreshToken(opts: { refreshToken?: string, accessToken?: string }) { + const { refreshToken, accessToken } = opts; + let jwsRefreshToken = accessToken || refreshToken; + if (oauth.getTokenType(jwsRefreshToken) === 'jwks') { // 可能是 jwks token - const jwksToken = await oauth.getJwksToken(refreshToken); + const jwksToken = await oauth.getJwksToken(jwsRefreshToken); if (!jwksToken) { throw new CustomError('Invalid refresh token'); } - const decoded = await jwksManager.decode(refreshToken); - const sub = decoded.sub; - const username = decoded.name; - const expiresIn = 2 * 3600; // 2 hours - const newToken = await jwksManager.sign({ - sub, - name: username, - exp: Math.floor(Date.now() / 1000) + expiresIn, + const decoded = await jwksManager.decode(jwsRefreshToken); + return await User.createJwksTokenResponse({ + id: decoded.sub.replace('user:', ''), + username: decoded.name }); - oauth.setJwksToken(newToken, { id: sub.replace('user:', ''), expire: expiresIn }); - return { - type: 'jwks', - accessToken: newToken, - refreshToken: newToken, - token: newToken, - refreshTokenExpiresIn: expiresIn, - accessTokenExpiresIn: expiresIn, + } + if (!refreshToken && !accessToken) { + throw new CustomError('Refresh Token or Access Token 必须提供一个'); + } + if (accessToken) { + try { + const token = await User.refreshTokenByAccessToken(accessToken); + return token; + } catch (e) { + // access token 无效,继续使用 refresh token 刷新 } } + const token = await User.refreshTokenByRefreshToken(refreshToken); + return { + type: 'default', + ...token, + }; + } + static async refreshTokenByAccessToken(accessToken: string) { + const accessUser = await User.verifyToken(accessToken); + if (!accessUser) { + throw new CustomError('Invalid access token'); + } + const refreshToken = accessUser.oauthExpand?.refreshToken; + if (refreshToken) { + return await User.refreshTokenByRefreshToken(refreshToken); + } else { + await User.oauth.delToken(accessToken); + const token = await User.oauth.generateToken(accessUser, { + ...accessUser.oauthExpand, + hasRefreshToken: true, + }); + return { + type: 'default', + ...token, + }; + } + } + static async refreshTokenByRefreshToken(refreshToken: string) { const token = await oauth.refreshToken(refreshToken); return { type: 'default', - accessToken: token.accessToken, - refreshToken: token.refreshToken, - token: token.accessToken, - accessTokenExpiresIn: token.accessTokenExpiresIn, - refreshTokenExpiresIn: token.refreshTokenExpiresIn, + ...token }; } /** @@ -171,30 +205,17 @@ export class User { * @returns */ static async resetToken(refreshToken: string, expand?: Record) { - if (refreshToken?.includes?.('.')) { + if (oauth.getTokenType(refreshToken) === 'jwks') { // 可能是 jwks token const jwksToken = await oauth.getJwksToken(refreshToken); if (!jwksToken) { throw new CustomError('Invalid refresh token'); } const decoded = await jwksManager.decode(refreshToken); - const sub = decoded.sub; - const username = decoded.name; - const expiresIn = 2 * 3600; // 2 hours - const newToken = await jwksManager.sign({ - sub, - name: username, - exp: Math.floor(Date.now() / 1000) + expiresIn, + return await User.createJwksTokenResponse({ + id: decoded.sub.replace('user:', ''), + username: decoded.name }); - oauth.setJwksToken(newToken, { id: sub.replace('user:', ''), expire: expiresIn }); - return { - type: 'jwks', - accessToken: newToken, - refreshToken: newToken, - token: newToken, - refreshTokenExpiresIn: expiresIn, - accessTokenExpiresIn: expiresIn, - }; } return await oauth.resetToken(refreshToken, expand); } @@ -296,10 +317,9 @@ export class User { const users = await query.limit(1); return users.length > 0 ? new User(users[0]) : null; } - static findByunionid() { - - } - + /** + * 创建新用户 + */ static async createUser(username: string, password?: string, description?: string) { const user = await User.findOne({ username }); if (user) { diff --git a/src/auth/oauth/oauth.ts b/src/auth/oauth/oauth.ts index 5511d7e..285afcd 100644 --- a/src/auth/oauth/oauth.ts +++ b/src/auth/oauth/oauth.ts @@ -298,6 +298,28 @@ export class OAuth { } return false; } + /** + * 获取token类型:jwks, secretKey, accessToken, refreshToken + * @param token 要检查的token + * @returns token类型或null + */ + getTokenType(token: string) { + if (!token) { + return null; + } + if (token.includes('.')) { + return 'jwks'; + } + if (token.startsWith('sk_')) { + return 'secretKey'; + } + if (token.startsWith('st_')) { + return 'accessToken'; + } + if (token.startsWith('rk_')) { + return 'refreshToken'; + } + } /** * 刷新token * @param refreshToken @@ -406,18 +428,23 @@ export class OAuth { */ async setJwksToken(token: string, opts: { id: string; expire: number }) { const expire = opts.expire ?? 2 * 3600; // 2 hours - const id = opts.id || ''; + const id = opts.id || '-'; // jwks token的过期时间比accessToken多3天,确保3天内可以用来refresh token const addExpire = 3 * 24 * 3600; await this.store.redis.set('user:jwks:' + token, id, 'EX', expire + addExpire); } - async deleteJwsToken(token: string) { + async deleteJwksToken(token: string) { await this.store.redis.expire('user:jwks:' + token, 0); } + /** + * 获取后就删除jwks token,确保token只能使用一次。 + * @param token + * @returns + */ async getJwksToken(token: string) { const id = await this.store.redis.get('user:jwks:' + token); if (id) { - this.deleteJwsToken(token); + this.deleteJwksToken(token); } return id; } diff --git a/src/routes/user/me.ts b/src/routes/user/me.ts index d08559f..bf97700 100644 --- a/src/routes/user/me.ts +++ b/src/routes/user/me.ts @@ -256,6 +256,7 @@ app .route({ path: 'user', key: 'switchCheck', + description: '切换用户或切换为用户组,获取切换后的token', middleware: ['auth'], }) .define(async (ctx) => { @@ -263,23 +264,13 @@ app const { username, accessToken } = ctx.query.data || {}; if (accessToken && username) { - const accessUser = await User.verifyToken(accessToken); - const refreshToken = accessUser.oauthExpand?.refreshToken; - if (refreshToken) { - const result = await User.refreshToken(refreshToken); - createCookie(token, ctx); - - ctx.body = result; - return; - } else if (accessUser) { - await User.oauth.delToken(accessToken); - const result = await User.oauth.generateToken(accessUser, { - ...accessUser.oauthExpand, - hasRefreshToken: true, - }); + const result = await User.refreshToken({ accessToken }); + if (result.accessToken) { + console.log('refreshToken result', result); createCookie(result, ctx); ctx.body = result; - return; + } else { + ctx.throw(500, 'Refresh Token Failed, please login again'); } } else { const result = await ctx.call( @@ -355,18 +346,19 @@ app args: { data: z.object({ refreshToken: z.string().describe('刷新token'), + accessToken: z.string().optional().describe('使用访问token去刷新token,如果提供了访问token,优先使用访问token去刷新token,刷新失败才会使用refreshToken去刷新'), }), } } }) .define(async (ctx) => { - const { refreshToken } = ctx.query.data || {}; + const { refreshToken, accessToken } = ctx.query.data || {}; try { - if (!refreshToken) { - ctx.throw(400, 'Refresh Token is required'); + if (!refreshToken && !accessToken) { + ctx.throw(400, 'Refresh Token or Access Token 必须提供一个'); } - const result = await User.refreshToken(refreshToken); - if (result) { + const result = await User.refreshToken({ accessToken, refreshToken }); + if (result.accessToken) { console.log('refreshToken result', result); createCookie(result, ctx); ctx.body = result;