Skip to content

Commit f2904f5

Browse files
committed
fix(token-revocation): accept non-JSON revocation responses
HttpClient defaults to responseType 'json', so revokeTokenAndLogout() failed whenever the revocation endpoint returned a non-JSON body. RFC 7009 section 2.2 states the response body must be ignored, so request it as text instead. Closes #1505
1 parent b40cf6d commit f2904f5

2 files changed

Lines changed: 88 additions & 12 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { HttpParams, provideHttpClient } from '@angular/common/http';
2+
import {
3+
HttpTestingController,
4+
provideHttpClientTesting,
5+
} from '@angular/common/http/testing';
6+
import { TestBed } from '@angular/core/testing';
7+
8+
import { OAuthService } from './oauth-service';
9+
import { provideOAuthClient } from './provider';
10+
import { MemoryStorage, OAuthLogger, OAuthStorage } from './types';
11+
12+
const silentLogger: OAuthLogger = {
13+
debug: () => undefined,
14+
info: () => undefined,
15+
log: () => undefined,
16+
warn: () => undefined,
17+
error: () => undefined,
18+
};
19+
20+
describe('OAuthService', () => {
21+
let httpMock: HttpTestingController;
22+
let service: OAuthService;
23+
let storage: OAuthStorage;
24+
25+
beforeEach(() => {
26+
TestBed.configureTestingModule({
27+
providers: [
28+
provideHttpClient(),
29+
provideHttpClientTesting(),
30+
provideOAuthClient(),
31+
{ provide: OAuthLogger, useValue: silentLogger },
32+
{ provide: OAuthStorage, useClass: MemoryStorage },
33+
],
34+
});
35+
36+
httpMock = TestBed.inject(HttpTestingController);
37+
service = TestBed.inject(OAuthService);
38+
storage = TestBed.inject(OAuthStorage);
39+
40+
service.configure({
41+
clientId: 'test-client',
42+
revocationEndpoint: 'https://auth.example.com/oauth/revoke',
43+
});
44+
storage.setItem('access_token', 'access-token');
45+
storage.setItem('refresh_token', 'refresh-token');
46+
});
47+
48+
afterEach(() => {
49+
httpMock.verify();
50+
});
51+
52+
it('should accept plain text token revocation responses', async () => {
53+
const revokePromise = service.revokeTokenAndLogout();
54+
55+
const requests = httpMock.match('https://auth.example.com/oauth/revoke');
56+
expect(requests.length).toBe(2);
57+
expect(
58+
requests.map((request) =>
59+
(request.request.body as HttpParams).get('token_type_hint')
60+
)
61+
).toEqual(['access_token', 'refresh_token']);
62+
63+
for (const request of requests) {
64+
expect(request.request.method).toBe('POST');
65+
expect(request.request.responseType).toBe('text');
66+
expect(request.request.headers.get('Content-Type')).toBe(
67+
'application/x-www-form-urlencoded'
68+
);
69+
request.flush('Token revoked');
70+
}
71+
72+
await expectAsync(revokePromise).toBeResolved();
73+
});
74+
});

projects/lib/src/oauth-service.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2814,18 +2814,20 @@ export class OAuthService extends AuthConfig implements OnDestroy {
28142814
}
28152815

28162816
return new Promise((resolve, reject) => {
2817-
let revokeAccessToken: Observable<void>;
2818-
let revokeRefreshToken: Observable<void>;
2817+
let revokeAccessToken: Observable<unknown>;
2818+
let revokeRefreshToken: Observable<unknown>;
28192819

28202820
if (accessToken) {
28212821
const revokationParams = params
28222822
.set('token', accessToken)
28232823
.set('token_type_hint', 'access_token');
2824-
revokeAccessToken = this.http.post<void>(
2825-
revokeEndpoint,
2826-
revokationParams,
2827-
{ headers }
2828-
);
2824+
// Per RFC 7009 section 2.2 the response body is to be ignored, so we
2825+
// request it as text to avoid HttpClient's default JSON parsing failing
2826+
// on non-JSON (e.g. empty or plain-text) revocation responses.
2827+
revokeAccessToken = this.http.post(revokeEndpoint, revokationParams, {
2828+
headers,
2829+
responseType: 'text',
2830+
});
28292831
} else {
28302832
revokeAccessToken = of(null);
28312833
}
@@ -2834,11 +2836,11 @@ export class OAuthService extends AuthConfig implements OnDestroy {
28342836
const revokationParams = params
28352837
.set('token', refreshToken)
28362838
.set('token_type_hint', 'refresh_token');
2837-
revokeRefreshToken = this.http.post<void>(
2838-
revokeEndpoint,
2839-
revokationParams,
2840-
{ headers }
2841-
);
2839+
// See note above: ignore the response body (RFC 7009 section 2.2).
2840+
revokeRefreshToken = this.http.post(revokeEndpoint, revokationParams, {
2841+
headers,
2842+
responseType: 'text',
2843+
});
28422844
} else {
28432845
revokeRefreshToken = of(null);
28442846
}

0 commit comments

Comments
 (0)