|
10 | 10 | // * `grant_type=authorization_code` for vanilla exchange-a-code-for-a-token OAuth |
11 | 11 | // * `grant_type=refresh_token` for refreshing a previously-granted token |
12 | 12 | // * `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 |
13 | 14 | // |
14 | 15 | // And because of the different types of token that can be requested: |
15 | 16 | // |
@@ -239,15 +240,18 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => { |
239 | 240 | requestedGrant = await validateAssertionGrant(client, params); |
240 | 241 | break; |
241 | 242 | case GRANT_TOKEN_EXCHANGE: |
242 | | - requestedGrant = await validateTokenExchangeGrant(client, params); |
| 243 | + requestedGrant = await validateTokenExchangeGrant(params); |
243 | 244 | break; |
244 | 245 | default: |
245 | 246 | // Joi validation means this should never happen. |
246 | 247 | throw Error('unreachable'); |
247 | 248 | } |
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 | + } |
251 | 255 | requestedGrant.grantType = params.grant_type; |
252 | 256 | requestedGrant.ppidSeed = params.ppid_seed; |
253 | 257 | requestedGrant.resource = params.resource; |
@@ -404,7 +408,7 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => { |
404 | 408 | * This check happens on their side, and for now we will grant the request. |
405 | 409 | * See FXA-12925 |
406 | 410 | */ |
407 | | - async function validateTokenExchangeGrant(client, params) { |
| 411 | + async function validateTokenExchangeGrant(params) { |
408 | 412 | const subjectToken = await oauthDB.getRefreshToken( |
409 | 413 | encrypt.hash(params.subject_token) |
410 | 414 | ); |
@@ -459,20 +463,26 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => { |
459 | 463 |
|
460 | 464 | async function tokenHandler(req) { |
461 | 465 | var params = req.payload; |
462 | | - const client = await authenticateClient(req.headers, params); |
463 | 466 |
|
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 | + } |
474 | 483 | } |
475 | 484 | const grant = await validateGrantParameters(client, params); |
| 485 | + |
476 | 486 | const tokens = await generateTokens(grant); |
477 | 487 |
|
478 | 488 | // For token exchange, revoke the original refresh token after successful generation |
@@ -508,10 +518,9 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => { |
508 | 518 | glean.oauth.tokenCreated(req, { |
509 | 519 | uid, |
510 | 520 | oauthClientId, |
511 | | - reason: req.payload?.grant_type || '', |
| 521 | + reason: params.grant_type, |
512 | 522 | }); |
513 | 523 |
|
514 | | - // the client receiving keys at the end of the scoped keys flow |
515 | 524 | if (tokens.keys_jwe) { |
516 | 525 | statsd.increment('oauth.rp.keys-jwe', { clientId: oauthClientId }); |
517 | 526 | } |
@@ -641,8 +650,6 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => { |
641 | 650 | .valid(SUBJECT_TOKEN_TYPE_REFRESH) |
642 | 651 | .required(), |
643 | 652 | scope: validators.scope.required(), |
644 | | - ttl: Joi.number().positive().optional(), |
645 | | - resource: validators.resourceUrl.optional(), |
646 | 653 | }) |
647 | 654 | ), |
648 | 655 | }, |
@@ -690,6 +697,7 @@ module.exports = ({ log, oauthDB, db, mailer, devices, statsd, glean }) => { |
690 | 697 | expires_in: Joi.number().required(), |
691 | 698 | }), |
692 | 699 | // token exchange |
| 700 | + // Does not require client_id because the client is identified from the subject_token |
693 | 701 | Joi.object({ |
694 | 702 | access_token: validators.accessToken.required(), |
695 | 703 | refresh_token: validators.refreshToken.required(), |
|
0 commit comments