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:
ISSclaim must be the client idSUBclaim must be the user id for code, implicit and password grants and the client id for client credetials grantJTIclaim is required in order to track blacklisted tokensSCOPEclaim 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,SUBandSCOPEas the corresponding token JTIclaim must also be present and set to JTI of the corresponding access token.EXPclaim must be present
Authentication tokens which were discussed in the previous section must contain the following claims:
ISSclaim must be the client idSUBclaim must be the resource owner's idAUDclaim must be authorization server's authorize endpoint URLIATclaim 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": ""}