Skip to content

Commit 203fbc4

Browse files
committed
fix(auth): For token exchange grant, use client_id from subject_token
Because: * With this grant, we are currently calling 'authenticateClient' with the request query parameters. This type of token exchange does not need to look up the client by query param because we discern it from the 'subject_token' passed to us and validate against our allowlist in config instead. This commit: * Updates tokenHandler for the new grant type accordingly follow on to FXA-12925
1 parent ef21bad commit 203fbc4

2 files changed

Lines changed: 30 additions & 84 deletions

File tree

packages/fxa-auth-server/lib/routes/oauth/token.js

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
// * `grant_type=authorization_code` for vanilla exchange-a-code-for-a-token OAuth
1111
// * `grant_type=refresh_token` for refreshing a previously-granted token
1212
// * `grant_type=fxa-credentials` for directly granting via an FxA identity assertion
13+
// * `grant_type=urn:ietf:params:oauth:grant-type:token-exchange` for token exchange, e.g. refresh token for a new refresh token
1314
//
1415
// And because of the different types of token that can be requested:
1516
//
@@ -239,15 +240,18 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => {
239240
requestedGrant = await validateAssertionGrant(client, params);
240241
break;
241242
case GRANT_TOKEN_EXCHANGE:
242-
requestedGrant = await validateTokenExchangeGrant(client, params);
243+
requestedGrant = await validateTokenExchangeGrant(params);
243244
break;
244245
default:
245246
// Joi validation means this should never happen.
246247
throw Error('unreachable');
247248
}
248-
requestedGrant.name = client.name;
249-
requestedGrant.canGrant = client.canGrant;
250-
requestedGrant.publicClient = client.publicClient;
249+
// Token exchange gets client info from the subject_token, not from client auth
250+
if (client) {
251+
requestedGrant.name = client.name;
252+
requestedGrant.canGrant = client.canGrant;
253+
requestedGrant.publicClient = client.publicClient;
254+
}
251255
requestedGrant.grantType = params.grant_type;
252256
requestedGrant.ppidSeed = params.ppid_seed;
253257
requestedGrant.resource = params.resource;
@@ -404,7 +408,7 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => {
404408
* This check happens on their side, and for now we will grant the request.
405409
* See FXA-12925
406410
*/
407-
async function validateTokenExchangeGrant(client, params) {
411+
async function validateTokenExchangeGrant(params) {
408412
const subjectToken = await oauthDB.getRefreshToken(
409413
encrypt.hash(params.subject_token)
410414
);
@@ -459,20 +463,26 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => {
459463

460464
async function tokenHandler(req) {
461465
var params = req.payload;
462-
const client = await authenticateClient(req.headers, params);
463466

464-
// Refuse to generate new access tokens for disabled clients that are already
465-
// connected to the account. We allow disabled clients to claim existing authorization
466-
// codes, because otherwise we risk erroring out halfway through an app login flow
467-
// and presenting a very confusing user experience. The /authorization endpoint refuses
468-
// to create new codes for disabled clients.
469-
if (
470-
DISABLED_CLIENTS.has(hex(client.id)) &&
471-
params.grant_type !== GRANT_AUTHORIZATION_CODE
472-
) {
473-
throw OauthError.disabledClient(hex(client.id));
467+
// Token exchange doesn't require client authentication since the
468+
// subject_token is already bound to an allowed client.
469+
let client = null;
470+
if (params.grant_type !== GRANT_TOKEN_EXCHANGE) {
471+
client = await authenticateClient(req.headers, params);
472+
// Refuse to generate new access tokens for disabled clients that are already
473+
// connected to the account. We allow disabled clients to claim existing authorization
474+
// codes, because otherwise we risk erroring out halfway through an app login flow
475+
// and presenting a very confusing user experience. The /authorization endpoint refuses
476+
// to create new codes for disabled clients.
477+
if (
478+
DISABLED_CLIENTS.has(hex(client.id)) &&
479+
params.grant_type !== GRANT_AUTHORIZATION_CODE
480+
) {
481+
throw OauthError.disabledClient(hex(client.id));
482+
}
474483
}
475484
const grant = await validateGrantParameters(client, params);
485+
476486
const tokens = await generateTokens(grant);
477487

478488
// For token exchange, revoke the original refresh token after successful generation
@@ -508,10 +518,9 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => {
508518
glean.oauth.tokenCreated(req, {
509519
uid,
510520
oauthClientId,
511-
reason: req.payload?.grant_type || '',
521+
reason: params.grant_type,
512522
});
513523

514-
// the client receiving keys at the end of the scoped keys flow
515524
if (tokens.keys_jwe) {
516525
statsd.increment('oauth.rp.keys-jwe', { clientId: oauthClientId });
517526
}
@@ -641,8 +650,6 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => {
641650
.valid(SUBJECT_TOKEN_TYPE_REFRESH)
642651
.required(),
643652
scope: validators.scope.required(),
644-
ttl: Joi.number().positive().optional(),
645-
resource: validators.resourceUrl.optional(),
646653
})
647654
),
648655
},
@@ -690,6 +697,7 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => {
690697
expires_in: Joi.number().required(),
691698
}),
692699
// token exchange
700+
// Does not require client_id because the client is identified from the subject_token
693701
Joi.object({
694702
access_token: validators.accessToken.required(),
695703
refresh_token: validators.refreshToken.required(),

packages/fxa-auth-server/test/oauth/routes/token.js

Lines changed: 2 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,6 @@ describe('token exchange grant_type', function () {
473473
const request = {
474474
headers: {},
475475
payload: {
476-
client_id: FIREFOX_IOS_CLIENT_ID,
477476
grant_type: GRANT_TOKEN_EXCHANGE,
478477
subject_token: REFRESH_TOKEN,
479478
subject_token_type: SUBJECT_TOKEN_TYPE_REFRESH,
@@ -511,7 +510,6 @@ describe('token exchange grant_type', function () {
511510
const request = {
512511
headers: {},
513512
payload: {
514-
client_id: NON_FIREFOX_CLIENT_ID,
515513
grant_type: GRANT_TOKEN_EXCHANGE,
516514
subject_token: REFRESH_TOKEN,
517515
subject_token_type: SUBJECT_TOKEN_TYPE_REFRESH,
@@ -551,7 +549,6 @@ describe('token exchange grant_type', function () {
551549
const request = {
552550
headers: {},
553551
payload: {
554-
client_id: FIREFOX_IOS_CLIENT_ID,
555552
grant_type: GRANT_TOKEN_EXCHANGE,
556553
subject_token: REFRESH_TOKEN,
557554
subject_token_type: SUBJECT_TOKEN_TYPE_REFRESH,
@@ -613,7 +610,6 @@ describe('token exchange grant_type', function () {
613610
const request = {
614611
headers: {},
615612
payload: {
616-
client_id: FIREFOX_IOS_CLIENT_ID,
617613
grant_type: GRANT_TOKEN_EXCHANGE,
618614
subject_token: REFRESH_TOKEN,
619615
subject_token_type: SUBJECT_TOKEN_TYPE_REFRESH,
@@ -628,65 +624,8 @@ describe('token exchange grant_type', function () {
628624
assert.equal(result.refresh_token, 'new_refresh_token');
629625
assert.include(result.scope, SYNC_SCOPE);
630626
assert.include(result.scope, RELAY_SCOPE);
631-
// Verify original token was revoked
632-
assert.isNotNull(removedTokenId);
633-
});
634-
635-
it('revokes original token after successful exchange', async () => {
636-
let removedTokenId = null;
637-
const originalTokenId = '1234567890abcdef';
638-
639-
const routes = proxyquire(tokenRoutePath, {
640-
...tokenRoutesDepMocks,
641-
'../../oauth/grant': {
642-
generateTokens: (grant) => ({
643-
access_token: 'new_access_token',
644-
token_type: 'bearer',
645-
scope: grant.scope.toString(),
646-
expires_in: 3600,
647-
refresh_token: 'new_refresh_token',
648-
}),
649-
validateRequestedGrant: () => ({ offline: true, scope: 'testo' }),
650-
},
651-
})({
652-
...tokenRoutesArgMocks,
653-
log: {
654-
debug: () => {},
655-
warn: () => {},
656-
info: () => {},
657-
},
658-
oauthDB: {
659-
...tokenRoutesArgMocks.oauthDB,
660-
async getRefreshToken() {
661-
return {
662-
userId: buf(UID),
663-
clientId: buf(FIREFOX_IOS_CLIENT_ID),
664-
tokenId: buf(originalTokenId),
665-
scope: ScopeSet.fromString(SYNC_SCOPE),
666-
profileChangedAt: Date.now(),
667-
};
668-
},
669-
async removeRefreshToken({ tokenId }) {
670-
removedTokenId = hex(tokenId);
671-
},
672-
},
673-
});
674-
675-
const request = {
676-
headers: {},
677-
payload: {
678-
client_id: FIREFOX_IOS_CLIENT_ID,
679-
grant_type: GRANT_TOKEN_EXCHANGE,
680-
subject_token: REFRESH_TOKEN,
681-
subject_token_type: SUBJECT_TOKEN_TYPE_REFRESH,
682-
scope: RELAY_SCOPE,
683-
},
684-
emitMetricsEvent: () => {},
685-
};
686-
687-
await routes[0].config.handler(request);
688-
689-
assert.equal(removedTokenId, originalTokenId);
627+
// Verify original token was revoked with correct ID
628+
assert.equal(hex(removedTokenId), '1234567890abcdef');
690629
});
691630
});
692631
});
@@ -795,7 +734,6 @@ describe('/oauth/token POST', function () {
795734
auth: { credentials: null },
796735
headers: {},
797736
payload: {
798-
client_id: FIREFOX_IOS_CLIENT_ID,
799737
grant_type: GRANT_TOKEN_EXCHANGE,
800738
subject_token: REFRESH_TOKEN,
801739
subject_token_type: SUBJECT_TOKEN_TYPE_REFRESH,

0 commit comments

Comments
 (0)