Skip to content

Commit be49b12

Browse files
authored
feat(auth0-auth-js): add revokeToken support (SDK-8850) (#221)
Adds AuthClient.revokeToken() which calls the RFC 7009 /oauth/revoke endpoint. Introduces TokenRevocationError for failed revocations.
1 parent 21e9777 commit be49b12

4 files changed

Lines changed: 130 additions & 0 deletions

File tree

packages/auth0-auth-js/src/auth-client.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3992,3 +3992,93 @@ test('database.changePassword request carries the Auth0-Client telemetry header'
39923992
await c.database.changePassword({ email: 'a@b.com', connection: 'db' });
39933993
expect(headers?.get('Auth0-Client')).toBeTruthy();
39943994
});
3995+
3996+
describe('revokeToken', () => {
3997+
const revocationEndpoint = `https://${domain}/oauth/revoke`;
3998+
3999+
const setupRevocationHandlers = (handler: Parameters<typeof http.post>[1]) => {
4000+
server.use(
4001+
http.get(`https://${domain}/.well-known/openid-configuration`, () =>
4002+
HttpResponse.json({ ...buildOpenIdConfiguration(domain), revocation_endpoint: revocationEndpoint })
4003+
),
4004+
http.post(revocationEndpoint, handler)
4005+
);
4006+
};
4007+
4008+
const makeClient = () =>
4009+
new AuthClient({
4010+
domain,
4011+
clientId: '<client_id>',
4012+
clientSecret: '<client_secret>',
4013+
discoveryCache: { ttl: 0 },
4014+
});
4015+
4016+
test('should successfully revoke a token', async () => {
4017+
let capturedToken: string | null = null;
4018+
let capturedHint: string | null = null;
4019+
setupRevocationHandlers(async ({ request }) => {
4020+
const body = await request.formData();
4021+
capturedToken = body.get('token') as string;
4022+
capturedHint = body.get('token_type_hint') as string;
4023+
return new HttpResponse(null, { status: 200 });
4024+
});
4025+
4026+
await expect(
4027+
makeClient().revokeToken({ token: '<refresh_token>', tokenTypeHint: 'refresh_token' })
4028+
).resolves.toBeUndefined();
4029+
expect(capturedToken).toBe('<refresh_token>');
4030+
expect(capturedHint).toBe('refresh_token');
4031+
});
4032+
4033+
test('should revoke a token without tokenTypeHint', async () => {
4034+
let capturedHint: FormDataEntryValue | null = null;
4035+
setupRevocationHandlers(async ({ request }) => {
4036+
const body = await request.formData();
4037+
capturedHint = body.get('token_type_hint');
4038+
return new HttpResponse(null, { status: 200 });
4039+
});
4040+
4041+
await expect(
4042+
makeClient().revokeToken({ token: '<refresh_token>' })
4043+
).resolves.toBeUndefined();
4044+
expect(capturedHint).toBeNull();
4045+
});
4046+
4047+
test('should throw TokenRevocationError when revocation fails', async () => {
4048+
setupRevocationHandlers(() =>
4049+
HttpResponse.json(
4050+
{ error: '<error_code>', error_description: '<error_description>' },
4051+
{ status: 400 }
4052+
)
4053+
);
4054+
4055+
await expect(
4056+
makeClient().revokeToken({ token: '<invalid_token>' })
4057+
).rejects.toThrowError(
4058+
expect.objectContaining({
4059+
code: 'token_revocation_error',
4060+
message: 'An error occurred while trying to revoke the token.',
4061+
cause: expect.objectContaining({
4062+
error: '<error_code>',
4063+
error_description: '<error_description>',
4064+
}),
4065+
})
4066+
);
4067+
});
4068+
4069+
test('should send client credentials on the revocation request', async () => {
4070+
let capturedClientId: string | null = null;
4071+
let capturedClientSecret: string | null = null;
4072+
setupRevocationHandlers(async ({ request }) => {
4073+
const body = await request.formData();
4074+
capturedClientId = body.get('client_id') as string;
4075+
capturedClientSecret = body.get('client_secret') as string;
4076+
return new HttpResponse(null, { status: 200 });
4077+
});
4078+
4079+
await makeClient().revokeToken({ token: '<refresh_token>', tokenTypeHint: 'refresh_token' });
4080+
4081+
expect(capturedClientId).toBe('<client_id>');
4082+
expect(capturedClientSecret).toBe('<client_secret>');
4083+
});
4084+
});

packages/auth0-auth-js/src/auth-client.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
BuildLinkUserUrlError,
88
BuildUnlinkUserUrlError,
99
TokenExchangeError,
10+
TokenRevocationError,
1011
MissingClientAuthError,
1112
NotSupportedError,
1213
NotSupportedErrorCode,
@@ -46,6 +47,7 @@ import {
4647
TokenByPasswordlessEmailOptions,
4748
TokenByPasswordlessSmsOptions,
4849
TokenByRefreshTokenOptions,
50+
RevokeTokenOptions,
4951
TokenForConnectionOptions,
5052
TokenResponse,
5153
ActClaim,
@@ -1113,6 +1115,27 @@ export class AuthClient {
11131115
}
11141116
}
11151117

1118+
/**
1119+
* Revokes a token at the Auth0 /oauth/revoke endpoint.
1120+
*
1121+
* @throws {TokenRevocationError} If the revocation request fails.
1122+
*/
1123+
public async revokeToken(options: RevokeTokenOptions): Promise<void> {
1124+
const { configuration } = await this.#discover();
1125+
const params: Record<string, string> = {};
1126+
if (options.tokenTypeHint) {
1127+
params['token_type_hint'] = options.tokenTypeHint;
1128+
}
1129+
try {
1130+
await client.tokenRevocation(configuration, options.token, params);
1131+
} catch (e) {
1132+
throw new TokenRevocationError(
1133+
'An error occurred while trying to revoke the token.',
1134+
toOAuth2Error(e)
1135+
);
1136+
}
1137+
}
1138+
11161139
/**
11171140
* Retrieves a token using Resource Owner Password Grant.
11181141
* @param options Options for authenticating with username and password.

packages/auth0-auth-js/src/errors.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ export class TokenExchangeError extends ApiError {
171171
}
172172
}
173173

174+
/**
175+
* Error thrown when revoking a token fails.
176+
*/
177+
export class TokenRevocationError extends ApiError {
178+
constructor(message: string, cause?: OAuth2Error) {
179+
super('token_revocation_error', message, cause);
180+
this.name = 'TokenRevocationError';
181+
}
182+
}
183+
174184
/**
175185
* Error thrown when verifying the logout token.
176186
*/

packages/auth0-auth-js/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,13 @@ export interface TokenVaultExchangeOptions {
605605
extra?: Record<string, string | string[]>;
606606
}
607607

608+
export interface RevokeTokenOptions {
609+
/** The token to revoke. */
610+
token: string;
611+
/** Hint about the token type. Per RFC 7009. */
612+
tokenTypeHint?: 'refresh_token' | 'access_token';
613+
}
614+
608615
export interface BuildLogoutUrlOptions {
609616
/**
610617
* The URL to which the user should be redirected after the logout.

0 commit comments

Comments
 (0)