diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 483f8e54c..f9a5ef312 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -89,6 +89,9 @@ export const UNIVERSAL_AUTH = { }, RENEW_ACCESS_TOKEN: { accessToken: "The access token to renew." + }, + REVOKE_ACCESS_TOKEN: { + accessToken: "The access token to revoke." } } as const; diff --git a/backend/src/server/routes/v1/identity-access-token-router.ts b/backend/src/server/routes/v1/identity-access-token-router.ts index 387c54c13..7ed62e679 100644 --- a/backend/src/server/routes/v1/identity-access-token-router.ts +++ b/backend/src/server/routes/v1/identity-access-token-router.ts @@ -36,4 +36,29 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid }; } }); + + server.route({ + url: "/token/revoke", + method: "POST", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Revoke access token", + body: z.object({ + accessToken: z.string().trim().describe(UNIVERSAL_AUTH.REVOKE_ACCESS_TOKEN.accessToken) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req) => { + await server.services.identityAccessToken.revokeAccessToken(req.body.accessToken); + return { + message: "Successfully revoked access token" + }; + } + }); }; diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 4b53c8174..898d0bc62 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -106,6 +106,24 @@ export const identityAccessTokenServiceFactory = ({ return { accessToken, identityAccessToken: updatedIdentityAccessToken }; }; + const revokeAccessToken = async (accessToken: string) => { + const appCfg = getConfig(); + + const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as JwtPayload & { + identityAccessTokenId: string; + }; + if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) throw new UnauthorizedError(); + + const identityAccessToken = await identityAccessTokenDAL.findOne({ + [`${TableName.IdentityAccessToken}.id` as "id"]: decodedToken.identityAccessTokenId, + isAccessTokenRevoked: false + }); + if (!identityAccessToken) throw new UnauthorizedError(); + + const revokedToken = await identityAccessTokenDAL.deleteById(identityAccessToken.id); + return { revokedToken }; + }; + const fnValidateIdentityAccessToken = async (token: TIdentityAccessTokenJwtPayload, ipAddress?: string) => { const identityAccessToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId, @@ -132,5 +150,5 @@ export const identityAccessTokenServiceFactory = ({ return { ...identityAccessToken, orgId: identityOrgMembership.orgId }; }; - return { renewAccessToken, fnValidateIdentityAccessToken }; + return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken }; }; diff --git a/docs/api-reference/endpoints/universal-auth/revoke-access-token.mdx b/docs/api-reference/endpoints/universal-auth/revoke-access-token.mdx new file mode 100644 index 000000000..082a76544 --- /dev/null +++ b/docs/api-reference/endpoints/universal-auth/revoke-access-token.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke Access Token" +openapi: "POST /api/v1/auth/token/revoke" +--- diff --git a/docs/mint.json b/docs/mint.json index 4a106a79c..18e639d4c 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -416,7 +416,8 @@ "api-reference/endpoints/universal-auth/create-client-secret", "api-reference/endpoints/universal-auth/list-client-secrets", "api-reference/endpoints/universal-auth/revoke-client-secret", - "api-reference/endpoints/universal-auth/renew-access-token" + "api-reference/endpoints/universal-auth/renew-access-token", + "api-reference/endpoints/universal-auth/revoke-access-token" ] }, {