In this post, we will take a closer look at the Authorization code grant type of the OAuth 2.0 framework. We will have a look at how it works, when it should be used, and illustrate it with an example.
OAuth 2.0
OAuth 2.0 is a protocol for authorization.
More specifically it was designed to solve the problem of delegated authorization.
Delegated authorization means that the owner of some resource allows an application (client) to access that resource
on behalf of the owner.
An example of this is that you log in to a chat application and the chat application wants to suggest friends to you based
on your e-mail contacts.
For that, it needs access to your e-mail contact list at your e-mail service provider (e.g. Gmail).
Using OAuth the chat application can ask for permission to see your contact list and if you consent,
it can access your e-mail list on your behalf.
Meaning you delegated your authorization to the client chat application.
There are a couple of actors in OAuth:
- Resource owner
- This is the owner of the resource that the client wants to access
- The owner of the email contact list in the example
- This is the owner of the resource that the client wants to access
- Resource server
- This is the server that holds the protected resource that the client wants to access
- The email service provider in the example
- This is the server that holds the protected resource that the client wants to access
- Authorization server
- This is the server that checks the user, his permission and if permission is given issues access tokens
- The e-mail service provider authorization server in the example
- This is the server that checks the user, his permission and if permission is given issues access tokens
- Client
- This is the application that wants to access the resource
- The chat application in the example
- This is the application that wants to access the resource
Then there are also multiple grant types in OAuth. Which one to use depends on how the client application looks like. The different grant types are:
- Authorization code
- Is used when the client application has a server and frontend part, so there is both a back-channel and front-channel
- Client credentials
- Is used when there is no user involved, only the client server and the resource server, so just a back-channel
- PKCE
- Is used when the client is a mobile application. It is an extension of the authorization code that prevents CSRF and authorization code injects, originally developed for mobile applications, but is encouraged to be used also in place of authorization code for higher security
- Device Authorization
- Is used when the client is a device with no browser or limited input capability
- Refresh token
- Is used when the client wants to exchange a refresh token for an access token
Authorization code grant type
As mentioned above the authorization code grant type is used when there is a secure back-channel and an insecure front-channel. This is the usual client-server architecture, with a frontend application and a backend application.
The authorization code grant type has the following steps:
- The client starts the flow by redirecting the resource owner to the authorization endpoint on the authorization server
- The authorization server authenticates the resource owner and confirms that the resource owner grants the access request of the client
- Assuming the authentication is successful and access is granted, the authorization server redirects the resource owner to a callback endpoint on the client application also passing back an authorization code
- The client exchanges the authorization code for an access token at the token endpoint on the authorization server
- The authorization server authenticates the client and validates the authorization code and the redirect uri. If everything checks out, the authorization server responds with an access token that can also optionally contain a refresh token
- The client uses the access token to access the resource on the resource server
Now let us take a closer look at all the steps.
1. The client redirects the resource owner
In the first step of the authorization code flow, the client application redirects the resource owner to the authorization endpoint on the authorization server. The redirect URL points to the authorization endpoint and contains the following URL-encoded parameters:
- response_type
- This parameter is required and has to be set to “code”
- client_id
- This parameter is required and identifies the client to the authorization server
- redirect_uri
- This parameter is optional and tells the authorization server where to redirect the user after successful authentication
- scope
- This parameter is optional and tells the authorization server for which scopes we are requesting the access token. It is optional because there might be default defined scopes, and in that case, we don’t need to explicitly pass the scope
- state
- This parameter is optional but recommended and it is used to maintain the state between the original redirect and the callback. The authorization server takes this parameter and passes it back as a parameter in the callback request
An important thing is that all the parameters should be URL encoded.
2. The authorization server authenticates the resource owner
When the resource owner is redirected to the authorization server, the authorization server validates if the request is valid.
If the request is valid, it authenticates the user - usually, this means the user has to log in if he is not already logged in at the
authorization server.
After successful authentication of the user, the authorization server obtains consent from the user about the client application’s access request.
This happens either automatically or there is a consent form shown to the resource owner saying that the application wants to
access certain things and the resource owner has to explicitly allow this.
3. The authorization server redirects the user
After successfully obtaining consent, the authorization server redirects the resource owner in one of two ways:
- If there was a redirect_uri in the initial redirect, the authorization redirects to that location
- If there is no redirect_uri in the initial redirect, the authorization redirects to a location that is configured in the authorization server itself
On top of that it adds two parameters to the redirect:
- code
- This parameter contains the authorization code issued by the authorization server that should later be exchanged for the access token
- state
- This parameter is added if the initial redirect contained a state parameter and the value is the same as in the initial request. So this parameter is taken from the initial request and just passed back in the callback request
4. The client exchanges the code for the access token
On the callback, the client receives an authorization code that can be exchanged for an access token.
So far everything was happening on the front channel (in the browser of the resource owner), but the exchange request
should happen on the secure back-channel (it should be a server to server call).
The server has to make a POST request to the token endpoint and the request should contain the following parameters in the
application/x-www-form-urlencoded format:
- grant_type
- This parameter is required and the value should be “authorization_code”
- code
- This parameter is required and should contain the code received in the callback from the authorization server
- redirect_uri
- This parameter is required if there was a redirect_uri parameter in the first redirect and their values must match
- client_id
- This parameter is required if the client is not authenticating using other means
On top of that, the client might be required to authenticate. So far I have seen two ways of authentication:
- Basic authentication
- We add an Authorization header with the value “Basic {credentials}”
- {credentials} is base64 encoded string “clientId:clientSecret”
- We add an Authorization header with the value “Basic {credentials}”
- Add the client id and client secret as parameters in the body
- In this case, we need to add the parameters client_id and client_secret with their respective values
5. The authorization server issues the access token
The authorization server must validate the exchange request and if the request is valid, it returns the access token in the response.
The parameters in the response are:
- access_token
- This parameter is required and contains the access token issued by the authorization server and that we attach later on to the requests to protected resources
- token_type
- This parameter is required and tells us what kind of token the access token is. The most common type is a bearer token but there are also other token types
- expires_in
- This parameter is recommended and contains the lifetime of the access token in seconds
- refresh_token
- This parameter is optional and contains the refresh token, which can be used to obtain new access tokens using the refresh token request
- scope
- This parameter is optional if the scope is equal to the scope requested by the client, otherwise required
The response parameters are passed back in the request body in the JSON format.
6. The client uses the access token to request the resource from the resource server
Once we receive the access token, we can use it to request resources from the resource server. The most common access token is the Bearer access token which is used by adding an Authorization header with the value “Bearer {access token}”.
7. The client uses the refresh token to get a new access token
If the client was also issued a refresh token, it can use it to get a fresh access token. To get a new access token we need to issue a POST request to the refresh endpoint with the following parameters in the application/x-www-form-urlencoded format:
- grant_type
- This parameter is required and its value must be “refresh_token”
- refresh_token
- This parameter is required and its value is the refresh token issued by the authorization request
- scope
- This parameter is optional. We can leave it out if we want to request the same scope as the original token request. We can request fewer scopes, but we must not request more scopes than in the original request
On top of that, the client should again authenticate using one of the two methods described in the token exchange request.
Example
Now that we went over the theory, let us have a look at a live example.
The scenario I imagined is that we have an application that wants to show the users’ favourite players in the NBA. This list of favourite players is supplied by another server from a protected endpoint. So our application needs to be allowed to access that endpoint, which is what we use the authorization code flow for.
The example setup has three parts:
- A client application
- React frontend app served from an Express JS backend. The application can:
- display the current access token
- get the list of favourite players from the Spring server
- initiate the OAuth flow by redirecting
- exchange the authorization code for the access token
- React frontend app served from an Express JS backend. The application can:
- A resource server
- Java Spring app that has a protected endpoint serving the list of favourite players. The endpoint is protected by the Keycloak authorization server
- An authorization server
- Keycloak instance that issues the access tokens for the client and which is used to verify the tokens on the resource server
Now let us have a look at some specifics from each of the parts.
Resource server
The resource server in our example is a Spring boot backend server that is protected using OAuth.
The server has only one endpoint that is serving the list of favourite players.
For simplicity, it is returning just a static predefined list of players.
The controller is a standard rest controller serving GET requests to /favouritePlayers, nothing fancy there.
package com.devflection.resource.server;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PlayersController {
@GetMapping("/favouritePlayers")
@ResponseStatus(HttpStatus.OK)
@CrossOrigin(origins = "*")
List<String> healthcheck() {
return List.of(
"Michael Jordan",
"LeBron James",
"Kobe Bryant",
"Luka Dončič",
"Kyrie Irving",
"Giannis Antetokounmpo",
"Stephen Curry");
}
}
A bit more interesting is how we secure the endpoint. There is a SecurityConfig, where we say that we want all requests to our endpoint to be authenticated and to have the scope devflectionPlayers. And the final line says that we want to use JWTs.
package com.devflection.resource.server;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests(
authorizationRequest -> authorizationRequest.antMatchers(HttpMethod.GET, "/favouritePlayers")
.hasAuthority("SCOPE_devflectionPlayers")
.anyRequest()
.authenticated()
).oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
}
The final piece of the puzzle is to add a dependency to spring oauth2 resource server starter in the pom:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
And we need to add the needed properties in the application.properties file so that the tokens can be verified. The two properties that are needed are the issuer-uri, which is checked against the iss field in the tokens, and the jwk-set-uri, which is the uri to the JWKS endpoint of the authorization server, where Spring can get the tokens and validate them.
server.port=${PORT}
spring.security.oauth2.resourceserver.jwt.issuer-uri=${OAUTH_ISSUER_URI}
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${OAUTH_JWK_URI}
logging.level.org.springframework.security=DEBUG
Usually, it is enough to only set the issuer-uri property, and Spring can get to the JWKS uri from that. But since we are running our example Keycloak from a docker-compose file, the issuer and docker container names are different and the issuer check was failing. So we need to point Spring directly to the JWKS where it can get the keys.
Authorization server
For the authorization server, I used a Keycloak instance, where I had to configure a couple of things.
- Add our custom scope
- I added a devflectionPlayers scope
- Register a client
- I registered a client named devflecton-node-js, made the client confidential, allowed all redirect uris, and added our custom scope to the list of client scopes
- Add a test user
- I created a new test user with the username test and password password
Client application
For the client application, we have a React frontend served from an Express backend.
The backend does a couple of things:
-
It serves the static frontend app files
const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); app.use(express.static(path.join(__dirname, "..", "build")));
-
It makes the request to the Spring server to get the list of players
app.get("/myFavouritePlayersInTheNBA", async (req, res) => { await fetch(process.env.RESOURCE_SERVER_PLAYERS_ENDPOINT, { method: 'GET', headers: { 'Authorization': 'Bearer ' + accessToken } }).then(response => { if (response.ok) { return response.json(); } throw response; }).then(data => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(data)); }).catch(err => res.sendStatus(err.status)); });
-
It starts the OAuth authorization code flow by redirecting the user
app.get("/startOAuthFlow", (req, res) => { const authorizeRedirectUrl = prepareAuthorizeRedirectUrl() res.redirect(authorizeRedirectUrl); }); function prepareAuthorizeRedirectUrl() { const redirectUri = encodeURIComponent(process.env.REDIRECT_URI); return `${process.env.AUTHORIZE_REQUEST_URL}?response_type=code& scope=${process.env.SCOPE}& state=xyz& client_id=${process.env.CLIENT_ID} &redirect_uri=${redirectUri}`; }
-
It receives the callback from the authorization server and exchanges the code for the token
app.get("/authorizationCallback", async (req, res) => { await fetch(process.env.TOKEN_ENDPOINT_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: new URLSearchParams({ 'grant_type': 'authorization_code', 'code': req.query.code, 'client_id': process.env.CLIENT_ID, 'client_secret': process.env.CLIENT_SECRET, 'redirect_uri': process.env.REDIRECT_URI }) }).then(response => { if (response.ok) { return response.json(); } throw response; }).then(data => { accessToken = data.access_token; res.redirect("/index.html"); }).catch(err => res.send(err)); });
-
It exposes the currently saved access token
app.get("/getAccessToken", (req, res) => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(accessToken)); });
And on the react side we have a couple of components:
-
One to display the currently saved access token
import React, { useState, useEffect } from 'react'; import './Token.css'; const GET_TOKEN_URL = "/getAccessToken"; function Token() { const [token, setToken] = useState(null); useEffect(() => { fetch(GET_TOKEN_URL) .then(response => { if (response.ok) { return response.json(); } throw response; }).then(data => setToken(data)) .catch(err => console.log(err)); }); return ( <span className="token-container"> <p className="token-message"> <strong>Current access token:</strong> </p> <p className="token-message"> {getTokenMessage()} </p> </span> ); function getTokenMessage() { if (token) { return token; } else { return "There is currently no access token"; } } } export default Token;
-
One component to get the players list from the express backend
import React, { useState, useCallback } from 'react'; import './Players.css'; const GET_FAVOURITE_PLAYERS_URL = "/myFavouritePlayersInTheNBA"; function Players() { const [favouritePlayers, setFavouritePlayers] = useState([]); const [error, setError] = useState(''); const fetchData = useCallback( () => { fetch(GET_FAVOURITE_PLAYERS_URL) .then(response => { if (response.ok) { return response.json(); } throw response; }).then(data => setFavouritePlayers(data)) .catch(err => { if (err.status === 401) { setError("The request was rejected as unathorized!"); } else { setError("There was a problem with the request!"); } }); }); return ( <div className='players-container'> <span className='players-message'> <button className="btn" onClick={fetchData}>Get favourite players!</button> </span> <span className='players-message'> {getPlayers()} </span> </div> ); function getPlayers() { if (error) { return <p>{error}</p> } if (favouritePlayers.length > 0) { let playerList = favouritePlayers.map(player => { return <li>{player}</li> }); return <ul>{playerList}</ul>; } else { return <p>No favourite players!</p>; } } } export default Players;
-
And one component to trigger the authorization code flow
import './Auth.css'; const START_OAUTH = "/startOAuthFlow"; function Auth() { function redirect() { window.location.href = START_OAUTH } return ( <div className='auth-container'> <button className="btn" onClick={redirect}> Get the access token! </button> </div> ); } export default Auth;
Putting it all together
For the example, I dockerized the client application and the resource server and then prepared a docker-compose file that also includes a Keycloak instance. And for everything to be able to connect I added a couple of environment variables to each docker file so everything points to the proper address.
version: '3.7'
services:
frontend:
container_name: frontend
build: ./frontend
environment:
- CLIENT_ID=devflection-node-js
- CLIENT_SECRET=MDLMGUtpSKdZ8C9ctccKJVPl6nZBu20D
- SCOPE=devflectionPlayers
- TOKEN_ENDPOINT_URL=http://keycloak:8080/auth/realms/master/protocol/openid-connect/token
- AUTHORIZE_REQUEST_URL=http://localhost:8083/auth/realms/master/protocol/openid-connect/auth
- REDIRECT_URI=http://localhost:5000/authorizationCallback
- RESOURCE_SERVER_PLAYERS_ENDPOINT=http://resource-server:8080/favouritePlayers
ports:
- 5000:5000
resource-server:
container_name: resource-server
build: ./resource-server
environment:
- OAUTH_ISSUER_URI=http://localhost:8083/auth/realms/master
- OAUTH_JWK_URI=http://keycloak:8080/auth/realms/master/protocol/openid-connect/certs
- PORT=8080
ports:
- 8080:8080
keycloak:
image: quay.io/keycloak/keycloak:legacy
volumes:
- ./keycloak/data:/opt/jboss/keycloak/standalone/data/
ports:
- 8083:8080
The services are accessible:
- Frontend
- locally in the browser at localhost:5000
- in other containers at frontend:5000
- Resource server
- locally in the browser at localhost:8080
- in other containers at resource-server:8080
- Keycloak
- locally in the browser at localhost:8083
- in other containers at keylcloak:8080
Once we pass those endpoints to the applications we can then execute the full flow where the user is redirected to the authorization server, logs in, consents and is redirected back to the app, where we exchange the code for the access token and can query the Spring backend for the list of players.
The environment variables that we pass to the React/Express application are:
- CLIENT_ID
- This is the client id for our application that we get from Keycloak
- CLIENT_SECRET
- This is the client secret for our application that we get from Keycloak
- SCOPE
- This is the scope that we are requesting for our access token
- TOKEN_ENDPOINT_URL
- This is the endpoint on Keycloak where the token exchange request should go
- AUTHORIZE_REQUEST_URL
- This is the endpoint to where we must redirect the user at the start of the authorization code flow
- REDIRECT_URI
- This is the callback URL to where the authorization server redirects the user after successfully logging in
- RESOURCE_SERVER_PLAYERS_ENDPOINT
- This is the protected Spring backend endpoint
The environment variables that we pass to the Spring application are:
- OAUTH_ISSUER_URI
- This is used by Spring to validate the iss field of the JWT access token
- OAUTH_JWK_URI
- This is JWKS URL and it is used by Spring to get the keys to validate the access token
- PORT
- This is the port on which the Spring application should run and should match the container port
The final piece for the example is a Keycloak instance.
There is a Keycloak service configured in the docker-compose file which is mounting a volume.
The volume is a Keycloak database where everything is already configured (client, user, scope).
So you can just run
docker-compose up -d
And then in your browser, open http://localhost:5000 and you should be able to click through the application.
You can browse and get the full example repository on GitHub.
Conclusion
In this post, we were looking at one of the multiple grant types of OAuth, the authorization code grant type. This grant type is used when there is a client application with a frontend part and a backend part. We went over the flow and showed an example application.
Overall, OAuth 2.0 is a nice way of solving the delegated authorization problem. However, it might be a bit overwhelming at first, especially since there are also a lot of misleading usages of OAuth online. Often OAuth is used for authentication, but that is not what it was meant for. OAuth was designed to solve the problem of delegated authorization, where users to their resources on some resource server to some other client applications.
To solve the authentication problem OpenID Connect was designed as an extra layer on top of OAuth and that can be used for authentication. But more on OIDC in a future post :)
This is it for the Authorization code grant type of the OAuth 2.0 framework.
Thank you for reading through, I hope you found this article useful.