This design must be aware of the OAuth2 thread models and mitigation strategies as described in the following resources:
- OAuth 2.0 Threat Model and Security Considerations
- OAuth Security
- Common OAuth2 Vulnerabilities and Mitigation Techniques
- OAuth1, OAuth2, OAuth...?
A host of vulnerabilities can be removed by pinning redirect_uri
, scope
, response_type
(read: allowed grants for each client) variables in client settings when registering clients (apps).
Authentication is the process of ascertaining that somebody really is who he claims to be.
Authorization refers to rules that determine who is allowed to do what
The design of the OAuth system must be aware of this distinction and delegate authentication to the appropriate services.
During registration, clients may define an authentication service by its URI and supply an accompanying session signing key - these are considered trusted authenticators if approved for use. This is not required and most clients will likely allow the authorization service to determine where users should authenticate.
The authentication service must authenticate users by generating a JWT and redirecting the user back to the authorization flow with the query parameter session
set to the JWT.
In a distributed services architecture, it may be beneficial to use JWT to sign data between servers.
The examples that follow assume that the identity (read: authentication) service is separate to the oauth service. As such there are deviations from the OAuth 2.0 spec where we sign GET request parameters and attach them to a URL.
JWT can be used for access and refresh tokens. With regards to access tokens, it is recommended to follow the OAuth 2.0 JWT Bearer tokens spec with the following definitions:
ISS
claim must be the client idSUB
claim must be the user id for code, implicit and password grants and the client id for client credetials grantJTI
claim is required in order to track blacklisted tokensSCOPE
claim containing a comma-delimited list of scope strings e.g. "user,post:create,post:publish"
Refresh tokens are not covered by the spec but these can also be JWT tokens with:
- Identical
ISS
,AUD
,SUB
andSCOPE
as the corresponding token JTI
claim must also be present and set to JTI of the corresponding access token.EXP
claim must be present
Authentication tokens which were discussed in the previous section must contain the following claims:
ISS
claim must be the client idSUB
claim must be the resource owner's idAUD
claim must be authorization server's authorize endpoint URLIAT
claim must be present
During client registration, it is important to record a specific grant type and a single redirect URI. This prevents a host of vulnerabilities. The values for grant_type
and redirect_uri
passed by clients during the various flows may be ignored or verified against the stored values.
It is also important to pin down a set of scopes for each client. This will allow better controls and reviews of what clients can ask of resource owners (in many cases, the user).
At it's simplest the data model for a client must hold the following fields:
id - string - A generated client identifier grant_type - enum["code", "implicit", "password", "client"] - The allowed grant type for a client secret - string - A generated client secret for the client credentials grant type redirect_uri - string - A specific redirect URI for authorization code and implicit grant types scopes - List<string> - A list of allowed scopes that the client may request from users or on behalf of itself
-> GET /oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...
<- 303 /login?next=%2Foauth%2Fauthorize%3Fclient_id%3D...%26scope%3D...%26state%3D...
<- 200 {"code": ..., "state": ...}
<- 403 {"error": "", "error_description": "", "error_uri": "", "state": ""}
-> POST /login?username=...&password=...&next=%2Foauth%2Fauthorize%3Fclient_id%3D...%26scope%3D...%26state%3D...
<- 303 /oauth/authorize?session=...&client_id=...&scope=...&state=...
<- 403 {"error": "", "error_description": "", "error_uri": "", "state": ""}
-> POST /oauth/token?code=...&client_id=...
<- 200 {"access_token": "...", "refresh_token": "...", "token_type": "...", "expires_in": "...", "scope": "..."}
<- 403 {"error": "", "error_description": "", "error_uri": "", "state": ""}
-> GET /oauth/authorize?client_id=...&scope=...&state=...
<- 200 {"access_token": "...", "token_type": "...", "expires_in": "...", "scope": "..."}}
<- 303 /ident/sessions/oauthlogin?req=Sign({"redirect": "/oauth/authorize?client_id=...&scope=...&state=..."})
<- 403 {"error": "", "error_description": "", "error_uri": "", "state": ""}
-> POST /ident/sessions/oauthlogin?username=...&password=...&next=/oauth/authorize?client_id=...&scope=...&state=...
<- 303 /oauth/authorize?client_id=...&scope=...&state=...
<- 403 {"error": "", "error_description": "", "error_uri": "", "state": ""}
This grant type should only permitted by clients that also own the authorization service. Note that while the state parameter is not present a CSRF token would typically be present either in the form or as a header.
-> POST /oauth/token?client_id=...&scope=...&username=...&password=...
<- 200 {"access_token": "...", "token_type": "...", "expires_in": "...", "scope": "..."}}
<- 403 {"error": "", "error_description": "", "error_uri": "", "state": ""}
The client credentials grant does not deviate from the OAuth 2.0 spec except for the ommission of client_id from the POST form.
It is important to note that this flow does not issue refresh tokens and that clients MUST be authenticated before an access token is issued
-> POST /oauth/token?&client_id=...&client_secret=...&scope=...
<- 200 {"access_token": "...", "token_type": "...", "expires_in": "...", "scope": "..."}}
<- 403 {"error": "", "error_description": "", "error_uri": "", "state": ""}