OAuth Explained Part 2: Authorization Code Grant

Posted on | 1930 words | ~10 mins
Computers OAuth OAuthExplained RFC6749

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:

Authorization Codeflow

  1. The client redirects the resource owner’s user agent (hereafter, we’ll just say “browser”) to the authorization server.
  2. The authorization server authenticates the user.
  3. The authorization server asks the resource owner for consent to give the client access to the protected resources.
  4. The authorization server redirects the resource owner’s browser to the client, including an authorization code in the URI.
  5. The client sends the authorization code to the authorization server using an HTTP POST request.
  6. The authorization server responds to the client’s request with an access token (and optionally a refresh token).
  7. The client sends a request to the resource server, including the access token.
  8. 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.

Log inscreen

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.

OAuth consentscreen

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.


  1. The parameters are encoded as described in RFC 6749 Appendix B. I’ll just refer to this as “Appendix B encoding”. ↩︎

  2. 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). ↩︎

  3. Not all authorization servers will require this, but Auth0 does. ↩︎

  4. The % at the start is the prompt, and should not be included if you are following along. ↩︎

  5. 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. ↩︎

  6. Proof Key for Code Exchange (PKCE) will be covered later. ↩︎