In Part 1, I explained the OAuth protocol flow at a high-level. In this part, we will dive in to the most popular authorization grant type: the authorization code grant.
Grant Flow
The authorization code grant is a redirection-based flow:
- The client redirects the resource owner’s user agent (hereafter, we’ll just say “browser”) to the authorization server.
- The authorization server authenticates the user.
- The authorization server asks the resource owner for consent to give the client access to the protected resources.
- The authorization server redirects the resource owner’s browser to the client, including an authorization code in the URI.
- The client sends the authorization code to the authorization server using an HTTP POST request.
- The authorization server responds to the client’s request with an access token (and optionally a refresh token).
- The client sends a request to the resource server, including the access token.
- The resource server fulfils the client’s request.
Step by Step
Authorization Request
The initial authorization request is made by redirecting the resource owner’s browser to the authorization endpoint on the authorization server. The redirection can be achieved by any suitable means, but will usually be done using an HTTP redirection status code (e.g. 303). A number of parameters are required, which will be included in the redirection URI’s query component1:
- response_type
- For the authorization code flow, this will always be
code
. - client_id
- The client identifier the client was issued with during registration.
- redirect_uri
- The URI of the client’s redirection endpoint. The authorization response will be provided by redirecting the resource owner’s browser to this URI. This is optional2.
- scope
- The scopes that authorization is being requested for. The specification says this is optional, in which case a pre-defined default list of scopes is used. If there isn’t a pre-defined default, the request will fail.
- state
- An opaque value which will be included in the authorization response. This is
used to protect the client’s redirection endpoint against Cross-Site Request
Forgery (CSRF), so it should not be guessable and should be stored such that it
is only accessible to the client. Including
state
is optional, but strongly recommended.
Let’s make an authorization request for the read:photos
and write:photos
scopes. We will also use the offline_access
scope to get a refresh token3.
We redirect our user’s browser to the following URI:
https://jammystuff.eu.auth0.com/authorize?
response_type=code&
client_id=p1LJZfMDXrd2EEQp6oV77ASWuN9tlvsK&
redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback&
scope=read%3Aphotos%20write%3Aphotos%20offline_access&
state=veryrandomsecret
The authorization server will first authenticate the resource owner. If they have an existing authenticated session, that may be used, but if not they will be presented with a log in screen. The log in can be by any methods supported by the authorization server, including username and password, email “magic links”, WebAuthn, TOTP (e.g. Google Authenticator), etc.
Once the resource owner has been authenticated, the authorization server asks
for their consent to grant the requested access to the client. This will almost
always be by presenting a screen explaining the scopes that have been requested,
and allowing the resource owner to approve or deny the request (either
scope-by-scope, or overall). In this example, the authorization server explains
that we are granting Example Client
access to read and write the resource
owner’s photos.
Finally, the authorization server sends an authorization response by redirecting
the user’s browser to the redirect_uri
, including the response parameters.
Authorization Response
Successful Response
If the authorization request is correct, and the resource owner approves the it, the authorization server issues an authorization code in the authorization response. The response is sent by redirecting the resource owner’s browser to the client’s redirection endpoint, including some parameters in the URI’s query component:
- code
- The authorization code, which will be bound to the client ID and redirection URI that was used in the authorization request. It should be short-lived (~10 minutes) and must only be used once.
- state
- If
state
was sent in the authorization request, the exact value must be included in the response.
A successful response to our example authorization request would look like this:
https://app.example.com/oauth/callback?
code=0xXns_kANpljeIDT&
state=veryrandomsecret
Error Response
If the authorization request fails, the authorization server usually returns an
error response by redirecting the user’s browser to the client’s redirection
endpoint. The exception to this is when the error is an invalid or mismatching
redirect_uri
parameter. In this case, the authorization server should inform
the user, but must not automatically redirect them to the invalid
redirect_uri
.
For all other errors, the error response will be a redirection to the client’s redirection endpoint including the following parameters in the URI’s query component:
- error
- An error code. The errors are listed below.
- error_description
- A human-readable description providing additional information about the error. This audience of this description is the client developer, not the resource owner. This is optional.
- error_uri
- This is similar to
error_description
, but is a URI to a web page with the information rather than including it in the URI parameter itself. It is optional. - state
- If
state
was sent in the authorization request, the exact value must be included in the response.
The error codes can be one of the following:
- invalid_request
- The request is missing a parameter, repeats a parameter, includes an unsupported value for a parameter, includes multiple credentials or multiple methods of passing credentials, or is otherwise malformed.
- unauthorized_client
- The client is not authorized to request an authorization code using this grant type.
- access_denied
- The request was denied. This could be because consent was denied by the resource owner, or it could have been denied due to the authorization server’s policy.
- unsupported_response_type
- The authorization server does not support this grant type.
- invalid_scope
- The scope is invalid or malformed.
- server_error
- This has the same meaning as a
500 Internal Server Error
status code. It is sent as a parameter because an HTTP status code can’t be returned to the client in a redirection-based flow. - temporarily_unavailable
- This has the same meaning as a
503 Service Unavailable
status code. It is sent as a parameter because an HTTP status code can’t be returned to the client in a redirection-based flow.
For example, if the user denied our example authorization request, they would be redirected here:
https://app.example.com/oauth/callback?
error=access_denied&
error_description=User%20did%20not%20authorize%20the%20request&
state=veryrandomsecret
Access Token Request
Once the client has been issued with an authorization code, it needs to trade the code for an access token. To do this, the client sends an HTTP POST request to the authorization server, including the following parameters in the request body using Appendix B encoding:
- grant_type
- For the authorization code flow, this will always be
authorization_code
. - code
- The authorization code. The authorization server must check that it was issued to the client making the request.
- redirect_uri
- If the
redirect_uri
parameter was included in the authorization request, it must be included here with an identical value. - client_id
- If the client is not authenticating with the authorization server, or is authenticating by including the credentials in the request body, the client ID is included as a parameter.
- client_secret
- If the client is authenticating with the authorization server by including the credentials in the request body, the client secret is included as a parameter.
It is not recommended for client authentication to be done using parameters in the request body. Instead, HTTP Basic authentication is usually used, but authorization servers can support other authentication methods as well.
To illustrate how this works, let’s trade in the authorization code from our example authorization request using curl4 5:
% curl -i \
--basic \
-u p1LJZfMDXrd2EEQp6oV77ASWuN9tlvsK:ewfPPimoGanRHixXXI5j3kPoRErUci-VJrGnIRTtOQusqwzzjgjEf_ZBqYqYUOIp \
--data-urlencode grant_type=authorization_code \
--data-urlencode code=0xXns_kANpljeIDT \
--data redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback \
https://jammystuff.eu.auth0.com/oauth/token
HTTP/2 200
date: Sat, 12 Feb 2022 09:22:34 GMT
content-type: application/json
cf-ray: 6dc4c0d718e274fd-LHR
cache-control: no-store
set-cookie: did=s%3Av0%3A501248f0-8be5-11ec-9dfc-a545f2a6457d.QvpJ3ihNjShTeAQ4B1FOakKLc7HYWvu7kzghVteh1ig; Max-Age=31557600; Path=/; Expires=Sun, 12 Feb 2023 15:22:34 GMT; HttpOnly; Secure; SameSite=None
strict-transport-security: max-age=31536000
vary: Accept-Encoding, Origin
cf-cache-status: DYNAMIC
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
ot-baggage-auth0-request-id: 6dc4c0d718e274fd
ot-tracer-sampled: true
ot-tracer-spanid: 1474cb5c4bbacffe
ot-tracer-traceid: 1bd53cd75e04c881
pragma: no-cache
x-auth0-requestid: 7a66e91e0aba255dc520
x-content-type-options: nosniff
x-ratelimit-limit: 30
x-ratelimit-remaining: 29
x-ratelimit-reset: 1644657755
set-cookie: did_compat=s%3Av0%3A501248f0-8be5-11ec-9dfc-a545f2a6457d.QvpJ3ihNjShTeAQ4B1FOakKLc7HYWvu7kzghVteh1ig; Max-Age=31557600; Path=/; Expires=Sun, 12 Feb 2023 15:22:34 GMT; HttpOnly; Secure
set-cookie: __cf_bm=doX2okr31XhxPpCOHZDolyZJ4NAwLT3grjLJzC8CVh4-1644657754-0-AUphOk/ub6/C8x6ZE+bp0m5gqqa2iMdtEN+AxBl3cRrTN1S7jtWjmRXISApVIS+kh4WWcGFHW3v0gDWcYSOmRrc=; path=/; expires=Sat, 12-Feb-22 09:52:34 GMT; domain=.eu.auth0.com; HttpOnly; Secure; SameSite=None
server: cloudflare
alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlFrTkRNVGhEUkVFeE1VVkVSakEzTWtNeFFrVXdPREUxT0RrMU9UUTBSakEyUVVGR09FUXlSUSJ9.eyJpc3MiOiJodHRwczovL2phbW15c3R1ZmYuZXUuYXV0aDAuY29tLyIsInN1YiI6ImF1dGgwfDYxZjcwNzkyYzFkZDJlMDA2ZWE2ZDI5MiIsImF1ZCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tIiwiaWF0IjoxNjQ0NjU3NzU0LCJleHAiOjE2NDQ3NDQxNTQsImF6cCI6InAxTEpaZk1EWHJkMkVFUXA2b1Y3N0FTV3VOOXRsdnNLIiwic2NvcGUiOiJyZWFkOnBob3RvcyB3cml0ZTpwaG90b3Mgb2ZmbGluZV9hY2Nlc3MifQ.iYH1YxWtrOvJ2ruebFVRFEXnAGhYDM3xs2arEOiI_vGNhlxFN7OPq_ztWy6t7QDDbEq1FdG6Tyxi4gQRaaegk93eNo5BRVlf26nhpiO-UiLz3h6sQMFg8Grxwy0DE7J5suk9AQNdqMzTsg1PbcJ1jRj4o3f0iDN9ydWrfaHgP_JJrjwIRwx6Kaoe_yl1HTGwyzEdkolkl9hA_dg2d9il3tvm4KZyu0X-WJIxOknovnRtbvjzT45ZelXVF_td9mvRmdOwFx_6siUqOWJnvR6NlphB7-G9kYA-Z6bstRMbuv7FceSTo_yictzq5A-LINo__Yv37zOjjhIEAmHGL7VA5A",
"refresh_token": "fI1tqMHyaIvEOYX9zZ30OYZXc2sXuZZgPf7RqSnnJtXMD",
"scope": "read:photos write:photos offline_access",
"expires_in": 86400,
"token_type": "Bearer"
}
Access Token Response
The token endpoint response was explained in the previous part. You can see a successful response in the example above.
Aside: Refreshing an Access Token
While it’s not unique to this grant type and refresh tokens were covered at a conceptual level in part 1, given that we have a refresh token from our example, I’ll show how to use it here.
The request to refresh an access token is similar to the initial access token request in that it is an HTTP POST request to the authorization server’s token endpoint, and it must be an authenticated request if the client has authentication credentials. The parameters of the request are slightly different though:
- grant_type
- When refreshing an access token, this will always be
refresh_token
. - refresh_token
- The refresh token. The authorization server must check that it was issued to the client making the request.
- scope
- The scopes that the client would like the access token to have. None of the scopes can be ones that were not granted in the initial authorization request, but using this it is possible to obtain a less-privileged access token than the initial one.
The response will be the same as for an access token request.
Let’s use the refresh token from our example response above to request a new access token:
% curl -i \
--basic \
-u p1LJZfMDXrd2EEQp6oV77ASWuN9tlvsK:ewfPPimoGanRHixXXI5j3kPoRErUci-VJrGnIRTtOQusqwzzjgjEf_ZBqYqYUOIp \
--data-urlencode grant_type=refresh_token \
--data-urlencode refresh_token=fI1tqMHyaIvEOYX9zZ30OYZXc2sXuZZgPf7RqSnnJtXMD \
https://jammystuff.eu.auth0.com/oauth/token
HTTP/2 200
date: Sat, 12 Feb 2022 21:53:42 GMT
content-type: application/json
cf-ray: 6dc90d220fad75c9-LHR
cache-control: no-store
set-cookie: did=s%3Av0%3A3eb0e1c0-8c4e-11ec-97f1-2b39d50e4826.1PiOHXkahlaRpcpLHo%2FpT559kfGwMdXkcQFR0Al3lig; Max-Age=31557600; Path=/; Expires=Mon, 13 Feb 2023 03:53:42 GMT; HttpOnly; Secure; SameSite=None
strict-transport-security: max-age=31536000
vary: Accept-Encoding, Origin
cf-cache-status: DYNAMIC
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
ot-baggage-auth0-request-id: 6dc90d220fad75c9
ot-tracer-sampled: true
ot-tracer-spanid: 0ed1e4ce1f74f67e
ot-tracer-traceid: 465beb962e83f26f
pragma: no-cache
x-auth0-requestid: a2e34adf6ec2c9322e35
x-content-type-options: nosniff
x-ratelimit-limit: 30
x-ratelimit-remaining: 29
x-ratelimit-reset: 1644702823
set-cookie: did_compat=s%3Av0%3A3eb0e1c0-8c4e-11ec-97f1-2b39d50e4826.1PiOHXkahlaRpcpLHo%2FpT559kfGwMdXkcQFR0Al3lig; Max-Age=31557600; Path=/; Expires=Mon, 13 Feb 2023 03:53:42 GMT; HttpOnly; Secure
set-cookie: __cf_bm=FjQpYq6wXp6LeumKltkBRpqKntFRchg6c8pyzSbjf.Q-1644702822-0-AU1TuB+glFBNB/j2/Oa2dYJYYv/6FRAWFm0qSbbpU3vZXrxVefI4jquLMH3j3wwZcAgMZAeMZNGYfgR6+6U1Pik=; path=/; expires=Sat, 12-Feb-22 22:23:42 GMT; domain=.eu.auth0.com; HttpOnly; Secure; SameSite=None
server: cloudflare
alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlFrTkRNVGhEUkVFeE1VVkVSakEzTWtNeFFrVXdPREUxT0RrMU9UUTBSakEyUVVGR09FUXlSUSJ9.eyJpc3MiOiJodHRwczovL2phbW15c3R1ZmYuZXUuYXV0aDAuY29tLyIsInN1YiI6ImF1dGgwfDYxZjcwNzkyYzFkZDJlMDA2ZWE2ZDI5MiIsImF1ZCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tIiwiaWF0IjoxNjQ0NzAyODIyLCJleHAiOjE2NDQ3ODkyMjIsImF6cCI6InAxTEpaZk1EWHJkMkVFUXA2b1Y3N0FTV3VOOXRsdnNLIiwic2NvcGUiOiJyZWFkOnBob3RvcyB3cml0ZTpwaG90b3Mgb2ZmbGluZV9hY2Nlc3MifQ.O19YS8K7xuVFa9dLPdzPTcUx-57NSKoANh4HqBYlGwQgCeFm5Ws2TRhvH04wAKJLtvP5TKwqLzuzl8UN4XTk8xxuGZ8dVfmF_VawQOd3BPHOPMM_qKJgzR0N1kf1ZEW6X9VzCpIsJ2YGiLgrxfEZnLld8xmV-r8Cdk4W0kY9uzhi-BY_GibA4HbB2xEJrp7e8nLeJSiqpeP2e3QaW0rv6aRYL556sLBhrdqYtn_KRZ2sJ58hL5Gn2CWNrqPcqHRiGKP8caJ3pbznNdZ1PdKigRO7_5KBjapEESwO9tuW37jsoUZ83YQVRuG6s9Qd74sG3h-kLkcX9SzeCau9uECdTQ",
"scope": "read:photos write:photos offline_access",
"expires_in": 86400,
"token_type": "Bearer"
}
Summary
In this post, I’ve explained the authorization code grant type in detail. The authorization code grant type is the most popular grant type used where a client is making requests on behalf of a resource owner. While RFC 6749 included another grant type for single page application clients, improvements in web APIs and OAuth extensions to increase security for public clients6 mean that the authorization code grant is now recommended for almost all situations where the client is acting on behalf of a resource owner.
The next post will cover the grant type for single page applications, the implicit grant.
-
The parameters are encoded as described in RFC 6749 Appendix B. I’ll just refer to this as “Appendix B encoding”. ↩︎
-
If the redirect URI is how the response is provided, how can it optional? During registration, the client provides a list of possible redirect URIs. If there is only one URI in the list, the client doesn’t need to provide it during the authorization request (although there is no harm in providing it anyway). ↩︎
-
Not all authorization servers will require this, but Auth0 does. ↩︎
-
The
%
at the start is the prompt, and should not be included if you are following along. ↩︎ -
It’s a bad idea to show a request like this on the public Internet, as it includes your client credentials! I’m including it here so you can see the actual request, but I have destroyed the client credentials that were used. ↩︎
-
Proof Key for Code Exchange (PKCE) will be covered later. ↩︎