From faa7924acfad5527fc0d0bc7d88b6f27b788d530 Mon Sep 17 00:00:00 2001 From: Lisa Chan Date: Fri, 27 May 2022 20:26:09 -0400 Subject: [PATCH] docs(Swagger): Add Firefox Accounts OAuth Server API documenation to Swagger docs --- packages/fxa-auth-server/docs/api.md | 450 +------------- packages/fxa-auth-server/docs/oauth/api.md | 580 +----------------- .../docs/swagger/auth-server-api.ts | 298 +++++++++ .../fxa-auth-server/docs/swagger/misc-api.ts | 66 -- .../docs/swagger/oauth-server-api.ts | 209 +++++++ .../docs/swagger/shared/descriptions.ts | 90 ++- .../docs/swagger/swagger-options.ts | 91 ++- .../docs/swagger/swagger-tags.ts | 2 + .../lib/routes/oauth/authorization.js | 96 +-- .../oauth/authorized-clients/destroy.js | 17 +- .../routes/oauth/authorized-clients/list.js | 37 +- .../lib/routes/oauth/client/get.js | 26 +- .../lib/routes/oauth/destroy.js | 20 +- .../fxa-auth-server/lib/routes/oauth/index.js | 1 + .../lib/routes/oauth/introspect.js | 38 +- .../fxa-auth-server/lib/routes/oauth/jwks.js | 5 +- .../lib/routes/oauth/key_data.js | 15 +- .../fxa-auth-server/lib/routes/oauth/token.js | 104 ++-- .../lib/routes/oauth/verify.js | 17 +- 19 files changed, 873 insertions(+), 1289 deletions(-) create mode 100644 packages/fxa-auth-server/docs/swagger/auth-server-api.ts create mode 100644 packages/fxa-auth-server/docs/swagger/oauth-server-api.ts diff --git a/packages/fxa-auth-server/docs/api.md b/packages/fxa-auth-server/docs/api.md index 0b3a6aada71..cc3aecb7db3 100644 --- a/packages/fxa-auth-server/docs/api.md +++ b/packages/fxa-auth-server/docs/api.md @@ -1,449 +1 @@ -# Firefox Accounts authentication server API - -**WARNING:** -Some of this information is wrong, -use it at your own risk. -It may be worth verifying things in the source code -before acting on anything you read here. - - - -This document provides protocol-level details -of the Firefox Accounts auth server API. -For a prose description of the client/server protocol -and details on how each parameter is derived, -see the [API design document](https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol). -For a reference client implementation, -see [`fxa-auth-client`](https://github.com/mozilla/fxa/tree/main/packages/fxa-auth-client). - - - -- [Overview](#overview) - - [URL structure](#url-structure) - - [Request format](#request-format) - - [Response format](#response-format) - - [Defined errors](#defined-errors) - - [Responses from intermediary servers](#responses-from-intermediary-servers) - - [Validation](#validation) -- [Back-off protocol](#back-off-protocol) - -## Overview - -### URL structure - - - -All requests use URLs of the form: - -``` -https:///v1/ -``` - -Note that: - -- All API access must be over a properly-validated HTTPS connection. -- The URL embeds a version identifier `v1`. - Future revisions of this API may introduce new version numbers. -- The base URI of the server may be configured on a per-client basis: - - For a list of development servers - see [Firefox Accounts deployments on MDN](https://developer.mozilla.org/en-US/Firefox_Accounts#Firefox_Accounts_deployments). - - The canonical URL for Mozilla's hosted Firefox Accounts server - is `https://api.accounts.firefox.com/v1`. - - -### Request format - - - -Requests that require authentication -use [Hawk](https://github.com/hapijs/hawk) request signatures. -These endpoints are marked -with a :lock: icon. -Where the authentication is optional, -there will also be a :question: icon. - -All POST requests must have a content-type of `application/json` -with a UTF8-encoded JSON body -and must specify the content-length header. -Keys and other binary data are included in the JSON -as hexadecimal strings. - -The following request headers may be specified -to influence the behaviour of the server: - -- `Accept-Language` - may be used to localize - emails and SMS messages. - - -### Response format - - - -All requests receive -a JSON response body -with a `Content-Type: application/json` header -and appropriate `Content-Length` set. -The body structure -depends on the endpoint returning it. - -Successful responses will have -an HTTP status code of 200 -and a `Timestamp` header -that contains the current server time -in seconds since the epoch. - -Error responses caused by invalid client behaviour -will have an HTTP status code in the 4xx range. -Error responses caused by server-side problems -will have an HTTP status code in the 5xx range. -Failures due to invalid behavior from the client - -To simplify error handling for the client, -the type of error is indicated by both -a defined HTTP status code -and an application-specific `errno` in the body. -For example: - -```js -{ - "code": 400, // Matches the HTTP status code - "errno": 107, // Stable application-level error number - "error": "Bad Request", // String description of the error type - "message": "Invalid parameter in request body", // Specific error message - "info": "https://docs.dev.lcip.og/errors/1234" // Link to more information -} -``` - -Responses for some errors may include additional parameters. - - - -#### Defined errors - -The currently-defined values -for `code` and `errno` are: - -- `code: 400, errno: 100`: - Incorrect Database Patch Level -- `code: 400, errno: 101`: - Account already exists -- `code: 400, errno: 102`: - Unknown account -- `code: 400, errno: 103`: - Incorrect password -- `code: 400, errno: 104`: - Unverified account -- `code: 400, errno: 105`: - Invalid verification code -- `code: 400, errno: 106`: - Invalid JSON in request body -- `code: 400, errno: 107`: - Invalid parameter in request body -- `code: 400, errno: 108`: - Missing parameter in request body -- `code: 401, errno: 109`: - Invalid request signature -- `code: 401, errno: 110`: - Invalid authentication token in request signature -- `code: 401, errno: 111`: - Invalid timestamp in request signature -- `code: 411, errno: 112`: - Missing content-length header -- `code: 413, errno: 113`: - Request body too large -- `code: 429, errno: 114`: - Client has sent too many requests -- `code: 401, errno: 115`: - Invalid nonce in request signature -- `code: 410, errno: 116`: - This endpoint is no longer supported -- `code: 400, errno: 120`: - Incorrect email case -- `code: 400, errno: 123`: - Unknown device -- `code: 400, errno: 124`: - Session already registered by another device -- `code: 400, errno: 125`: - The request was blocked for security reasons -- `code: 400, errno: 126`: - Account must be reset -- `code: 400, errno: 127`: - Invalid unblock code -- `code: 400, errno: 129`: - Invalid phone number -- `code: 400, errno: 130`: - Invalid region -- `code: 400, errno: 131`: - Invalid message id -- `code: 500, errno: 132`: - Message rejected -- `code: 400, errno: 133`: - Email account sent complaint -- `code: 400, errno: 134`: - Email account hard bounced -- `code: 400, errno: 135`: - Email account soft bounced -- `code: 400, errno: 136`: - Email already exists -- `code: 400, errno: 137`: - Can not delete primary email -- `code: 400, errno: 138`: - Unverified session -- `code: 400, errno: 139`: - Can not add secondary email that is same as your primary -- `code: 400, errno: 140`: - Email already exists -- `code: 400, errno: 141`: - Email already exists -- `code: 400, errno: 142`: - Sign in with this email type is not currently supported -- `code: 400, errno: 143`: - Unknown email -- `code: 400, errno: 144`: - Email already exists -- `code: 400, errno: 145`: - Reset password with this email type is not currently supported -- `code: 400, errno: 146`: - Invalid signin code -- `code: 400, errno: 147`: - Can not change primary email to an unverified email -- `code: 400, errno: 148`: - Can not change primary email to an email that does not belong to this account -- `code: 400, errno: 149`: - This email can not currently be used to login -- `code: 400, errno: 150`: - Can not resend email code to an email that does not belong to this account -- `code: 500, errno: 151`: - Failed to send email -- `code: 422, errno: 151`: - Failed to send email -- `code: 400, errno: 152`: - Invalid token verification code -- `code: 400, errno: 153`: - Expired token verification code -- `code: 400, errno: 154`: - TOTP token already exists for this account. -- `code: 400, errno: 155`: - TOTP token not found. -- `code: 400, errno: 156`: - Recovery code not found. -- `code: 400, errno: 157`: - Unavailable device command. -- `code: 400, errno: 158`: - Recovery key not found. -- `code: 400, errno: 159`: - Recovery key is not valid. -- `code: 400, errno: 160`: - This request requires two step authentication enabled on your account. -- `code: 400, errno: 161`: - Recovery key already exists. -- `code: 400, errno: 162`: - Unknown client_id -- `code: 400, errno: 164`: - Stale auth timestamp -- `code: 409, errno: 165`: - Redis WATCH detected a conflicting update -- `code: 400, errno: 166`: - Not a public client -- `code: 400, errno: 167`: - Incorrect redirect URI -- `code: 400, errno: 168`: - Invalid response_type -- `code: 400, errno: 169`: - Requested scopes are not allowed -- `code: 400, errno: 170`: - Public clients require PKCE OAuth parameters -- `code: 400, errno: 171`: - Required Authentication Context Reference values could not be satisfied -- `code: 404, errno: 176`: - Unknown subscription -- `code: 400, errno: 177`: - Unknown subscription plan -- `code: 400, errno: 178`: - Subscription payment token rejected -- `code: 503, errno: 201`: - Service unavailable -- `code: 503, errno: 202`: - Feature not enabled -- `code: 500, errno: 203`: - A backend service request failed. -- `code: 500, errno: 998`: - An internal validation check failed. - -The following errors -include additional response properties: - -- `errno: 100`: level, levelRequired -- `errno: 101`: email -- `errno: 102`: email -- `errno: 103`: email -- `errno: 105` -- `errno: 107`: validation -- `errno: 108`: param -- `errno: 111`: serverTime -- `errno: 114`: retryAfter, retryAfterLocalized, verificationMethod, verificationReason -- `errno: 120`: email -- `errno: 124`: deviceId -- `errno: 125`: verificationMethod, verificationReason -- `errno: 126`: email -- `errno: 130`: region -- `errno: 132`: reason, reasonCode -- `errno: 133`: bouncedAt -- `errno: 134`: bouncedAt -- `errno: 135`: bouncedAt -- `errno: 152` -- `errno: 153` -- `errno: 162`: clientId -- `errno: 164`: authAt -- `errno: 167`: redirectUri -- `errno: 169`: invalidScopes -- `errno: 171`: foundValue -- `errno: 201`: retryAfter -- `errno: 202`: retryAfter -- `errno: 203`: service, operation -- `errno: 998`: op, data - -#### Responses from intermediary servers - - - -As with any HTTP-based API, -clients must handle standard errors that may be returned -by proxies, load-balancers or other intermediary servers. -These non-application responses can be identified -by the absence of a correctly-formatted JSON response body. - -Common examples include: - -- `413 Request Entity Too Large`: - may be returned by an upstream proxy server. -- `502 Gateway Timeout`: - may be returned if a load-balancer can't connect to application servers. - - -### Validation - -In the documentation that follows, -some properties of requests and responses -are validated by common code -that has been refactored and extracted. -For reference, -those common validations are defined here. - -#### lib/routes/validators - -- `HEX_STRING`: `/^(?:[a-fA-F0-9]{2})+$/` -- `BASE_36`: `/^[a-zA-Z0-9]*$/` -- `URL_SAFE_BASE_64`: `/^[A-Za-z0-9_-]+$/` -- `PKCE_CODE_VERIFIER`: `/^[A-Za-z0-9-\._~]{43,128}$/` -- `DISPLAY_SAFE_UNICODE`: `/^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uD800-\uDFFF\uE000-\uF8FF\uFFF9-\uFFFF])*$/` -- `DISPLAY_SAFE_UNICODE_WITH_NON_BMP`: `/^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uE000-\uF8FF\uFFF9-\uFFFF])*$/` -- `BEARER_AUTH_REGEX`: `/^Bearer\s+([a-z0-9+\/]+)$/i` -- `service`: `string, max(16), regex(/^[a-zA-Z0-9\-]*$/)` -- `hexString`: `string, regex(/^(?:[a-fA-F0-9]{2})+$/)` -- `clientId`: `module.exports.hexString.length(16)` -- `clientSecret`: `module.exports.hexString` -- `accessToken`: `module.exports.hexString.length(64)` -- `refreshToken`: `module.exports.hexString.length(64)` -- `authorizationCode`: `module.exports.hexString.length(64)` -- `scope`: `string, max(256), regex(/^[a-zA-Z0-9 _\/.:-]*$/), allow('')` -- `assertion`: `string, min(50), max(10240), regex(/^[a-zA-Z0-9_\-\.~=]+$/)` -- `pkceCodeChallengeMethod`: `string, valid('S256')` -- `pkceCodeChallenge`: `string, length(43), regex(module, exports.URL_SAFE_BASE_64)` -- `pkceCodeVerifier`: `string, length(43), regex(module, exports.PKCE_CODE_VERIFIER)` -- `jwe`: `string, max(1024), regex(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/)` -- `verificationMethod`: `string, valid()` -- `authPW`: `string, length(64), regex(HEX_STRING), required` -- `wrapKb`: `string, length(64), regex(/^(?:[a-fA-F0-9]{2})+$/)` -- `recoveryKeyId`: `string, regex(HEX_STRING), max(32)` -- `recoveryData`: `string, regex(/[a-zA-Z0-9.]/), max(1024), required` -- `E164_NUMBER`: `/^\+[1-9]\d{1,14}$/` -- `DIGITS`: `/^[0-9]+$/` -- `DEVICE_COMMAND_NAME`: `/^[a-zA-Z0-9._\/\-:]{1,100}$/` -- `IP_ADDRESS`: `string, ip` - -#### lib/metrics/context - -- `SCHEMA`: object({ - - `deviceId`: string, length(32), regex(HEX_STRING), optional - - `entrypoint`: ENTRYPOINT_SCHEMA.optional - - `entrypointExperiment`: ENTRYPOINT_SCHEMA.optional - - `entrypointVariation`: ENTRYPOINT_SCHEMA.optional - - `flowId`: string, length(64), regex(HEX_STRING), optional - - `flowBeginTime`: number, integer, positive, optional - - `utmCampaign`: UTM_CAMPAIGN_SCHEMA.optional - - `utmContent`: UTM_SCHEMA.optional - - `utmMedium`: UTM_SCHEMA.optional - - `utmSource`: UTM_SCHEMA.optional - - `utmTerm`: UTM_SCHEMA.optional - }), unknown(false), and('flowId', 'flowBeginTime') -- `schema`: SCHEMA.optional -- `requiredSchema`: SCHEMA.required - -#### lib/features - -- `schema`: array, items(string), optional - -#### lib/devices - -- `schema`: { - - - `id`: isA.string.length(32).regex(HEX_STRING) - - `location`: isA.object({ - - `city`: isA.string.optional.allow(null) - - `country`: isA.string.optional.allow(null) - - `state`: isA.string.optional.allow(null) - - `stateCode`: isA.string.optional.allow(null) - - }) - - `name`: isA.string.max(255).regex(DISPLAY_SAFE_UNICODE_WITH_NON_BMP) - - `nameResponse`: isA.string.max(255).allow('') - - `type`: isA.string.max(16) - - `pushCallback`: validators.pushCallbackUrl({ scheme: 'https' }).regex(PUSH_SERVER_REGEX).max(255).allow('') - - `pushPublicKey`: isA.string.max(88).regex(URL_SAFE_BASE_64).allow('') - - `pushAuthKey`: isA.string.max(24).regex(URL_SAFE_BASE_64).allow('') - - `pushEndpointExpired`: isA.boolean.strict - - `availableCommands`: isA.object.pattern(validators.DEVICE_COMMAND_NAME - - `isA.string.max(2048)) - - } - -## Back-off protocol - - - -During periods of heavy load, -the server may request that clients enter a "back-off" state, -in which they avoid making further requests. - -At such times, -it will return a `503 Service Unavailable` response -with a `Retry-After` header denoting the number of seconds to wait -before issuing any further requests. -It will also include `errno: 201` -and a `retryAfter` field -matching the value of the `Retry-After` header -in the body. - -For example, -the following response indicates that the client -should suspend making further requests -for 30 seconds: - -``` -HTTP/1.1 503 Service Unavailable -Retry-After: 30 -Content-Type: application/json - -{ - "code": 503, - "errno": 201, - "error": "Service Unavailable", - "message": "Service unavailable", - "info": "https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/api.md#response-format", - "retryAfter": 30, - "retryAfterLocalized": "in a few seconds" -} -``` - - +# [Firefox Accounts Authentication Server API](https://mozilla.github.io/ecosystem-platform/api#tag/Auth-Server-API-Overview) diff --git a/packages/fxa-auth-server/docs/oauth/api.md b/packages/fxa-auth-server/docs/oauth/api.md index 9933ae8120f..10b23e59887 100644 --- a/packages/fxa-auth-server/docs/oauth/api.md +++ b/packages/fxa-auth-server/docs/oauth/api.md @@ -1,579 +1 @@ -# Firefox Accounts OAuth Server API - -## Overview - -### URL Structure - -``` -https:///v1/ -``` - -Note that: - -- All API access must be over HTTPS. -- The URL embeds a version identifier "v1"; future revisions of this API may introduce new version numbers. -- The base URL of the server may be configured on a per-client basis. - -### Errors - -Invalid requests will return 4XX responses. Internal failures will return 5XX. Both will include JSON responses describing the error. - -**Example error:** - -```js -{ - "code": 400, // matches the HTTP status code - "errno": 101, // stable application-level error number - "error": "Bad Request", // string description of error type - "message": "Unknown client" -} -``` - -The currently-defined error responses are: - -| status code | errno | description | -| :---------: | :---: | --------------------------------------------- | -| 400 | 101 | unknown client id | -| 400 | 102 | incorrect client secret | -| 400 | 103 | `redirect_uri` doesn't match registered value | -| 401 | 104 | invalid fxa assertion | -| 400 | 105 | unknown code | -| 400 | 106 | incorrect code | -| 400 | 107 | expired code | -| 400 | 108 | invalid token | -| 400 | 109 | invalid request parameter | -| 400 | 110 | invalid response_type | -| 401 | 111 | unauthorized | -| 403 | 112 | forbidden | -| 415 | 113 | invalid content type | -| 400 | 114 | invalid scopes | -| 400 | 115 | expired token | -| 400 | 116 | not a public client | -| 400 | 117 | incorrect code_challenge | -| 400 | 118 | pkce parameters missing | -| 400 | 119 | stale authentication timestamp | -| 400 | 120 | mismatch acr value | -| 400 | 121 | invalid grant_type | -| 500 | 999 | internal server error | - -## API Endpoints - -- [GET /v1/client/:id][client] -- [GET /v1/authorization][redirect] -- [POST /v1/authorization][authorization] -- [POST /v1/token][token] -- [POST /v1/destroy][delete] -- [POST /v1/verify][verify] -- [GET /v1/jwks][jwks] -- [POST /v1/key-data][key-data] -- [POST /v1/authorized-clients][authorized-clients] -- [POST /v1/authorized-clients/destroy][authorized-clients-destroy] -- [POST /v1/introspect][introspect] - -### GET /v1/client/:id - -This endpoint is for the fxa-content-server to retrieve information -about a client to show in its user interface. - -#### Request Parameters - -- `id`: The `client_id` of a client asking for permission. - -**Example:** - -```sh -curl -v "https://oauth.accounts.firefox.com/v1/client/5901bd09376fadaa" -``` - -#### Response - -A valid 200 response will be a JSON blob with the following properties: - -- `name`: A string name of the client. -- `image_uri`: A url to a logo or image that represents the client. -- `redirect_uri`: The url registered to redirect to after successful oauth. -- `trusted`: Whether the client is a trusted internal application. - -**Example:** - -```json -{ - "name": "Where's My Fox", - "image_uri": "https://mozilla.org/firefox.png", - "redirect_uri": "https://wheres.my.firefox.com/oauth", - "trusted": true -} -``` - -### GET /v1/authorization - -This endpoint starts the OAuth flow. A client redirects the user agent -to this url. This endpoint will then redirect to the appropriate -content-server page. - -#### Request Parameters - -- `client_id`: The id returned from client registration. -- `state`: A value that will be returned to the client as-is upon redirection, so that clients can verify the redirect is authentic. -- `redirect_uri`: Optional. If supplied, a string URL of where to redirect afterwards. Must match URL from registration. -- `scope`: Optional. A space-separated list of scopes that the user has authorized. This could be pruned by the user at the confirmation dialog. If this includes the scope `openid`, this will be an OpenID Connect authentication request. -- `access_type`: Optional. If provided, should be `online` or `offline`. `offline` will result in a refresh_token being provided, so that the access_token can be refreshed after it expires. -- `action`: Optional. If provided, should be `email`, `signup`, `signin`, or `force_auth`. Send to improve the user experience. - - If unspecified then Firefox Accounts will try choose intelligently between `signin` and `signup` based on the user's browser state. - - `email` triggers the email-first flow, which uses the email address to determine whether to display signup or signin. This is becoming the **preferred** action and is slowly replacing `signin` and `signup`. - - `signin` triggers the signin flow. (will become deprecated and replaced by `email`) - - `signup` triggers the signup flow. (will become deprecated and replaced by `email`) - - `force_auth` requires the user to sign in using the address specified in `email`. -- `email`: Optional if `action` is `email`, `signup`, `signin`. Optional, but deprecated, if `prompt` is `none` (use `login_hint` instead in this case). Required if `action` is `force_auth`. - - if `action` is `email`, the email address will be used to determine whether to display the signup or signin form, but the user is free to change it. - - If `action` is `signup` or `signin`, the email address will be pre-filled into the account form, but the user is free to change it. - - If `action` is `signin`, the literal string `blank` will force the user to enter an email address and the last signed in email address will be ignored. - - If `action` is `signin` and no email address is specified, the last - signed in email address will be used as the default. - - If `action` is `force_auth`, the user is unable to modify the email - address and is unable to sign up if the address is not registered. -- `id_token_hint`: An OpenID ID Token for the user being logged in silently via `prompt=none`. If this parameter and `login_hint` are both included in a `prompt=none` request, this parameter will be used, and the `login_hint` will be ignored. See the [prompt=none doc][prompt-none] for more info. -- `login_hint`: An alias to `email` -- `prompt`: Specifies whether the Authorization Server prompts the End-User for reauthentication and consent. - - `consent`: The Authorization Server SHOULD prompt the End-User for consent before returning information to the Client. - - `none`: The Authorization Server MUST NOT display any authentication or consent user interface pages. See the [prompt=none doc][prompt-none] for more info. - -**Example:** - -```sh -curl -v "https://oauth.accounts.firefox.com/v1/authorization?client_id=5901bd09376fadaa&state=1234&scope=profile:email&action=signup" -``` - -### POST /v1/authorization - -This endpoint should be used by the fxa-content-server, requesting that -we supply a short-lived code (currently 15 minutes) that will be sent -back to the client. This code will be traded for a token at the -[token][] endpoint. - -#### Request Parameters - -- `client_id`: The id returned from client registration. -- `assertion`: A FxA assertion for the signed-in user. -- `state`: A value that will be returned to the client as-is upon redirection, so that clients can verify the redirect is authentic. -- `access_type`: Optional. A value of `offline` will generate a refresh token along with the access token. -- `acr_values`: Optional. A string-separated list of acr values that the token should have a claim for. Specifying `AAL2` will require the token to have an authentication assurance level >= 2 which corresponds to requiring 2FA. -- `code_challenge_method`: Required if using [PKCE](pkce.md). Must be `S256`, no other value is accepted. -- `code_challenge`: Required if using [PKCE](pkce.md). A minimum length of 43 characters and a maximum length of 128 characters string, encoded as `BASE64URL`. -- `keys_jwe`: Optional. A JWE bundle to be returned to the client when it redeems the authorization code. -- `redirect_uri`: Optional. If supplied, a string URL of where to redirect afterwards. Must match URL from registration. -- `resource`: Optional if `response_type=token`, forbidden if `response_type=code`. Indicates the target service or resource at which access is being requested. Its value must be an absolute URI, and may include a query component but must not include a fragment component. Added to the `aud` claim of JWT access tokens. -- `response_type`: Optional. If supplied, must be either `code` or `token`. `code` is the default. `token` means the implicit grant is desired, and requires that the client have special permission to do so. - - **Note: new implementations should not use `response_type=token`; instead use `grant_type=fxa-credentials` at the [token][] endpoint.** -- `scope`: Optional. A string-separated list of scopes that the user has authorized. This could be pruned by the user at the confirmation dialog. -- `ttl`: Optional if `response_type=token`, forbidden if `response_type=code`. Indicates the requested lifespan in seconds for the implicit grant token. The value is subject to an internal maximum limit, so clients must check the `expires_in` result property for the actual TTL. - -**Example:** - -```sh -curl -v \ --X POST \ --H "Content-Type: application/json" \ -"https://oauth.accounts.firefox.com/v1/authorization" \ --d '{ - "client_id": "5901bd09376fadaa", - "assertion": "", - "state": "1234", - "scope": "profile:email" -}' -``` - -#### Response - -A valid request will return a 200 response, with JSON containing the `redirect` to follow. It will include the following query parameters: - -- `code`: A string that the client will trade with the [token][] endpoint. Codes have a configurable expiration value, default is 15 minutes. Codes are single use only. -- `state`: The same value as was passed as a request parameter. - -**Example:** - -```json -{ - "redirect": "https://example.domain/path?foo=bar&code=4ab433e31ef3a7cf7c20590f047987922b5c9ceb1faff56f0f8164df053dd94c&state=1234" -} -``` - -##### Implicit Grant - -If requesting an implicit grant (token), the response will match the -[/v1/token][token] response. - -### POST /v1/token - -After receiving an authorization grant from the user, clients exercise that grant -at this endpoint to obtain tokens that can be used to access attached services -for a particular user. - -The following types of grant are possible: - -- `authorization_code`: a single-use code as produced by the [authorization][] endpoint, - obtained through a redirect-based authorization flow. -- `refresh_token`: a token previously obtained from this endpoint when using - `access_type=offline`. -- `fxa-credentials`: an FxA identity assertion, obtained by directly authenticating - the user's account. - -#### Request Parameters - -- `ppid_seed`: (optional) Seed used in `sub` claim generation of - JWT access tokens/ID tokens for clients with [Pseudonymous Pairwise - Identifiers (PPID)](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/oauth/pairwise-pseudonymous-identifiers.md) - enabled. Used to forcibly rotate the `sub` claim. Must be an integer in the range 0-1024. - Defaults to 0. -- `resource`: (optional) Indicates the target service or resource at which access is being - requested. Its value must be an absolute URI, and may include a query component but - must not include a fragment component. Added to the `aud` claim of JWT access tokens. -- `ttl`: (optional) Seconds that the access_token should be valid. - If unspecified this will default to the maximum value allowed by the - server, which is a configurable option but would typically be measured - in minutes or hours. -- `grant_type`: Either `authorization_code`, `refresh_token`, or `fxa-credentials`. - - If `authorization_code`: - - `client_id`: The id returned from client registration. - - `client_secret`: The secret returned from client registration. - Forbidden for public clients, required otherwise. - - `code`: A string that was received from the [authorization][] endpoint. - - `code_verifier`: The [PKCE](pkce.md) code verifier. - Required for public clients, forbidden otherwise. - - If `refresh_token`: - - `client_id`: The id returned from client registration. - - `client_secret`: The secret returned from client registration. - Forbidden for public (PKCE) clients, required otherwise. - - `refresh_token`: A string that received from the [token][] - endpoint specifically as a refresh token. - - `scope`: (optional) A subset of scopes provided to this - refresh_token originally, to receive an access_token with less - permissions. - - If `fxa-credentials`: - - `client_id`: The id returned from client registration. - - `assertion`: FxA identity assertion authenticating the user. - - `scope`: (optional) A string-separated list of scopes to be authorized. - - `access_type`: (optional) Determines whether to generate a `refresh_token` (if `offline`) - or not (if `online`). - -**Example:** - -```sh -curl -v \ --X POST \ --H "Content-Type: application/json" \ -"https://oauth.accounts.firefox.com/v1/token" \ --d '{ - "client_id": "5901bd09376fadaa", - "client_secret": "20c6882ef864d75ad1587c38f9d733c80751d2cbc8614e30202dc3d1d25301ff", - "ttl": 3600, - "grant_type": "authorization_code", - "code": "4ab433e31ef3a7cf7c20590f047987922b5c9ceb1faff56f0f8164df053dd94c" -}' -``` - -#### Response - -A valid request will return a JSON response with these properties: - -- `access_token`: A string that can be used for authorized requests to service providers. -- `scope`: A string of space-separated permissions that this token has. -- `expires_in`: **Seconds** until this access token will no longer be valid. -- `token_type`: A string representing the token type. Currently will always be "bearer". -- `auth_at`: An integer giving the time at which the user authenticated to the Firefox Accounts server when generating this token, as a UTC unix timestamp (i.e. **seconds since epoch**). -- `refresh_token`: (Optional) A refresh token to fetch a new access token when this one expires. Only present if: - - `grant_type=authorization_code` and the original authorization request included `access_type=offline`. - - `grant_type=fxa-credentials` and the request included `access_type=offline`. -- `id_token`: (Optional) If the authorization was requested with `openid` scope, then this property will contain the OpenID Connect ID Token. -- `keys_jwe`: (Optional) Returns the JWE bundle of key material for any scopes that have keys, if `grant_type=authorization_code`. - -**Example:** - -```json -{ - "access_token": "558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0", - "scope": "profile:email profile:avatar", - "token_type": "bearer", - "expires_in": 3600, - "refresh_token": "58d59cc97c3ca183b3a87a65eec6f93d5be051415b53afbf8491cc4c45dbb0c6", - "auth_at": 1422336613 -} -``` - -### POST /v1/destroy - -After a client is done using a token, the responsible thing to do is to -destroy the token afterwards. A client can use this route to do so. - -#### Request Parameters - -- `token|access_token|refresh_token|refresh_token_id`: The hex string access token. By default, `token` is assumed to be the access token. - -**Example:** - -```sh -curl -v \ --X POST \ --H "Content-Type: application/json" \ -"https://oauth.accounts.firefox.com/v1/destroy" \ --d '{ - "token": "558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" -}' -``` - -#### Response - -A valid request will return an empty response, with a 200 status code. - -### POST /v1/verify - -Attached services can post tokens to this endpoint to learn about which -user and scopes are permitted for the token. - -#### Request Parameters - -- `token`: A token string received from a client - -**Example:** - -```sh -curl -v \ --X POST \ --H "Content-Type: application/json" \ -"https://oauth.accounts.firefox.com/v1/verify" \ --d '{ - "token": "558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" -}' -``` - -#### Response - -A valid request will return JSON with these properties: - -- `user`: The uid of the respective user. -- `client_id`: The client_id of the respective client. -- `scope`: An array of scopes allowed for this token. -- `email`: **REMOVED** The email of the respective user. - -**Example:** - -```json -{ - "user": "5901bd09376fadaa076afacef5251b6a", - "client_id": "45defeda038a1c92", - "scope": ["profile:email", "profile:avatar"], - "email": "foo@example.com" -} -``` - -### GET /v1/jwks - -This endpoint returns the [JWKs](https://tools.ietf.org/html/rfc7517) -that are used for signing OpenID Connect id tokens. - -#### Request - -```sh -curl -v "https://oauth.accounts.firefox.com/v1/jwks" -``` - -#### Response - -A valid response will return JSON of the `keys`. - -**Example:** - -```json -{ - "keys": [ - "alg": "RS256", - "use": "sig", - "kty": "RSA", - "kid": "2015.12.02-1", - "n":"xaQHsKpu1KSK-YEMoLzZS7Xxciy3esGrhrrqW_JBrq3IRmeGLaqlE80zcpIVnStyp9tbet2niYTemt8ug591YWO5Y-S0EgQyFTxnGjzNOvAL6Cd2iGie9QeSehfFLNyRPdQiadYw07fw-h5gweMpVJs8nTgS-Bcorlw9JQM6Il1cUpbP0Lt-F_5qrzlaOiTEAAb4JGOusVh0n-MZfKt7w0mikauMH5KfhflwQDn4YTzRkWJzlldXr1Cs0ZkYzOwS4Hcoku7vd6lqCUO0GgZvkuvCFqdVKzpa4CGboNdfIjcGVF4f1CTQaQ0ao51cwLzq1pgi5aWYhVH7lJcm6O_BQw", - "e":"AQAC" - ] -} -``` - -### POST /v1/keydata - -This endpoint returns the required scoped key metadata. - -#### Request - -```sh -curl -X POST \ - https://oauth.accounts.firefox.com/v1/key-data \ - -H 'cache-control: no-cache' \ - -H 'content-type: application/json' \ - -d '{ - "client_id": "aaa6b9b3a65a1871", - "assertion": "eyJhbGciOiJSUzI1NiJ9.eyJwdWJsaWMta2V5Ijp7Imt0eSI6IlJTQSIsIm4iOiJvWmdsNkpwM0Iwcm5BVXppNThrdS1iT0RvR3ZuUGNnWU1UdXQ1WkpyQkJiazBCdWU4VUlRQ0dnYVdrYU5Xb29INkktMUZ6SXU0VFpZYnNqWGJ1c2JRRlQxOGREUkN6VVRubFlXdVZXUzhoSWhKc3lhZHJwSHJOVkI1VndmSlRKZVgwTjFpczBXcU1qdUdOc2VMLXluYnFjOVhueElncFJaai05QnZqY2ZKYXNOUTNZdHR3VHZVaFJOLVFGNWgxQkY1MnA2QmdOTVBvWmQ5MC1EU0xydlpseXp6MEh0Q2tFZnNsc013czVkR0ExTlZ1dEwtcGVDeU50VTFzOEtFaDlzcGxXeF9lQlFybTlYQU1kYXp5ZWR6VUpJU1UyMjZmQzhEUHh5c0ZreXpCbjlDQnFDQUpTNjQzTGFydUVDaS1rMGhKOWFmM2JXTmJnWmpSNVJ2NXF4THciLCJlIjoiQVFBQiJ9LCJwcmluY2lwYWwiOnsiZW1haWwiOiIwNjIxMzM0YzIwNjRjNmYzNmJlOGFkOWE0N2M1NTliY2FwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9LCJpYXQiOjE1MDY5Njk2OTU0MzksImV4cCI6MTUwNjk2OTY5NjQzOSwiZnhhLXZlcmlmaWVkRW1haWwiOiIzMjM2NzJiZUBtb3ppbGxhLmNvbSIsImlzcyI6ImFwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9.hFZd5zFheXOFrXKkJvw6Vpv2l7ctlxuBTvuh5f_jLPAjZoJ9ri-vaJjL_WYBFUvS2xHzfx3-ldxLddyTKwCDAJeB_NkOFL_WJSrMet9C7_Z1hH9HmydeXIT82xJmhrwzW-WOO4ibQvRbocEFiNujynKsg1gS8v0iiYjIX-0cXCrlkxkbVx_8EXJFKDDOGzK9v7Zq6D7gkhP-CHEaNYaTHMn65tLQtBS6snGdaXlxoGHMWmDL6STbnJzWa7sa4QwHf-AgT1rUkQQAUHNa_XLZ0FEzqiCPctMadlihiUZL2V6vxIDBS4mHUF4qj0FvIMJflivDnJVkRNijDuP-h-Lh_A~eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJvYXV0aC5meGEiLCJleHAiOjE1MDY5Njk2OTY0MzksImlzcyI6ImFwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9.M5xyk3RffucgaavjbUm7Eqnt47hzeGbGa2VR3jnVEIlRHfz5S25Qf3ngejwee7XECvIywbaKWeijXFOwS-EkB-7qP1gl4oNJjPmbnCk7S1lgckLWvdMIU-HLGKjrN6Mw76__LzvAbsusSeGmsvTCIVuOJ49Xs3tC1fLyB_re0QNpCcS6AUnJ1KOxIMEM3Om7ysNO5F_AqcD3PwlEti5lbwSk8iP5TWL12C2Nkb_6Hxze_mA1NZNAHOips9bF2J7oy1hqGoMYj1XYZrsyjpPWEuZQATAPlKSjbh1hq-UtDeT7DlwEmIbIUd3JA8qh1MkHKGgavd4fIMap0IPmr9rs4A", - "scope": "https://identity.mozilla.com/apps/sample-scope-can-scope-key" -}' -``` - -#### Response - -A valid response will return JSON the scoped key information for every scope that has scoped keys: - -**Example:** - -```json -{ - "https://identity.mozilla.com/apps/sample-scope-can-scope-key": { - "identifier": "https://identity.mozilla.com/apps/sample-scope-can-scope-key", - "keyRotationSecret": "0000000000000000000000000000000000000000000000000000000000000000", - "keyRotationTimestamp": 1506970363512 - } -} -``` - -### GET /v1/authorized-clients - -This endpoint returns a list of all OAuth client instances connected to the user's account, -including the the scopes granted to each client instance -and the time at which it was last active, if available. -It must be authenticated with an identity assertion for the user's account. - -#### Request Parameters - -- `assertion`: A FxA assertion for the signed-in user. - -**Example:** - -```sh -curl -X POST \ - https://oauth.accounts.firefox.com/v1/authorized-clients \ - -H 'cache-control: no-cache' \ - -H 'content-type: application/json' \ - -d '{ - "assertion": "eyJhbGciOiJSUzI1NiJ9.eyJwdWJsaWMta2V5Ijp7Imt0eSI6IlJTQSIsIm4iOiJvWmdsNkpwM0Iwcm5BVXppNThrdS1iT0RvR3ZuUGNnWU1UdXQ1WkpyQkJiazBCdWU4VUlRQ0dnYVdrYU5Xb29INkktMUZ6SXU0VFpZYnNqWGJ1c2JRRlQxOGREUkN6VVRubFlXdVZXUzhoSWhKc3lhZHJwSHJOVkI1VndmSlRKZVgwTjFpczBXcU1qdUdOc2VMLXluYnFjOVhueElncFJaai05QnZqY2ZKYXNOUTNZdHR3VHZVaFJOLVFGNWgxQkY1MnA2QmdOTVBvWmQ5MC1EU0xydlpseXp6MEh0Q2tFZnNsc013czVkR0ExTlZ1dEwtcGVDeU50VTFzOEtFaDlzcGxXeF9lQlFybTlYQU1kYXp5ZWR6VUpJU1UyMjZmQzhEUHh5c0ZreXpCbjlDQnFDQUpTNjQzTGFydUVDaS1rMGhKOWFmM2JXTmJnWmpSNVJ2NXF4THciLCJlIjoiQVFBQiJ9LCJwcmluY2lwYWwiOnsiZW1haWwiOiIwNjIxMzM0YzIwNjRjNmYzNmJlOGFkOWE0N2M1NTliY2FwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9LCJpYXQiOjE1MDY5Njk2OTU0MzksImV4cCI6MTUwNjk2OTY5NjQzOSwiZnhhLXZlcmlmaWVkRW1haWwiOiIzMjM2NzJiZUBtb3ppbGxhLmNvbSIsImlzcyI6ImFwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9.hFZd5zFheXOFrXKkJvw6Vpv2l7ctlxuBTvuh5f_jLPAjZoJ9ri-vaJjL_WYBFUvS2xHzfx3-ldxLddyTKwCDAJeB_NkOFL_WJSrMet9C7_Z1hH9HmydeXIT82xJmhrwzW-WOO4ibQvRbocEFiNujynKsg1gS8v0iiYjIX-0cXCrlkxkbVx_8EXJFKDDOGzK9v7Zq6D7gkhP-CHEaNYaTHMn65tLQtBS6snGdaXlxoGHMWmDL6STbnJzWa7sa4QwHf-AgT1rUkQQAUHNa_XLZ0FEzqiCPctMadlihiUZL2V6vxIDBS4mHUF4qj0FvIMJflivDnJVkRNijDuP-h-Lh_A~eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJvYXV0aC5meGEiLCJleHAiOjE1MDY5Njk2OTY0MzksImlzcyI6ImFwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9.M5xyk3RffucgaavjbUm7Eqnt47hzeGbGa2VR3jnVEIlRHfz5S25Qf3ngejwee7XECvIywbaKWeijXFOwS-EkB-7qP1gl4oNJjPmbnCk7S1lgckLWvdMIU-HLGKjrN6Mw76__LzvAbsusSeGmsvTCIVuOJ49Xs3tC1fLyB_re0QNpCcS6AUnJ1KOxIMEM3Om7ysNO5F_AqcD3PwlEti5lbwSk8iP5TWL12C2Nkb_6Hxze_mA1NZNAHOips9bF2J7oy1hqGoMYj1XYZrsyjpPWEuZQATAPlKSjbh1hq-UtDeT7DlwEmIbIUd3JA8qh1MkHKGgavd4fIMap0IPmr9rs4A", -}' -``` - -#### Response - -A valid 200 response will be a JSON array -where each item has the following properties: - -- `client_id`: The hex id of the client. -- `refresh_token_id`: (optional) The ID of the refresh token held the client instance -- `client_name`: The string name of the client. -- `created_time`: Integer time of token creation. -- `last_access_time`: Integer last-access time for the token. -- `scope`: Sorted list of all scopes granted to the client instance. - -For clients that use refresh tokens, each refresh token is taken to represent -a separate instance of that client and is returned as a separate entry in the list, -with the `refresh_token_id` field distinguishing each. - -For clients that only use access tokens, all active access tokens are combined -into a single entry in the list, and the `refresh_token_id` field will not be present. - -**Example:** - -```json -[ - { - "client_id": "5901bd09376fadaa", - "refresh_token_id": "6e8c38f6a9c27dc0e4df698dc3e3e8b101ad6d79e87842b1ca96ad9b3cd8ed28", - "name": "Example Sync Client", - "created_time": 1528334748000, - "last_access_time": 1528334748000, - "scope": ["profile", "https://identity.mozilla.com/apps/oldsync"] - }, - { - "client_id": "5901bd09376fadaa", - "refresh_token_id": "eb5e17f246a6b0937356412118ea12b67a638232d6b376e2511cf38a0c4eecf9", - "name": "Example Sync Client", - "created_time": 1528334748000, - "last_access_time": 1528334834000, - "scope": ["profile", "https://identity.mozilla.com/apps/oldsync"] - }, - { - "client_id": "23d10a14f474ca41", - "name": "Example Website", - "created_time": 1328334748000, - "last_access_time": 1476677854037, - "scope": ["profile:email", "profile:uid"] - } -] -``` - -### POST /v1/authorized-clients/destroy - -This endpoint revokes tokens granted to a given client. -It must be authenticated with an identity assertion for the user's account. - -#### Request Parameters - -- `client_id`: The `client_id` of the client whose tokens should be deleted. -- `refresh_token_id`: (Optional) The specific `refresh_token_id` to be destroyed. -- `assertion`: A FxA assertion for the signed-in user. - -**Example:** - -```sh -curl -X POST \ - https://oauth.accounts.firefox.com/v1/authorized-clients/destroy \ - -H 'cache-control: no-cache' \ - -H 'content-type: application/json' \ - -d '{ - "client_id": "5901bd09376fadaa", - "refresh_token_id": "6e8c38f6a9c27dc0e4df698dc3e3e8b101ad6d79e87842b1ca96ad9b3cd8ed28", - "assertion": "eyJhbGciOiJSUzI1NiJ9.eyJwdWJsaWMta2V5Ijp7Imt0eSI6IlJTQSIsIm4iOiJvWmdsNkpwM0Iwcm5BVXppNThrdS1iT0RvR3ZuUGNnWU1UdXQ1WkpyQkJiazBCdWU4VUlRQ0dnYVdrYU5Xb29INkktMUZ6SXU0VFpZYnNqWGJ1c2JRRlQxOGREUkN6VVRubFlXdVZXUzhoSWhKc3lhZHJwSHJOVkI1VndmSlRKZVgwTjFpczBXcU1qdUdOc2VMLXluYnFjOVhueElncFJaai05QnZqY2ZKYXNOUTNZdHR3VHZVaFJOLVFGNWgxQkY1MnA2QmdOTVBvWmQ5MC1EU0xydlpseXp6MEh0Q2tFZnNsc013czVkR0ExTlZ1dEwtcGVDeU50VTFzOEtFaDlzcGxXeF9lQlFybTlYQU1kYXp5ZWR6VUpJU1UyMjZmQzhEUHh5c0ZreXpCbjlDQnFDQUpTNjQzTGFydUVDaS1rMGhKOWFmM2JXTmJnWmpSNVJ2NXF4THciLCJlIjoiQVFBQiJ9LCJwcmluY2lwYWwiOnsiZW1haWwiOiIwNjIxMzM0YzIwNjRjNmYzNmJlOGFkOWE0N2M1NTliY2FwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9LCJpYXQiOjE1MDY5Njk2OTU0MzksImV4cCI6MTUwNjk2OTY5NjQzOSwiZnhhLXZlcmlmaWVkRW1haWwiOiIzMjM2NzJiZUBtb3ppbGxhLmNvbSIsImlzcyI6ImFwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9.hFZd5zFheXOFrXKkJvw6Vpv2l7ctlxuBTvuh5f_jLPAjZoJ9ri-vaJjL_WYBFUvS2xHzfx3-ldxLddyTKwCDAJeB_NkOFL_WJSrMet9C7_Z1hH9HmydeXIT82xJmhrwzW-WOO4ibQvRbocEFiNujynKsg1gS8v0iiYjIX-0cXCrlkxkbVx_8EXJFKDDOGzK9v7Zq6D7gkhP-CHEaNYaTHMn65tLQtBS6snGdaXlxoGHMWmDL6STbnJzWa7sa4QwHf-AgT1rUkQQAUHNa_XLZ0FEzqiCPctMadlihiUZL2V6vxIDBS4mHUF4qj0FvIMJflivDnJVkRNijDuP-h-Lh_A~eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJvYXV0aC5meGEiLCJleHAiOjE1MDY5Njk2OTY0MzksImlzcyI6ImFwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9.M5xyk3RffucgaavjbUm7Eqnt47hzeGbGa2VR3jnVEIlRHfz5S25Qf3ngejwee7XECvIywbaKWeijXFOwS-EkB-7qP1gl4oNJjPmbnCk7S1lgckLWvdMIU-HLGKjrN6Mw76__LzvAbsusSeGmsvTCIVuOJ49Xs3tC1fLyB_re0QNpCcS6AUnJ1KOxIMEM3Om7ysNO5F_AqcD3PwlEti5lbwSk8iP5TWL12C2Nkb_6Hxze_mA1NZNAHOips9bF2J7oy1hqGoMYj1XYZrsyjpPWEuZQATAPlKSjbh1hq-UtDeT7DlwEmIbIUd3JA8qh1MkHKGgavd4fIMap0IPmr9rs4A", -}' -``` - -#### Response - -A valid 200 response will return an empty JSON object. - -### POST /v1/introspect - -This endpoint returns the status of the token and meta-information about this token. - -#### Request Parameters - -- `token`: An OAuth token for the user. -- `token_type_hint`: A literal string `"access_token"` or `"refresh_token"` - -**Example:** - -```sh -curl -X POST \ - -H "Content-Type: application/json" \ - "https://oauth.accounts.firefox.com/v1/introspect" \ - -d '{"token":"5e00491407a01507bdc4002fd7b675fb4e7d039045a7e6755e4aed0d3e287c69"}' -``` - -#### Response - -A valid request will return a JSON response with these properties: - -- `active`: Boolean indicator of weather the presented token is active. -- `scope`: Optional. A space-seperated list of scopes associated with this token. -- `client_id`: Optional. The hex id of the client whose token was passed. -- `token_type`: A string representing the token type. It will be `"access_token"` or `"refresh_token"`. -- `iat`: Optional. Integer time of token creation. -- `sub`: Optional. The hex id of the user. -- `jti`: Optional. The hex id of the token -- `exp`: Optional. Integer time of token expiration. -- `fxa-lastUsedAt`: Optional. Integer time when this token is last used. - -**Example:** - -```json -{ - "active": true, - "scope": "profile https://identity.mozilla.com/account/subscriptions", - "client_id": "59cceb6f8c32317c", - "token_type": "access_token", - "iat": 1566535888243, - "sub": "913fe9395bb946b48c1521d7beb2cb24", - "jti": "5ae05d8fe413a749e0f4eb3c495a1c526fb52c85ca5fde516df5dd77d41f7b5b", - "exp": 1566537688243 -} -``` +# [Firefox Accounts OAuth Server API](https://mozilla.github.io/ecosystem-platform/api#tag/OAuth-Server-API-Overview) diff --git a/packages/fxa-auth-server/docs/swagger/auth-server-api.ts b/packages/fxa-auth-server/docs/swagger/auth-server-api.ts new file mode 100644 index 00000000000..d69661ebeb4 --- /dev/null +++ b/packages/fxa-auth-server/docs/swagger/auth-server-api.ts @@ -0,0 +1,298 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import dedent from 'dedent'; + +export const AUTH_SERVER_API_DESCRIPTION = { + description: dedent` + This document provides protocol-level details of the Firefox Accounts auth server API. For a prose description of the client/server protocol and details on how each parameter is derived, see the [API design document](https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol). For a reference client implementation, see [fxa-auth-client](https://github.com/mozilla/fxa/tree/main/packages/fxa-auth-client). + + ## URL Structure for Auth Server + All requests use URLs of the form: + + > \`https:///v1/\` + + Note that: + + - All API access must be over a properly-validated HTTPS connection. + - The URL embeds a version identifier \`v1\`. + Future revisions of this API may introduce new version numbers. + - The base URI of the server may be configured on a per-client basis: + - For a list of development servers + see [Firefox Accounts deployments on MDN](https://developer.mozilla.org/en-US/Firefox_Accounts#Firefox_Accounts_deployments). + - The canonical URL for Mozilla's hosted Firefox Accounts server + is \`https://api.accounts.firefox.com/v1\`. + + ## Request Format + All POST requests must have a content-type of \`application/json\` with a UTF8-encoded JSON body and must specify the content-length header. Keys and other binary data are included in the JSON as hexadecimal strings. + + The following request headers may be specified to influence the behavior of the server: + + - \`Accept-Language\` may be used to localize emails and SMS messages. + + ## Response format + All requests receive a JSON response body with a \`Content-Type: application/json\` header and appropriate \`Content-Length\` set. The body structure depends on the endpoint returning it. + + Successful responses will have an HTTP status code of 200 and a \`Timestamp\` header that contains the current server time in seconds since the epoch. + + Error responses caused by invalid client behavior will have an HTTP status code in the 4xx range. Error responses caused by server-side problems will have an HTTP status code in the 5xx range. Failures due to invalid behavior from the client. + + To simplify error handling for the client, the type of error is indicated by both + a defined HTTP status code and an application-specific \`errno\` in the body. + + For example: + + > \`{\`
+ > \`"code": 400,\` // Matches the HTTP status code
+ > \`"errno": 107,\` // Stable application-level error number
+ > \`"error": "Bad Request",\` // String description of the error type
+ > \`"message": "Invalid parameter in request body",\` // Specific error message
+ > \`"info": "https://docs.dev.lcip.og/errors/1234"\` // Link to more information
+ > \`}\` + + Responses for some errors may include additional parameters. + + + ### Defined errors + + The currently-defined values for \`code\` and \`errno\` are: + + | status code | errno | description | + |-------------|-------|-------------------------------------------------------------------------------| + | 400 | 100 | Incorrect Database Patch Level | + | 400 | 101 | Account already exists | + | 400 | 102 | Unknown account | + | 400 | 103 | Incorrect password | + | 400 | 104 | Unverified account | + | 400 | 105 | Invalid verification code | + | 400 | 106 | Invalid JSON in request body | + | 400 | 107 | Invalid parameter in request body | + | 400 | 108 | Missing parameter in request body | + | 401 | 109 | Invalid request signature | + | 401 | 110 | Invalid authentication token in request signature | + | 401 | 111 | Invalid timestamp in request signature | + | 411 | 112 | Missing content-length header | + | 413 | 113 | Request body too large | + | 429 | 114 | Client has sent too many requests | + | 401 | 115 | Invalid nonce in request signature | + | 410 | 116 | This endpoint is no longer supported | + | 400 | 120 | Incorrect email case | + | 400 | 123 | Unknown device | + | 400 | 124 | Session already registered by another device | + | 400 | 125 | The request was blocked for security reasons | + | 400 | 126 | Account must be reset | + | 400 | 127 | Invalid unblock code | + | 400 | 129 | Invalid phone number | + | 400 | 130 | Invalid region | + | 400 | 131 | Invalid message id | + | 500 | 132 | Message rejected | + | 400 | 133 | Email account sent complaint | + | 400 | 134 | Email account hard bounced | + | 400 | 135 | Email account soft bounced | + | 400 | 136 | Email already exists | + | 400 | 137 | Can not delete primary email | + | 400 | 138 | Unverified session | + | 400 | 139 | Can not add secondary email that is same as your primary | + | 400 | 140 | Email already exists | + | 400 | 141 | Email already exists | + | 400 | 142 | Sign in with this email type is not currently supported | + | 400 | 143 | Unknown email | + | 400 | 144 | Email already exists | + | 400 | 145 | Reset password with this email type is not currently supported | + | 400 | 146 | Invalid signin code | + | 400 | 147 | Can not change primary email to an unverified email | + | 400 | 148 | Can not change primary email to an email that does not belong to this account | + | 400 | 149 | This email can not currently be used to login | + | 400 | 150 | Can not resend email code to an email that does not belong to this account | + | 500 | 151 | Failed to send email | + | 422 | 151 | Failed to send email | + | 400 | 152 | Invalid token verification code | + | 400 | 153 | Expired token verification code | + | 400 | 154 | TOTP token already exists for this account. | + | 400 | 155 | TOTP token not found. | + | 400 | 156 | Recovery code not found. | + | 400 | 157 | Unavailable device command. | + | 400 | 158 | Recovery key not found. | + | 400 | 159 | Recovery key is not valid. | + | 400 | 160 | This request requires two step authentication enabled on your account. | + | 400 | 161 | Recovery key already exists. | + | 400 | 162 | Unknown client_id | + | 400 | 164 | Stale auth timestamp | + | 409 | 165 | Redis WATCH detected a conflicting update | + | 400 | 166 | Not a public client | + | 400 | 167 | Incorrect redirect URI | + | 400 | 168 | Invalid response_type | + | 400 | 169 | Requested scopes are not allowed | + | 400 | 170 | Public clients require PKCE OAuth parameters | + | 400 | 171 | Required Authentication Context Reference values could not be satisfied | + | 404 | 176 | Unknown subscription | + | 400 | 177 | Unknown subscription plan | + | 400 | 178 | Subscription payment token rejected | + | 503 | 201 | Service unavailable | + | 503 | 202 | Feature not enabled | + | 500 | 203 | A backend service request failed. | + | 500 | 998 | An internal validation check failed. | + + The following errors include additional response properties: + + | errno | description | + |-------|-------------------------------------------------------------------------| + | 100 | level, levelRequired | + | 101 | email | + | 102 | email | + | 103 | email | + | 105 | | + | 107 | validation | + | 108 | param | + | 111 | serverTime | + | 114 | retryAfter, retryAfterLocalized, verificationMethod, verificationReason | + | 120 | email | + | 124 | deviceId | + | 125 | verificationMethod, verificationReason | + | 126 | email | + | 130 | region | + | 132 | reason, reasonCode | + | 133 | bouncedAt | + | 134 | bouncedAt | + | 135 | bouncedAt | + | 152 | | + | 153 | | + | 162 | clientId | + | 164 | authAt | + | 167 | redirectUri | + | 169 | invalidScopes | + | 171 | foundValue | + | 201 | retryAfter | + | 202 | retryAfter | + | 203 | service, operation | + | 998 | op, data | + + + ### Responses from intermediary servers + + As with any HTTP-based API, clients must handle standard errors that may be returned by proxies, load-balancers or other intermediary servers. These non-application responses can be identified by the absence of a correctly-formatted JSON response body. + + Common examples include: + + - \`413 Request Entity Too Large\`: may be returned by an upstream proxy server. + - \`502 Gateway Timeout\`: may be returned if a load-balancer can't connect to application servers. + + ## Validation + In the documentation that follows, some properties of requests and responses are validated by common code that has been refactored and extracted. For reference, those common validations are defined here. + + + ### lib/routes/validators + + - \`HEX_STRING\`: \`/^(?:[a-fA-F0-9]{2})+$/\` + - \`BASE_36\`: \`/^[a-zA-Z0-9]*$/\` + - \`URL_SAFE_BASE_64\`: \`/^[A-Za-z0-9_-]+$/\` + - \`PKCE_CODE_VERIFIER\`: \`/^[A-Za-z0-9-\._~]{43,128}$/\` + - \`DISPLAY_SAFE_UNICODE\`: \`/^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uD800-\uDFFF\uE000-\uF8FF\uFFF9-\uFFFF])*$/\` + - \`DISPLAY_SAFE_UNICODE_WITH_NON_BMP\`: \`/^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uE000-\uF8FF\uFFF9-\uFFFF])*$/\` + - \`BEARER_AUTH_REGEX\`: \`/^Bearer\s+([a-z0-9+\/]+)$/i\` + - \`service\`: \`string, max(16), regex(/^[a-zA-Z0-9\-]*$/)\` + - \`hexString\`: \`string, regex(/^(?:[a-fA-F0-9]{2})+$/)\` + - \`clientId\`: \`module.exports.hexString.length(16)\` + - \`clientSecret\`: \`module.exports.hexString\` + - \`accessToken\`: \`module.exports.hexString.length(64)\` + - \`refreshToken\`: \`module.exports.hexString.length(64)\` + - \`authorizationCode\`: \`module.exports.hexString.length(64)\` + - \`scope\`: \`string, max(256), regex(/^[a-zA-Z0-9 _\/.:-]*$/), allow('')\` + - \`assertion\`: \`string, min(50), max(10240), regex(/^[a-zA-Z0-9_\-\.~=]+$/)\` + - \`pkceCodeChallengeMethod\`: \`string, valid('S256')\` + - \`pkceCodeChallenge\`: \`string, length(43), regex(module, exports.URL_SAFE_BASE_64)\` + - \`pkceCodeVerifier\`: \`string, length(43), regex(module, exports.PKCE_CODE_VERIFIER)\` + - \`jwe\`: \`string, max(1024), regex(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/)\` + - \`verificationMethod\`: \`string, valid()\` + - \`authPW\`: \`string, length(64), regex(HEX_STRING), required\` + - \`wrapKb\`: \`string, length(64), regex(/^(?:[a-fA-F0-9]{2})+$/)\` + - \`recoveryKeyId\`: \`string, regex(HEX_STRING), max(32)\` + - \`recoveryData\`: \`string, regex(/[a-zA-Z0-9.]/), max(1024), required\` + - \`E164_NUMBER\`: \`/^\+[1-9]\d{1,14}$/\` + - \`DIGITS\`: \`/^[0-9]+$/\` + - \`DEVICE_COMMAND_NAME\`: \`/^[a-zA-Z0-9._\/\-:]{1,100}$/\` + - \`IP_ADDRESS\`: \`string, ip\` + + + ### lib/metrics/context + + - \`SCHEMA\`: object({ + - \`deviceId\`: string, length(32), regex(HEX_STRING), optional + - \`entrypoint\`: ENTRYPOINT_SCHEMA.optional + - \`entrypointExperiment\`: ENTRYPOINT_SCHEMA.optional + - \`entrypointVariation\`: ENTRYPOINT_SCHEMA.optional + - \`flowId\`: string, length(64), regex(HEX_STRING), optional + - \`flowBeginTime\`: number, integer, positive, optional + - \`utmCampaign\`: UTM_CAMPAIGN_SCHEMA.optional + - \`utmContent\`: UTM_SCHEMA.optional + - \`utmMedium\`: UTM_SCHEMA.optional + - \`utmSource\`: UTM_SCHEMA.optional + - \`utmTerm\`: UTM_SCHEMA.optional + }), unknown(false), and('flowId', 'flowBeginTime') + - \`schema\`: SCHEMA.optional + - \`requiredSchema\`: SCHEMA.required + + + ### lib/features + + - \`schema\`: array, items(string), optional + + + ### lib/devices + + - \`schema\`: { + + - \`id\`: isA.string.length(32).regex(HEX_STRING) + - \`location\`: isA.object({ + - \`city\`: isA.string.optional.allow(null) + - \`country\`: isA.string.optional.allow(null) + - \`state\`: isA.string.optional.allow(null) + - \`stateCode\`: isA.string.optional.allow(null) + - }) + - \`name\`: isA.string.max(255).regex(DISPLAY_SAFE_UNICODE_WITH_NON_BMP) + - \`nameResponse\`: isA.string.max(255).allow('') + - \`type\`: isA.string.max(16) + - \`pushCallback\`: validators.pushCallbackUrl({ scheme: 'https' }).regex(PUSH_SERVER_REGEX).max(255).allow('') + - \`pushPublicKey\`: isA.string.max(88).regex(URL_SAFE_BASE_64).allow('') + - \`pushAuthKey\`: isA.string.max(24).regex(URL_SAFE_BASE_64).allow('') + - \`pushEndpointExpired\`: isA.boolean.strict + - \`availableCommands\`: isA.object.pattern(validators.DEVICE_COMMAND_NAME + - \`isA.string.max(2048)) + + } + + ## Back-off protocol + + During periods of heavy load, the server may request that clients enter a "back-off" state, + in which they avoid making further requests. + + At such times, + it will return a \`503 Service Unavailable\` response + with a \`Retry-After\` header denoting the number of seconds to wait + before issuing any further requests. + It will also include \`errno: 201\` + and a \`retryAfter\` field + matching the value of the \`Retry-After\` header + in the body. + + For example, + the following response indicates that the client + should suspend making further requests + for 30 seconds: + + > \`HTTP/1.1 503 Service Unavailable\`
+ > \`Retry-After: 30\`
+ > \`Content-Type: application/json\` + > + > \`{\`
+ > \`"code": 503,\`
+ > \`"errno": 201,\`
+ > \`"error": "Service Unavailable",\`
+ > \`"message": "Service unavailable",\`
+ > \`"info": "https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/api.md#response-format",\`
+ > \`"retryAfter": 30,\`
+ > \`"retryAfterLocalized": "in a few seconds"\`
+ > \`}\` + `, +}; diff --git a/packages/fxa-auth-server/docs/swagger/misc-api.ts b/packages/fxa-auth-server/docs/swagger/misc-api.ts index 5b300c1620e..48845f2ac4c 100644 --- a/packages/fxa-auth-server/docs/swagger/misc-api.ts +++ b/packages/fxa-auth-server/docs/swagger/misc-api.ts @@ -61,86 +61,20 @@ const WELLKNOWN_PUBLIC_KEYS = { description: '/.well-known/public-keys', }; -const AUTHORIZATION_GET = { - ...TAGS_MISC, - description: '/authorization', -}; - -const AUTHORIZATION_POST = { - ...TAGS_MISC, - description: '/authorization', -}; - -const DESTROY_POST = { - ...TAGS_MISC, - description: '/destroy', -}; - -const AUTHORIZED_CLIENTS_DESTROY_POST = { - ...TAGS_MISC, - description: '/authorized-clients/destroy', -}; - -const AUTHORIZED_CLIENTS_POST = { - ...TAGS_MISC, - description: '/authorized_clients', -}; - -const CLIENT_CLIENTID_GET = { - ...TAGS_MISC, - description: '/oauth/client/{client_id}', -}; - const OAUTH_ID_TOKEN_VERIFY_POST = { ...TAGS_MISC, description: '/oauth/id-token-verify', }; -const INTROSPECT_POST = { - ...TAGS_MISC, - description: '/introspect', -}; - -const JWKS_GET = { - ...TAGS_MISC, - description: '/jwks', -}; - -const KEY_DATA_POST = { - ...TAGS_MISC, - description: '/key-data', -}; - -const TOKEN_POST = { - ...TAGS_MISC, - description: '/token', -}; - -const VERIFY_POST = { - ...TAGS_MISC, - description: '/verify', -}; - const API_DOCS = { ACCOUNT_GET, ACCOUNT_FINISH_SETUP_POST, ACCOUNT_LOCK_POST, ACCOUNT_SESSIONS_LOCATIONS_GET, ACCOUNT_STUB_POST, - AUTHORIZATION_GET, - AUTHORIZATION_POST, - AUTHORIZED_CLIENTS_DESTROY_POST, - AUTHORIZED_CLIENTS_POST, - CLIENT_CLIENTID_GET, - DESTROY_POST, - INTROSPECT_POST, - JWKS_GET, - KEY_DATA_POST, NEWSLETTERS_POST, OAUTH_ID_TOKEN_VERIFY_POST, SUPPORT_TICKET_POST, - TOKEN_POST, - VERIFY_POST, WELLKNOWN_BROWSERID_GET, WELLKNOWN_PUBLIC_KEYS, }; diff --git a/packages/fxa-auth-server/docs/swagger/oauth-server-api.ts b/packages/fxa-auth-server/docs/swagger/oauth-server-api.ts new file mode 100644 index 00000000000..bc3be0e2244 --- /dev/null +++ b/packages/fxa-auth-server/docs/swagger/oauth-server-api.ts @@ -0,0 +1,209 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import dedent from 'dedent'; +import TAGS from './swagger-tags'; + +const OAUTH_SERVER_API_DESCRIPTION = { + description: dedent` + ## URL Structure for OAuth Server + > \`https:///v1/\` + + Note that: + - All API access must be over HTTPS + - The URL embeds a version identifier "v1"; future versions of this API may introduce new version numbers. + - The base URL of the server may be configured on a per-client basis. + + ## Errors + Invalid requests will return 4XX responses. Internal failures will return 5XX. Both will include JSON responses describing the error. + + **Example error:** + > {
+ > \`"code": 400,\` // matches the HTTP status code
+ > \`"errno": 101,\` // stable application-level error number
+ > \`"error": "Bad Request",\` // string description of error type
+ > \`"message": "Unknown client"\`
+ > } + + The currently-defined error responses are: + + | status code | errno | description | + |-------------|-------|-------------------------------------------------| + | 400 | 101 | unknown client id | + | 400 | 102 | incorrect client secret | + | 400 | 103 | \`redirect_uri\` doesn't match registered value | + | 401 | 104 | invalid fxa assertion | + | 400 | 105 | unknown code | + | 400 | 106 | incorrect code | + | 400 | 107 | expired code | + | 400 | 108 | invalid token | + | 400 | 109 | invalid request parameter | + | 400 | 110 | invalid response_type | + | 401 | 111 | unauthorized | + | 403 | 112 | forbidden | + | 415 | 113 | invalid content type | + | 400 | 114 | invalid scopes | + | 400 | 115 | expired token | + | 400 | 116 | not a public client | + | 400 | 117 | incorrect code_challenge | + | 400 | 118 | pkce parameters missing | + | 400 | 119 | stale authentication timestamp | + | 400 | 120 | mismatch acr value | + | 400 | 121 | invalid grant_type | + | 500 | 999 | internal server error | + + + ## API Endpoints + - [GET /v1/authorization](#tag/OAuth-Server-API-Overview/operation/getAuthorization) + - [POST /v1/authorization](#tag/OAuth-Server-API-Overview/operation/postAuthorization) + - [POST /v1/authorized-clients](#tag/OAuth-Server-API-Overview/operation/postAuthorizedclients) + - [POST /v1/authorized-clients/destroy](#tag/OAuth-Server-API-Overview/operation/postAuthorizedclientsDestroy) + - [GET /v1/client/:id](#tag/OAuth-Server-API-Overview/operation/getClientClient_id) + - [POST /v1/destroy](#tag/OAuth-Server-API-Overview/operation/postDestroy) + - [POST /v1/introspect](#tag/OAuth-Server-API-Overview/operation/postIntrospect) + - [GET /v1/jwks](#tag/OAuth-Server-API-Overview/operation/getJwks) + - [POST /v1/key-data](#tag/OAuth-Server-API-Overview/operation/postKeydata) + - [POST /v1/token](#tag/OAuth-Server-API-Overview/operation/postToken) + - [POST /v1/verify](#tag/OAuth-Server-API-Overview/operation/postVerify) + `, +}; + +const TAGS_OAUTH_SERVER = { + tags: TAGS.OAUTH_SERVER, +}; + +const AUTHORIZATION_GET = { + ...TAGS_OAUTH_SERVER, + description: '/v1/authorization', + notes: [ + 'This endpoint starts the OAuth flow. A client redirects the user agent to this url. This endpoint will then redirect to the appropriate content-server page.', + ], +}; + +const AUTHORIZATION_POST = { + ...TAGS_OAUTH_SERVER, + description: '/v1/authorization', + notes: [ + dedent` + This endpoint should be used by the fxa-content-server, requesting that we supply a short-lived code (currently 15 minutes) that will be sent back to the client. This code will be traded for a token at the [token][] endpoint. + + Note: + + Responses + + Implicit Grant - If requesting an implicit grant (token), the response will match the [/v1/token][token] response. + `, + ], +}; + +const DESTROY_POST = { + ...TAGS_OAUTH_SERVER, + description: '/v1/destroy', + notes: [ + 'After a client is done using a token, the responsible thing to do is to destroy the token afterwards. A client can use this route to do so.', + ], +}; + +const AUTHORIZED_CLIENTS_DESTROY_POST = { + ...TAGS_OAUTH_SERVER, + description: '/v1/authorized-clients/destroy', + notes: [ + `This endpoint revokes tokens granted to a given client. It must be authenticated with an identity assertion for the user's account.`, + ], +}; + +const AUTHORIZED_CLIENTS_POST = { + ...TAGS_OAUTH_SERVER, + description: '/v1/authorized_clients', + notes: [ + dedent` + This endpoint returns a list of all OAuth client instances connected to the user's account, including the the scopes granted to each client instance and the time at which it was last active, if available. It must be authenticated with an identity assertion for the user's account + + Note: + + Responses + + For clients that use refresh tokens, each refresh token is taken to represent a separate instance of that client and is returned as a separate entry in the list, with the \`refresh_token_id\` field distinguishing each. + + For clients that only use access tokens, all active access tokens are combined into a single entry in the list, and the \`refresh_token_id\` field will not be present. + `, + ], +}; + +const CLIENT_CLIENTID_GET = { + ...TAGS_OAUTH_SERVER, + description: '/v1/client/{client_id}', + notes: [ + 'This endpoint is for the fxa-content-server to retrieve information about a client to show in its user interface.', + ], +}; + +const INTROSPECT_POST = { + ...TAGS_OAUTH_SERVER, + description: '/v1/introspect', + notes: [ + dedent` + This endpoint returns the status of the token and meta-information about this token. + + If the token has attribute \`active: false\`, none of the other attributes in the response will have content + `, + ], +}; + +const JWKS_GET = { + ...TAGS_OAUTH_SERVER, + description: '/v1/jwks', + notes: [ + 'This endpoint returns the [JWKs](https://datatracker.ietf.org/doc/html/rfc7517) that are used for signing OpenID Connect id tokens.', + ], +}; + +const KEY_DATA_POST = { + ...TAGS_OAUTH_SERVER, + description: '/v1/key-data', + notes: ['This endpoint returns the required scoped key metadata.'], +}; + +const TOKEN_POST = { + ...TAGS_OAUTH_SERVER, + description: '/v1/token', + notes: [ + dedent` + After receiving an authorization grant from the user, clients exercise that grant at this endpoint to obtain tokens that can be used to access attached services for a particular user. + + The following types of grant are possible: + + - \`authorization_code\`: a single-use code as produced by the [authorization][] endpoint, obtained through a redirect-based authorization flow. + - \`refresh_token\`: a token previously obtained from this endpoint when using access_type=offline. + - \`fxa-credentials\`: an FxA identity assertion, obtained by directly authenticating the user's account. + + **WARNING**: Do not include \`scope\` unless you want to downgrade it. + `, + ], +}; + +const VERIFY_POST = { + ...TAGS_OAUTH_SERVER, + description: '/v1/verify', + notes: [ + 'Attached services can post tokens to this endpoint to learn about which user and scopes are permitted for the token.', + ], +}; + +const API_DOCS = { + OAUTH_SERVER_API_DESCRIPTION, + AUTHORIZATION_GET, + AUTHORIZATION_POST, + DESTROY_POST, + AUTHORIZED_CLIENTS_DESTROY_POST, + AUTHORIZED_CLIENTS_POST, + CLIENT_CLIENTID_GET, + INTROSPECT_POST, + JWKS_GET, + KEY_DATA_POST, + TOKEN_POST, + VERIFY_POST, +}; + +export default API_DOCS; diff --git a/packages/fxa-auth-server/docs/swagger/shared/descriptions.ts b/packages/fxa-auth-server/docs/swagger/shared/descriptions.ts index f95e09bc873..dfc44184e88 100644 --- a/packages/fxa-auth-server/docs/swagger/shared/descriptions.ts +++ b/packages/fxa-auth-server/docs/swagger/shared/descriptions.ts @@ -6,28 +6,39 @@ import dedent from 'dedent'; const DESCRIPTIONS = { accessToken: - "An OAuth access token that the client can use to access data associated with the user's account.", + "An OAuth access token that the client can use for authorized requests to service providers to access data associated with the user's account.", accessType: 'If specified, a value of `offline` will cause the connecting client to be granted a refresh token alongside its access token.', acrValues: - 'A space-separated list of ACR values specifying acceptable levels of user authentication. Specifying `AAL2` will ensure that the user has been authenticated with 2FA before authorizing the requested grant.', + 'A space-separated list of ACR values specifying acceptable levels of user authentication that the token should have a claim for. Specifying `AAL2` will require the token to have an authentication assuarance level >= 2 which ensures that the user has been authenticated with 2FA before authorizing the requested grant.', + active: 'Boolean indicator of weather the presented token is active.', + assertion: 'A FxA assertion for the signed-in user.', authAt: - 'The timestamp for the session at which the user last authenticated to FxA, in seconds since the epoch.', + 'The UTC unix timestamp for the session at which the user last authenticated to FxA server when generating this token, in seconds since the epoch.', authPW: 'The PBKDF2/HKDF-stretched password as a hex string.', bundle: 'See [**decrypting the bundle**](https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#Decrypting_the_getToken2_Response) for information on how to extract kA|wrapKb from the bundle.', clientId: - 'The OAuth client identifier for the requesting client application (provided by the connecting client application).', + 'The OAuth client identifier for the requesting client application (provided by the connecting client application)', + clientIdPermission: ' asking for permission.', + clientIdRegistration: ' returned from client registration.', + clientIdToDelete: ' whose tokens should be deleted.', + clientName: 'The string name of the client.', clientSecret: 'The OAuth client secret for the requesting client application. Required for confidential clients, forbidden for public clients.', code: 'Time based code to verify secondary email', + codeOauth: + 'A string that the client will trade with the [token][] endpoint. Codes have a configurable expiration value, default is 15 minutes. Codes are single use only.', codeChallenge: - 'Required for public OAuth clients, who must authenticate their authorization code use via [**PKCE**](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/fxa-oauth-server/docs/pkce.md).', - codeChallengeMethod: `Required for public OAuth clients, who must authenticate their authorization code use via [**PKCE**](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/fxa-oauth-server/docs/pkce.md). The only support method is 'S256'.`, + 'Required for public OAuth clients, who must authenticate their authorization code use via [**PKCE**](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/fxa-oauth-server/docs/pkce.md). A minimum length of 43 characters and a maximum length of 128 characters string, encoded as `BASE64URL`.', + codeChallengeMethod: `Required for public OAuth clients, who must authenticate their authorization code use via [**PKCE**](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/fxa-oauth-server/docs/pkce.md). The only support method is 'S256', no other value is accepted.`, codeRecovery: "The code sent to the user's recovery email.", codeTotp: 'The TOTP code to check', + codeVerifier: + 'The [PKCE](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/oauth/pkce.md) code verifier. Required for public clients, forbidden otherwise.', command: "The id of the command to be invoked, as found in the device's availableCommands set.", + createdTime: 'Integer time of token creation.', customSessionToken: 'Custom session token id to destroy.', duration: 'Time interval in milliseconds until the certificate will expire, up to a maximum of 24 hours.', @@ -39,8 +50,11 @@ const DESCRIPTIONS = { emailSecondaryVerify: 'The secondary email address to verify.', excluded: 'Array of device ids to exclude from the notification. Ignored unless `to:"all"` is specified.', + exp: 'Integer time of token expiration.', expiresIn: 'The number of seconds until the access token will expire.', - filterIdleDevicesTimestamp: 'Filter device list to only show devices active since UTC timestamp.', + filterIdleDevicesTimestamp: + 'Filter device list to only show devices active since UTC timestamp.', + 'fxa-lastUsedAt': ' Integer time when this token is last used.', grantType: dedent` The type of grant flow being used. If not specified, it will default to fxa-credentials unless a code parameter is provided, in which case it will default to authorization_code. The value of this parameter determines which other parameters will be expected in the request body, as follows: - When \`grant_type=authorization_code\`: @@ -55,27 +69,51 @@ const DESCRIPTIONS = { - \`access_type\`: *string, valid(online, offline), optional* If specified, a value of offline will cause the client to be granted a refresh token alongside its access token. -In addition, the request must be authenticated with a sessionToken. `, + grantTypeOauth: dedent` + - If \`authorization_code\`: + - \`client_id\`: The id returned from client registration. + - \`client_secret\`: The secret returned from client registration. Forbidden for public clients, required otherwise. + - \`code\`: A string that was received from the [authorization][] endpoint. + - \`code_verifier\`: The [PKCE](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/oauth/pkce.md) code verifier. Required for public clients, forbidden otherwise. + - If \`refresh_token\`: + - \`client_id\`: The id returned from client registration. + - \`client_secret\`: The secret returned from client registration. Forbidden for public (PKCE) clients, required otherwise. + - \`refresh_token\`: A string that received from the [token][] endpoint specifically as a refresh token. + - \`scope\`: (optional) A subset of scopes provided to this refresh_token originally, to receive an access_token with less permissions. + - If \`fxa-credentials\`: + - \`client_id\`: The id returned from client registration. + - \`assertion\`: FxA identity assertion authenticating the user. + - \`scope\`: (optional) A string-separated list of scopes to be authorized. + - \`access_type\`: (optional) Determines whether to generate a \`refresh_token\` (if \`offline\`) or not (if \`online\`). + `, + iat: 'Integer time of token creation.', idToken: - 'Open OpenID Connect identity token, provisioned if the openid scope was requested.', + 'OpenID Connect identity token, provisioned if the authorization was requested with `openid` scope.', + imageUri: 'A url to a logo or image that represents the client.', indexQuery: 'The index of the most recently seen command item. Only commands enqueued after the given index will be returned.', indexSchema: 'The largest index of the commands returned in this response. This value can be passed as the index parameter in subsequent calls in order to page through all the items.', + jti: 'The hex id of the token.', keys: 'Indicates whether a key-fetch token should be returned in the success response.', keysJwe: - 'An encrypted bundle of key material, to be returned to the client when it redeems the authorization code.', + 'An encrypted JWE bundle of key material, to be returned to the client when it redeems the authorization code.', + keysJweOauth: + 'Returns the JWE bundle of key material for any scopes that have keys, if `grant_type=authorization_code`', last: 'Indicates whether more commands and enqueued than could be returned within the specific limit.', + lastAccessTime: 'Integer last-access time for the token.', limit: 'The maximum number of commands to return. The default and maximum value for limit is 100.', location: "Object containing the client's state and country", messages: 'An array of individual commands for the device to process.', + name: 'A string name of the client.', originalLoginEmail: 'This parameter is the original email used to login with. Typically, it is specified after a user logins with a different email case, or changed their primary email address.', queryKeys: 'Indicates whether a new `keyFetchToken` is required, default to `false`.', payload: 'Opaque payload to be forwarded to the device.', ppidSeed: - 'Seed used in sub claim generation of JWT access tokens/ID tokens for clients with [**Pseudonymous Pairwise Identifiers (PPID)**](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/oauth/pairwise-pseudonymous-identifiers.md) enabled. Used to forcibly rotate the `sub` claim. If not specified, it will default to `0`.', + 'Seed used in `sub` claim generation of JWT access tokens/ID tokens for clients with [Pseudonymous Pairwise Identifiers (PPID)](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/oauth/pairwise-pseudonymous-identifiers.md) enabled. Used to forcibly rotate the `sub` claim. Must be an integer in the range 0-1024. If not specified, it will default to `0`.', publicKey: 'The key to sign (run bin/generate-keypair from [**browserid-crypto**](https://github.com/mozilla/browserid-crypto)).', pushPayload: @@ -88,40 +126,64 @@ const DESCRIPTIONS = { redirectTo: 'URL that the client should be redirected to after handling the request.', redirectUri: - 'The URI at which the connecting client expects to receive the authorization code. If supplied this must match the value provided during OAuth client registration.', + 'The URI at which the connecting client expects to receive the authorization code and redirect to after a successful oauth. If supplied, this must match the URL value provided during OAuth client registration.', refreshToken: 'A token that can be used to grant a new access token when the current one expires, via `grant_type=refresh_token` on this endpoint.', + refreshTokenOauth: dedent` + A refresh token to fetch a new access token when this one expires. Only present if: + + - \`grant_type=authorization_code\` and the original authorization request included \`access_type=offline\`. + - \`grant_type=fxa-credentials\` and the request included \`access_type=offline\`. + `, + refreshTokenId: 'The specific `refresh_token_id` to be destroyed.', reminder: 'Indicates that verification originates from a reminder email.', resource: 'Indicates the target service or resource at which access is being requested. Its value must be an absolute URI, and may include a query component but must not include a fragment component. Added to the `aud` claim of JWT access tokens.', + resourceOauth: + ' Optional if `response_type=token`, forbidden if `response_type=code`.', responseType: "Determines the format of the response. Since we only support the authorization-code grant flow, the only permitted value is 'code'.", + responseTypeOauth: dedent` + If supplied, must be either code or token. code is the default. token means the implicit grant is desired, and requires that the client have special permission to do so. + + - Note: new implementations should not use \`response_type=token\`; instead use \`grant_type=fxa-credentials\` at the [token][] endpoint. + `, resume: 'Opaque URL-encoded string to be included in the verification link as a query parameter.', scope: - 'A space-separated list of scope values held by the granted access token that the connecting client will be granted. The requested scope will be provided by the connecting client as part of its authorization request, but may be pruned by the user in a confirmation dialog before being sent to this endpoint.', + 'A space-separated list of scope values that the user has authorized, or is held by the granted access token that the connecting client will be granted. The requested scope will be provided by the connecting client as part of its authorization request, but may be pruned by the user in a confirmation dialog before being sent to this endpoint.', service: 'Opaque alphanumeric token to be included in verification links.', serviceRP: 'Identifies the relying service the user was interacting with that triggered the password reset.', sessionToken: 'Indicates whether a new `sessionToken` is required, default to `false`.', state: - 'An opaque string provided by the connecting client application, which will be returned unmodified alongside the authorization code. This can be used by the connecting client to guard against certain classes of attack in the redirect-based OAuth flow.', + 'An opaque string value provided by the connecting client application, which will be returned unmodified upon redirection alongside the authorization code. This can be used by the connecting client guard against certain classes of attack in the redirect-based OAuth flow to verify that the redirect is authentic.', + sub: 'The hex id of the user.', target: 'The id of the device on which to invoke the command.', to: "Devices to notify. String `'all'` or an array containing the relevant device ids.", token: 'The token to be revoked. If the specific token does not exist then this call will silently succeed.', + tokenOauth: 'An OAuth token string received from a client for the user', tokenType: - 'The type of token, which determins how the client should use it in subsequent requests. Currently only Bearer tokens are supported.', + 'The type of token, which determines how the client should use it in subsequent requests. Currently only Bearer tokens are supported.', + tokenTypeOauth: + 'A string representing the token type. It will be `access_token` or `refresh_token`', tokenTypeHint: 'A hint as to what type of token is being revoked. Expected values are "access_token" or "refresh_token", Unrecognized values will be silently ignored, and specifying an incorrect hint may cause to the request to take longer but will still result in the token being destroyed.', + trusted: 'Whether the client is a trusted internal application.', ttl: 'The time in milliseconds after which the command should expire, if not processed by the device.', + ttlOauth: + 'Indicates the requested lifespan in seconds for the `access_token` or implicit grant token. If unspecified, the value will default to an internal maximum limit allowed by the server, which is a configurable option, so clients must check the `expires_in` result property for the actual TTL - it is typically measured in minutes or hours.', + ttlOauthPostAuth: + ' Optional if `response_type=token`, forbidden if `response_type=code`.', ttlPushNotification: 'Push notification TTL, defaults to `0`.', ttlValidate: 'The desired lifetime of the issued access token, in seconds. The actual lifetime may be smaller than requested depending on server configuration, and will be returned in the `expired_in` property of the response.', type: 'The type of code being verified.', uid: 'The user id.', unblockCode: 'Alphanumeric code used to unblock certain rate-limitings.', + user: 'The uid of the respective user.', verificationMethod: dedent` If this param is specified, it forces the login to be verified using the specified method. Currently supported methods: diff --git a/packages/fxa-auth-server/docs/swagger/swagger-options.ts b/packages/fxa-auth-server/docs/swagger/swagger-options.ts index 6f6e3247e1f..f9e24dfe461 100644 --- a/packages/fxa-auth-server/docs/swagger/swagger-options.ts +++ b/packages/fxa-auth-server/docs/swagger/swagger-options.ts @@ -3,76 +3,55 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import dedent from 'dedent'; +import { AUTH_SERVER_API_DESCRIPTION } from './auth-server-api'; +import OAUTH_SERVER_DOCS from './oauth-server-api'; import TAGS from './swagger-tags'; export const swaggerOptions = { info: { - title: 'Firefox Accounts Authentication Server API', + title: 'Firefox Accounts API Documentation', description: dedent` - [**WARNING**]: This information may not be up-to-date, use it at your own risk. It may be worth verifying information in the source code before acting on anything you read here. - - This document provides protocol-level details of the Firefox Accounts auth server API. For a prose description of the client/server protocol and details on how each parameter is derived, see the [**API design document**](https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol). For a reference client implementation, see [**fxa-auth-client**](https://github.com/mozilla/fxa/tree/main/packages/fxa-auth-client). - - * [Overview](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/api.md#overview) - * [URL structure](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/api.md#url-structure) - * [Request format](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/api.md#request-format) - * [Response format](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/api.md#response-format) - * [Defined errors](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/api.md#defined-errors) - * [Responses from intermediary servers](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/api.md#responses-from-intermediary-servers) - * [Validation](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/api.md#validation) - * [Back-off protocol](https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/api.md#back-off-protocol) + [**DISCLAIMER**]: This information may not be up-to-date - it may be worth verifying information in the source code before acting on anything you read here. `, }, basePath: '/v1', schemes: ['https'], tags: [ { - name: TAGS.ACCOUNT[1], - }, - { - name: TAGS.DEVICES_AND_SESSIONS[1], - }, - { - name: TAGS.EMAILS[1], - }, - { - name: TAGS.MISCELLANEOUS[1], - }, - { - name: TAGS.OAUTH[1], - }, - { - name: TAGS.PASSWORD[1], - }, - { - name: TAGS.RECOVERY_CODES[1], - }, - { - name: TAGS.RECOVERY_KEY[1], - }, - { - name: TAGS.SECURITY_EVENTS[1], - }, - { - name: TAGS.SESSION[1], - }, - { - name: TAGS.SIGN[1], + name: TAGS.AUTH_SERVER[1], + ...AUTH_SERVER_API_DESCRIPTION, }, { - name: TAGS.SUBSCRIPTIONS[1], + name: TAGS.OAUTH_SERVER[1], + ...OAUTH_SERVER_DOCS.OAUTH_SERVER_API_DESCRIPTION, }, - { - name: TAGS.THIRD_PARTY_AUTH[1], - }, - { - name: TAGS.TOTP[1], - }, - { - name: TAGS.UNBLOCK_CODES[1], - }, - { - name: TAGS.UTIL[1], + ], + 'x-tagGroups': [ + { + name: 'Firefox Accounts Auth Server API', + tags: [ + TAGS.AUTH_SERVER[1], + TAGS.ACCOUNT[1], + TAGS.DEVICES_AND_SESSIONS[1], + TAGS.EMAILS[1], + TAGS.MISCELLANEOUS[1], + TAGS.OAUTH[1], + TAGS.PASSWORD[1], + TAGS.RECOVERY_CODES[1], + TAGS.RECOVERY_KEY[1], + TAGS.SECURITY_EVENTS[1], + TAGS.SESSION[1], + TAGS.SIGN[1], + TAGS.SUBSCRIPTIONS[1], + TAGS.THIRD_PARTY_AUTH[1], + TAGS.TOTP[1], + TAGS.UNBLOCK_CODES[1], + TAGS.UTIL[1], + ], + }, + { + name: 'Firefox Accounts OAuth Server API', + tags: [TAGS.OAUTH_SERVER[1]], }, ], grouping: 'tags', diff --git a/packages/fxa-auth-server/docs/swagger/swagger-tags.ts b/packages/fxa-auth-server/docs/swagger/swagger-tags.ts index ad066fd4093..abe306e52a2 100644 --- a/packages/fxa-auth-server/docs/swagger/swagger-tags.ts +++ b/packages/fxa-auth-server/docs/swagger/swagger-tags.ts @@ -4,10 +4,12 @@ const TAGS = { ACCOUNT: ['api', 'Account'], + AUTH_SERVER: ['api', 'Auth Server API Overview'], DEVICES_AND_SESSIONS: ['api', 'Devices and Sessions'], EMAILS: ['api', 'Emails'], MISCELLANEOUS: ['api', 'Miscellaneous'], OAUTH: ['api', 'Oauth'], + OAUTH_SERVER: ['api', 'OAuth Server API Overview'], PASSWORD: ['api', 'Password'], RECOVERY_CODES: ['api', 'Recovery codes'], RECOVERY_KEY: ['api', 'Recovery key'], diff --git a/packages/fxa-auth-server/lib/routes/oauth/authorization.js b/packages/fxa-auth-server/lib/routes/oauth/authorization.js index df8edf4d190..f8df96fb49f 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/authorization.js +++ b/packages/fxa-auth-server/lib/routes/oauth/authorization.js @@ -11,8 +11,9 @@ const validators = require('../../oauth/validators'); const { validateRequestedGrant, generateTokens } = require('../../oauth/grant'); const { makeAssertionJWT } = require('../../oauth/util'); const verifyAssertion = require('../../oauth/assertion'); -const MISC_DOCS = require('../../../docs/swagger/misc-api').default; const OAUTH_DOCS = require('../../../docs/swagger/oauth-api').default; +const OAUTH_SERVER_DOCS = + require('../../../docs/swagger/oauth-server-api').default; const DESCRIPTION = require('../../../docs/swagger/shared/descriptions').default; @@ -181,7 +182,7 @@ module.exports = ({ log, oauthDB, config }) => { method: 'GET', path: '/authorization', config: { - ...MISC_DOCS.AUTHORIZATION_GET, + ...OAUTH_SERVER_DOCS.AUTHORIZATION_GET, cors: { origin: 'ignore' }, handler: async function redirectAuthorization(req, h) { // keys_jwk is barred from transiting the OAuth server @@ -202,27 +203,36 @@ module.exports = ({ log, oauthDB, config }) => { method: 'POST', path: '/authorization', config: { - ...MISC_DOCS.AUTHORIZATION_POST, + ...OAUTH_SERVER_DOCS.AUTHORIZATION_POST, cors: { origin: 'ignore' }, validate: { payload: Joi.object({ - client_id: validators.clientId, - assertion: validators.assertion.required(), + client_id: validators.clientId.description( + DESCRIPTION.clientId + DESCRIPTION.clientIdRegistration + ), + assertion: validators.assertion + .required() + .description(DESCRIPTION.assertion), redirect_uri: Joi.string() .max(256) // uri validation ref: https://github.com/hapijs/joi/blob/master/API.md#stringurioptions .uri({ scheme: ALLOWED_SCHEMES, - }), - scope: validators.scope.required(), + }) + .description(DESCRIPTION.redirectUri), + scope: validators.scope.required().description(DESCRIPTION.scope), response_type: Joi.string() .valid(RESPONSE_TYPE_CODE, RESPONSE_TYPE_TOKEN) - .default(RESPONSE_TYPE_CODE), - state: Joi.string().max(512).when('response_type', { - is: RESPONSE_TYPE_TOKEN, - then: Joi.optional(), - otherwise: Joi.required(), - }), + .default(RESPONSE_TYPE_CODE) + .description(DESCRIPTION.responseTypeOauth), + state: Joi.string() + .max(512) + .when('response_type', { + is: RESPONSE_TYPE_TOKEN, + then: Joi.optional(), + otherwise: Joi.required(), + }) + .description(DESCRIPTION.state), ttl: Joi.number() .positive() .default(MAX_TTL_S) @@ -230,11 +240,13 @@ module.exports = ({ log, oauthDB, config }) => { is: RESPONSE_TYPE_TOKEN, then: Joi.optional(), otherwise: Joi.forbidden(), - }), + }) + .description(DESCRIPTION.ttlOauth + DESCRIPTION.ttlOAuthPostAuth), access_type: Joi.string() .valid(ACCESS_TYPE_OFFLINE, ACCESS_TYPE_ONLINE) .default(ACCESS_TYPE_ONLINE) - .optional(), + .optional() + .description(DESCRIPTION.accessType), code_challenge_method: Joi.string() .valid(PKCE_SHA256_CHALLENGE_METHOD) .when('response_type', { @@ -245,39 +257,53 @@ module.exports = ({ log, oauthDB, config }) => { .when('code_challenge', { is: Joi.string().required(), then: Joi.required(), - }), + }) + .description(DESCRIPTION.codeChallengeMethod), code_challenge: Joi.string() .length(PKCE_CODE_CHALLENGE_LENGTH) .when('response_type', { is: RESPONSE_TYPE_CODE, then: Joi.optional(), otherwise: Joi.forbidden(), - }), - keys_jwe: validators.jwe.when('response_type', { - is: RESPONSE_TYPE_CODE, - then: Joi.optional(), - otherwise: Joi.forbidden(), - }), - acr_values: Joi.string().max(256).optional().allow(null), + }) + .description(DESCRIPTION.codeChallenge), + keys_jwe: validators.jwe + .when('response_type', { + is: RESPONSE_TYPE_CODE, + then: Joi.optional(), + otherwise: Joi.forbidden(), + }) + .description(DESCRIPTION.keysJwe), + acr_values: Joi.string() + .max(256) + .optional() + .allow(null) + .description(DESCRIPTION.acrValues), - resource: validators.resourceUrl.when('response_type', { - is: RESPONSE_TYPE_TOKEN, - then: Joi.optional(), - otherwise: Joi.forbidden(), - }), + resource: validators.resourceUrl + .when('response_type', { + is: RESPONSE_TYPE_TOKEN, + then: Joi.optional(), + otherwise: Joi.forbidden(), + }) + .description(DESCRIPTION.resource + DESCRIPTION.resourceOauth), }), }, response: { schema: Joi.object() .keys({ redirect: Joi.string(), - code: Joi.string(), - state: Joi.string(), - access_token: validators.accessToken, - token_type: Joi.string().valid('bearer'), - scope: Joi.string().allow(''), - auth_at: Joi.number(), - expires_in: Joi.number(), + code: Joi.string().description(DESCRIPTION.codeOauth), + state: Joi.string().description(DESCRIPTION.state), + access_token: validators.accessToken.description( + DESCRIPTION.accessToken + ), + token_type: Joi.string() + .valid('bearer') + .description(DESCRIPTION.tokenType), + scope: Joi.string().allow('').description(DESCRIPTION.scope), + auth_at: Joi.number().description(DESCRIPTION.authAt), + expires_in: Joi.number().description(DESCRIPTION.expiresIn), }) .with('access_token', [ 'token_type', diff --git a/packages/fxa-auth-server/lib/routes/oauth/authorized-clients/destroy.js b/packages/fxa-auth-server/lib/routes/oauth/authorized-clients/destroy.js index 84e557e882a..cbb0673a110 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/authorized-clients/destroy.js +++ b/packages/fxa-auth-server/lib/routes/oauth/authorized-clients/destroy.js @@ -6,19 +6,26 @@ const Joi = require('joi'); const validators = require('../../../oauth/validators'); const authorizedClients = require('../../../oauth/authorized_clients'); const verifyAssertion = require('../../../oauth/assertion'); -const MISC_DOCS = require('../../../../docs/swagger/misc-api').default; +const DESCRIPTION = + require('../../../../docs/swagger/shared/descriptions').default; +const OAUTH_SERVER_DOCS = + require('../../../../docs/swagger/oauth-server-api').default; module.exports = () => ({ method: 'POST', path: '/authorized-clients/destroy', config: { - ...MISC_DOCS.AUTHORIZED_CLIENTS_DESTROY_POST, + ...OAUTH_SERVER_DOCS.AUTHORIZED_CLIENTS_DESTROY_POST, cors: { origin: 'ignore' }, validate: { payload: Joi.object({ - client_id: validators.clientId, - refresh_token_id: validators.token.optional(), - assertion: validators.assertion, + client_id: validators.clientId.description( + DESCRIPTION.clientId + DESCRIPTION.clientIdToDelete + ), + refresh_token_id: validators.token + .optional() + .description(DESCRIPTION.refreshTokenId), + assertion: validators.assertion.description(DESCRIPTION.assertion), }), }, handler: async function (req) { diff --git a/packages/fxa-auth-server/lib/routes/oauth/authorized-clients/list.js b/packages/fxa-auth-server/lib/routes/oauth/authorized-clients/list.js index cd8e733f653..10da617b14f 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/authorized-clients/list.js +++ b/packages/fxa-auth-server/lib/routes/oauth/authorized-clients/list.js @@ -6,28 +6,47 @@ const Joi = require('joi'); const validators = require('../../../oauth/validators'); const verifyAssertion = require('../../../oauth/assertion'); const authorizedClients = require('../../../oauth/authorized_clients'); -const MISC_DOCS = require('../../../../docs/swagger/misc-api').default; +const DESCRIPTION = + require('../../../../docs/swagger/shared/descriptions').default; +const OAUTH_SERVER_DOCS = + require('../../../../docs/swagger/oauth-server-api').default; module.exports = () => ({ method: 'POST', path: '/authorized-clients', config: { - ...MISC_DOCS.AUTHORIZED_CLIENTS_POST, + ...OAUTH_SERVER_DOCS.AUTHORIZED_CLIENTS_POST, cors: { origin: 'ignore' }, validate: { payload: Joi.object({ - assertion: validators.assertion.required(), + assertion: validators.assertion + .required() + .description(DESCRIPTION.assertion), }), }, response: { schema: Joi.array().items( Joi.object({ - client_id: validators.clientId, - refresh_token_id: validators.token.optional(), - client_name: Joi.string().required(), - created_time: Joi.number().min(0).required(), - last_access_time: Joi.number().min(0).required().allow(null), - scope: Joi.array().items(Joi.string()).required(), + client_id: validators.clientId.description(DESCRIPTION.clientId), + refresh_token_id: validators.token + .optional() + .description(DESCRIPTION.refreshTokenId), + client_name: Joi.string() + .required() + .description(DESCRIPTION.clientName), + created_time: Joi.number() + .min(0) + .required() + .description(DESCRIPTION.createdTime), + last_access_time: Joi.number() + .min(0) + .required() + .allow(null) + .description(DESCRIPTION.lastAccessTime), + scope: Joi.array() + .items(Joi.string()) + .required() + .description(DESCRIPTION.scope), }) ), }, diff --git a/packages/fxa-auth-server/lib/routes/oauth/client/get.js b/packages/fxa-auth-server/lib/routes/oauth/client/get.js index c05df547e77..43e605ab7ee 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/client/get.js +++ b/packages/fxa-auth-server/lib/routes/oauth/client/get.js @@ -7,26 +7,36 @@ const Joi = require('joi'); const AppError = require('../../../oauth/error'); const validators = require('../../../oauth/validators'); -const MISC_DOCS = require('../../../../docs/swagger/misc-api').default; +const DESCRIPTION = + require('../../../../docs/swagger/shared/descriptions').default; +const OAUTH_SERVER_DOCS = + require('../../../../docs/swagger/oauth-server-api').default; module.exports = ({ log, oauthDB }) => ({ method: 'GET', path: '/client/{client_id}', config: { - ...MISC_DOCS.CLIENT_CLIENTID_GET, + ...OAUTH_SERVER_DOCS.CLIENT_CLIENTID_GET, cors: { origin: 'ignore' }, validate: { params: { - client_id: validators.clientId.required(), + client_id: validators.clientId + .required() + .description(DESCRIPTION.clientId + DESCRIPTION.clientIdPermission), }, }, response: { schema: Joi.object({ - id: validators.clientId, - name: Joi.string().required(), - trusted: Joi.boolean().required(), - image_uri: Joi.any(), - redirect_uri: Joi.string().required().allow(''), + id: validators.clientId.description( + DESCRIPTION.clientId + DESCRIPTION.clientIdPermission + ), + name: Joi.string().required().description(DESCRIPTION.name), + trusted: Joi.boolean().required().description(DESCRIPTION.trusted), + image_uri: Joi.any().description(DESCRIPTION.imageUri), + redirect_uri: Joi.string() + .required() + .allow('') + .description(DESCRIPTION.redirectUri), }), }, handler: async function requestInfoEndpoint(req) { diff --git a/packages/fxa-auth-server/lib/routes/oauth/destroy.js b/packages/fxa-auth-server/lib/routes/oauth/destroy.js index af73e5a0613..34eaf5c055e 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/destroy.js +++ b/packages/fxa-auth-server/lib/routes/oauth/destroy.js @@ -15,8 +15,9 @@ const { authenticateClient, clientAuthValidators, } = require('../../oauth/client'); -const MISC_DOCS = require('../../../docs/swagger/misc-api').default; const OAUTH_DOCS = require('../../../docs/swagger/oauth-api').default; +const OAUTH_SERVER_DOCS = + require('../../../docs/swagger/oauth-server-api').default; const DESCRIPTION = require('../../../docs/swagger/shared/descriptions').default; @@ -71,22 +72,29 @@ module.exports = ({ log, oauthDB }) => { method: 'POST', path: '/destroy', config: { - ...MISC_DOCS.DESTROY_POST, + ...OAUTH_SERVER_DOCS.DESTROY_POST, cors: { origin: 'ignore' }, validate: { headers: clientAuthValidators.headers, payload: Joi.object() .keys({ - client_id: clientAuthValidators.clientId.optional(), + client_id: clientAuthValidators.clientId + .optional() + .description(DESCRIPTION.clientId), // For historical reasons, we accept and ignore a client_secret if one // is provided without a corresponding client_id. // https://github.com/mozilla/fxa-oauth-server/pull/198 client_secret: clientAuthValidators.clientSecret .allow('') - .optional(), - access_token: validators.accessToken, + .optional() + .description(DESCRIPTION.clientSecret), + access_token: validators.accessToken.description( + DESCRIPTION.accessToken + ), refresh_token: validators.token, - refresh_token_id: validators.token, + refresh_token_id: validators.token.description( + DESCRIPTION.refreshTokenId + ), }) .rename('token', 'access_token') .xor('access_token', 'refresh_token', 'refresh_token_id'), diff --git a/packages/fxa-auth-server/lib/routes/oauth/index.js b/packages/fxa-auth-server/lib/routes/oauth/index.js index 0d2e5a8d9a1..2f4f716ffb6 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/index.js +++ b/packages/fxa-auth-server/lib/routes/oauth/index.js @@ -17,6 +17,7 @@ module.exports = (log, config, db, mailer, devices) => { const clientGetAlias = require('./client/get')({ log, oauthDB }); clientGetAlias.path = '/oauth/client/{client_id}'; + clientGetAlias.config.description = '/oauth/client/{client_id}'; clientGetAlias.config.notes = [ 'Retrieve metadata about the specified OAuth client, such as its display name and redirect URI.', ]; diff --git a/packages/fxa-auth-server/lib/routes/oauth/introspect.js b/packages/fxa-auth-server/lib/routes/oauth/introspect.js index 8649bb0b2e6..36ec121ca0b 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/introspect.js +++ b/packages/fxa-auth-server/lib/routes/oauth/introspect.js @@ -8,11 +8,15 @@ const validators = require('../../oauth/validators'); const hex = require('buf').to.hex; const AppError = require('../../oauth/error'); const { getTokenId } = require('../../oauth/token'); -const MISC_DOCS = require('../../../docs/swagger/misc-api').default; - +const OAUTH_SERVER_DOCS = + require('../../../docs/swagger/oauth-server-api').default; +const DESCRIPTION = + require('../../../docs/swagger/shared/descriptions').default; const PAYLOAD_SCHEMA = Joi.object({ - token: Joi.string().required(), - token_type_hint: Joi.string().equal('access_token', 'refresh_token'), + token: Joi.string().required().description(DESCRIPTION.tokenOauth), + token_type_hint: Joi.string() + .equal('access_token', 'refresh_token') + .description(DESCRIPTION.tokenTypeHint), }); // The "token introspection" endpoint, per https://tools.ietf.org/html/rfc7662 @@ -21,7 +25,7 @@ module.exports = ({ oauthDB }) => ({ method: 'POST', path: '/introspect', config: { - ...MISC_DOCS.INTROSPECT_POST, + ...OAUTH_SERVER_DOCS.INTROSPECT_POST, cors: { origin: 'ignore' }, validate: { payload: PAYLOAD_SCHEMA.options({ stripUnknown: true }), @@ -29,16 +33,22 @@ module.exports = ({ oauthDB }) => ({ response: { schema: Joi.object().keys({ // https://tools.ietf.org/html/rfc7662#section-2.2 - active: Joi.boolean().required(), - scope: validators.scope.optional(), - client_id: validators.clientId.optional(), - token_type: Joi.string().equal('access_token', 'refresh_token'), - exp: Joi.number().optional(), - iat: Joi.number().optional(), - sub: Joi.string().optional(), + active: Joi.boolean().required().description(DESCRIPTION.active), + scope: validators.scope.optional().description(DESCRIPTION.scope), + client_id: validators.clientId + .optional() + .description(DESCRIPTION.clientId), + token_type: Joi.string() + .equal('access_token', 'refresh_token') + .description(DESCRIPTION.tokenTypeOauth), + exp: Joi.number().optional().description(DESCRIPTION.exp), + iat: Joi.number().optional().description(DESCRIPTION.iat), + sub: Joi.string().optional().description(DESCRIPTION.sub), iss: Joi.string().optional(), - jti: Joi.string().optional(), - 'fxa-lastUsedAt': Joi.number().optional(), + jti: Joi.string().optional().description(DESCRIPTION.jti), + 'fxa-lastUsedAt': Joi.number() + .optional() + .description(DESCRIPTION['fxa-lastUsedAt']), }), }, handler: async function introspectEndpoint(req) { diff --git a/packages/fxa-auth-server/lib/routes/oauth/jwks.js b/packages/fxa-auth-server/lib/routes/oauth/jwks.js index 12267a04dd7..2adf1d10cc5 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/jwks.js +++ b/packages/fxa-auth-server/lib/routes/oauth/jwks.js @@ -3,13 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { PUBLIC_KEYS } = require('../../oauth/keys'); -const MISC_DOCS = require('../../../docs/swagger/misc-api').default; +const OAUTH_SERVER_DOCS = + require('../../../docs/swagger/oauth-server-api').default; module.exports = () => ({ method: 'GET', path: '/jwks', config: { - ...MISC_DOCS.JWKS_GET, + ...OAUTH_SERVER_DOCS.JWKS_GET, cors: { origin: 'ignore' }, cache: { privacy: 'public', diff --git a/packages/fxa-auth-server/lib/routes/oauth/key_data.js b/packages/fxa-auth-server/lib/routes/oauth/key_data.js index dccef1dbec7..8b5bd0d6228 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/key_data.js +++ b/packages/fxa-auth-server/lib/routes/oauth/key_data.js @@ -10,8 +10,11 @@ const validators = require('../../oauth/validators'); const verifyAssertion = require('../../oauth/assertion'); const { validateRequestedGrant } = require('../../oauth/grant'); const { makeAssertionJWT } = require('../../oauth/util'); -const MISC_DOCS = require('../../../docs/swagger/misc-api').default; +const DESCRIPTION = + require('../../../docs/swagger/shared/descriptions').default; const OAUTH_DOCS = require('../../../docs/swagger/oauth-api').default; +const OAUTH_SERVER_DOCS = + require('../../../docs/swagger/oauth-server-api').default; /** * We don't yet support rotating individual scoped keys, @@ -97,13 +100,15 @@ module.exports = ({ log, oauthDB }) => { method: 'POST', path: '/key-data', config: { - ...MISC_DOCS.KEY_DATA_POST, + ...OAUTH_SERVER_DOCS.KEY_DATA_POST, cors: { origin: 'ignore' }, validate: { payload: Joi.object({ - client_id: validators.clientId, - assertion: validators.assertion.required(), - scope: validators.scope.required(), + client_id: validators.clientId.description(DESCRIPTION.clientId), + assertion: validators.assertion + .required() + .description(DESCRIPTION.assertion), + scope: validators.scope.required().description(DESCRIPTION.scope), }), }, response: { diff --git a/packages/fxa-auth-server/lib/routes/oauth/token.js b/packages/fxa-auth-server/lib/routes/oauth/token.js index 67ed2fe440f..749864b69ef 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/token.js +++ b/packages/fxa-auth-server/lib/routes/oauth/token.js @@ -50,8 +50,9 @@ const { clientAuthValidators, } = require('../../oauth/client'); const ScopeSet = require('fxa-shared').oauth.scopes; -const MISC_DOCS = require('../../../docs/swagger/misc-api').default; const OAUTH_DOCS = require('../../../docs/swagger/oauth-api').default; +const OAUTH_SERVER_DOCS = + require('../../../docs/swagger/oauth-server-api').default; const DESCRIPTION = require('../../../docs/swagger/shared/descriptions').default; @@ -78,7 +79,7 @@ const SCOPES_TO_EXCLUDE_FROM_REFRESH_TOKEN_GRANTS = ScopeSet.fromArray([ ]); const PAYLOAD_SCHEMA = Joi.object({ - client_id: clientAuthValidators.clientId, + client_id: clientAuthValidators.clientId.description(DESCRIPTION.clientId), // The client_secret can be specified in Authorization header or request body, // but not both. In the code flow it is exclusive with `code_verifier`, and @@ -95,19 +96,28 @@ const PAYLOAD_SCHEMA = Joi.object({ .when('grant_type', { is: GRANT_FXA_ASSERTION, then: Joi.optional(), - }), + }) + .description(DESCRIPTION.clientSecret), - redirect_uri: validators.redirectUri.optional().when('grant_type', { - is: GRANT_AUTHORIZATION_CODE, - otherwise: Joi.forbidden(), - }), + redirect_uri: validators.redirectUri + .optional() + .when('grant_type', { + is: GRANT_AUTHORIZATION_CODE, + otherwise: Joi.forbidden(), + }) + .description(DESCRIPTION.redirectUri), grant_type: Joi.string() .valid(GRANT_AUTHORIZATION_CODE, GRANT_REFRESH_TOKEN, GRANT_FXA_ASSERTION) .default(GRANT_AUTHORIZATION_CODE) - .optional(), + .optional() + .description(DESCRIPTION.grantTypeOauth), - ttl: Joi.number().positive().default(MAX_TTL_S).optional(), + ttl: Joi.number() + .positive() + .default(MAX_TTL_S) + .optional() + .description(DESCRIPTION.ttlOauth), scope: Joi.alternatives() .conditional('grant_type', { @@ -118,7 +128,8 @@ const PAYLOAD_SCHEMA = Joi.object({ is: GRANT_FXA_ASSERTION, then: validators.scope.required(), otherwise: Joi.forbidden(), - }), + }) + .description(DESCRIPTION.scope), access_type: Joi.string() .valid(ACCESS_TYPE_OFFLINE, ACCESS_TYPE_ONLINE) @@ -127,7 +138,9 @@ const PAYLOAD_SCHEMA = Joi.object({ .when('grant_type', { is: GRANT_FXA_ASSERTION, otherwise: Joi.forbidden(), - }), + }) + .description(DESCRIPTION.accessType), + code: Joi.string() .length(config.get('oauthServer.unique.code') * 2) .regex(validators.HEX_STRING) @@ -135,26 +148,35 @@ const PAYLOAD_SCHEMA = Joi.object({ .when('grant_type', { is: GRANT_AUTHORIZATION_CODE, otherwise: Joi.forbidden(), - }), + }) + .description(DESCRIPTION.codeOauth), - code_verifier: validators.codeVerifier.when('code', { - is: Joi.string().required(), - otherwise: Joi.forbidden(), - }), + code_verifier: validators.codeVerifier + .when('code', { + is: Joi.string().required(), + otherwise: Joi.forbidden(), + }) + .description(DESCRIPTION.codeVerifier), - refresh_token: validators.token.required().when('grant_type', { - is: GRANT_REFRESH_TOKEN, - otherwise: Joi.forbidden(), - }), + refresh_token: validators.token + .required() + .when('grant_type', { + is: GRANT_REFRESH_TOKEN, + otherwise: Joi.forbidden(), + }) + .description(DESCRIPTION.refreshToken), - assertion: validators.assertion.required().when('grant_type', { - is: GRANT_FXA_ASSERTION, - otherwise: Joi.forbidden(), - }), + assertion: validators.assertion + .required() + .when('grant_type', { + is: GRANT_FXA_ASSERTION, + otherwise: Joi.forbidden(), + }) + .description(DESCRIPTION.assertion), - ppid_seed: validators.ppidSeed.optional(), + ppid_seed: validators.ppidSeed.optional().description(DESCRIPTION.ppidSeed), - resource: validators.resourceUrl.optional(), + resource: validators.resourceUrl.optional().description(DESCRIPTION.resource), }); module.exports = ({ log, oauthDB, db, mailer, devices }) => { @@ -356,7 +378,7 @@ module.exports = ({ log, oauthDB, db, mailer, devices }) => { method: 'POST', path: '/token', config: { - ...MISC_DOCS.TOKEN_POST, + ...OAUTH_SERVER_DOCS.TOKEN_POST, cors: { origin: 'ignore' }, validate: { headers: clientAuthValidators.headers, @@ -368,15 +390,27 @@ module.exports = ({ log, oauthDB, db, mailer, devices }) => { }, response: { schema: Joi.object().keys({ - access_token: validators.accessToken.required(), - refresh_token: validators.token, - id_token: validators.assertion, + access_token: validators.accessToken + .required() + .description(DESCRIPTION.accessToken), + refresh_token: validators.token.description( + DESCRIPTION.refreshTokenOauth + ), + id_token: validators.assertion.description(DESCRIPTION.idToken), session_token_id: validators.sessionTokenId.optional(), - scope: validators.scope.required(), - token_type: Joi.string().valid('bearer').required(), - expires_in: Joi.number().max(MAX_TTL_S).required(), - auth_at: Joi.number(), - keys_jwe: validators.jwe.optional(), + scope: validators.scope.required().description(DESCRIPTION.scope), + token_type: Joi.string() + .valid('bearer') + .required() + .description(DESCRIPTION.tokenType), + expires_in: Joi.number() + .max(MAX_TTL_S) + .required() + .description(DESCRIPTION.expiresIn), + auth_at: Joi.number().description(DESCRIPTION.authAt), + keys_jwe: validators.jwe + .optional() + .description(DESCRIPTION.keysJweOauth), }), }, handler: tokenHandler, diff --git a/packages/fxa-auth-server/lib/routes/oauth/verify.js b/packages/fxa-auth-server/lib/routes/oauth/verify.js index 6b9870c4abc..ea93342afce 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/verify.js +++ b/packages/fxa-auth-server/lib/routes/oauth/verify.js @@ -3,26 +3,31 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const Joi = require('joi'); +const DESCRIPTIONS = + require('../../../docs/swagger/shared/descriptions').default; const token = require('../../oauth/token'); const validators = require('../../oauth/validators'); -const MISC_DOCS = require('../../../docs/swagger/misc-api').default; +const OAUTH_SERVER_DOCS = + require('../../../docs/swagger/oauth-server-api').default; module.exports = ({ log }) => ({ method: 'POST', path: '/verify', config: { - ...MISC_DOCS.VERIFY_POST, + ...OAUTH_SERVER_DOCS.VERIFY_POST, cors: { origin: 'ignore' }, validate: { payload: Joi.object({ - token: validators.accessToken.required(), + token: validators.accessToken + .required() + .description(DESCRIPTIONS.token), }), }, response: { schema: Joi.object({ - user: Joi.string().required(), - client_id: Joi.string().required(), - scope: Joi.array(), + user: Joi.string().required().description(DESCRIPTIONS.user), + client_id: Joi.string().required().description(DESCRIPTIONS.clientId), + scope: Joi.array().description(DESCRIPTIONS.scope), generation: Joi.number().min(0), profile_changed_at: Joi.number().min(0), }),