Skip to content

Commit 0070b1c

Browse files
authored
Merge pull request #19948 from mozilla/FXA-12925.1
fix(auth): For token exchange grant, use client_id from subject_token
2 parents 8171cda + 203fbc4 commit 0070b1c

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)