Authentication and Authorisation using OIDC and OAuth 2. Part 2: A Public Client

Lydtech
Authentication and Authorisation using OIDC and OAuth 2. Part 2: A Public Client

Part 2: A public client - REST API and SPA app

Introduction

This is the second article in this series. Part 1 was a primer to the world of Authentication and Authorisation and introduced the important concepts that need to be understood before exploring in more detail. This article puts these concepts into action and shows how you can apply these to achieve Authentication and Authorisation in a sample application.

The article concentrates on a ‘public’ OAuth client. This is where the client itself has no way of reliably storing OAuth related secrets such as the client_secret field. Think of a Single Page Application (SPA) written in React or similar - there is no secure way to store a sensitive field here as the Javascript / TypeScript code is always available to the user in the browser. The diagram below illustrates the parts of this architecture:

This article will work through the following

  • Choose an Authorisation Server
  • Configure the Authorisation Server with users, roles and an OAuth client
  • Create a Resource Server to expose a REST API for the client to consume
  • Create a Single Page Application (SPA) as the OAuth client
  • Show how all the moving parts are wired together to provide authentication and authorisation via OIDC / OAuth2

Part 3 of this series looks at a ‘confidential’ OAuth client, in the form of a traditional server side web application.

Choosing an Authorisation Server

Previously, Spring offered support for all things OAuth, including support for creating Authorisation Servers and Resource Servers, via the spring-security-oauth library. This has now been deprecated, with OAuth support moving into the Spring Security Library itself. Note that Spring Security does not include support for creating custom Authorisation Servers due to the existence of a large number of specialised OAuth/OIDC compliant Authorisation Server products. Although, recently, Spring has introduced a new Spring Authorisation Server project to support the creation of custom Authorisation Servers again for the use cases that require that. This is explored in Part 4 of this series.

The Authorisation Server component plays a key role in the security of any architecture leveraging OIDC / OAuth. It will typically be responsible for storing user details and credentials, roles, groups and other information and be responsible for authenticating users. It is a crucial piece and it is imperative that this is done correctly. With this in mind, the industry has generally moved towards using specialist identity products such as Auth0, Keycloak, AWS Cognito, Firebase Authentication as Authorisation Servers rather than using tools such as Spring to create custom implementations.

For this series, Keycloak has been used for the Authorisation Server. The main reasons for this are:

  1. It is a well known, mature product
  2. Has very good documentation and support community
  3. Is very easy to run locally using Docker

Setting up Keycloak

Note the steps below are for running an Authorisation Server locally in the interest of demonstrating the concepts in this series of articles. In order to run in production, consult the Keycloak documentation.

Start Keycloak

Use the following command to run Keycloak locally using Docker

docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:21.1.1 start-dev

Create Realm

The Keycloak instance will have a default realm of ‘master’. The first step is to create a realm. A realm is a space where you manage objects, including users, applications, roles, and groups. A user belongs to and logs into a specific realm.

Once logged in to the admin console at http://localhost:8080/admin as the admin user (password: admin), you will see a drop down box in the top left corner to select a realm. The master realm will currently be selected. Drop the box down and click ‘Create Realm’. Choose a Realm name that is relevant to your situation. We’ll use ‘lydtech’ - our organisation name.

Once the new realm is selected there are many configuration options that can be changed. To help simplify this demo, navigate to ‘Realm Settings->Login->Email Settings’ and ensure ‘Email as username’ is enabled. Your settings should look as follows

Create Roles

Now that we have a Realm, we need to create 2 roles. We will create an ‘admin’ and ‘user’ role in order to demonstrate authorisation in action.

Navigate to ‘Realm Roles’ and click ‘Create role’. Enter the name ‘admin’ and an optional description.

Click Save. Your new role will be created and will now be visible on screen.

Repeat this process to add a ‘user’ role.

Create Users

Next, let's create 2 users with the relevant roles. Navigate to ‘Users’ and click ‘Create new user’. Enter the email ‘admin@example.com’, ensure ‘Email Verified’ is enabled (to avoid the user needing to verify their email) and choose an appropriate First / Last name as shown below

The user needs a password. We will set this in the admin console. With the new user selected, visit the ‘Credentials’ tab, click ‘Set Password’

Choose a simple password, ensure ‘Temporary’ is disabled and click ‘Save’

Finally, we need to map the user to the admin role we created previously. This can be done on the ‘Role Mapping’ tab when the user is selected. Choose to ‘Assign Role’ and select the ‘admin’ role. When complete, the admin role should be visible in the table as seen below

Repeat this exercise to create another user ‘user@example.com’ , choosing the ‘user’ role this time during role mapping

Create OAuth Client

Now that we have our realm, users and roles created, it is time to create our OAuth client. A Keycloak client represents an OAuth client application and is able to request Keycloak to authenticate a user. Typically a separate client would be created for each application that will authenticate / authorise users in your realm. For this article we will create a ‘public’ type client with the name ‘lydtech-public-client’

To create a client, navigate to ‘Clients’ and click ‘Create Client’. You will have 3 pages to complete.

Under ‘General Settings’, Client Type needs to be ‘OpenID Connect’. Choose an appropriate clientID - ‘lydtech-public-client’ in this case

Next under ‘Capability config’, ensure the following:

  1. ‘Client authentication’ should be disabled. This is a ‘public’ client type, not ‘confidential’
  2. For authentication flows, ensure only ‘Standard flow’ is selected - this is the ‘Authorisation Code’ flow that we focused on in Part 1. ‘Direct access grants’ will be enabled by default, but we do not want this; this is effectively a ‘Password’ grant type which is discouraged, and deprecated in OAuth 2.1.

Finally, under ‘Login Settings’ we need to provide the same URL (http://localhost:3000) for both a ‘Valid redirect URI’ and a ‘Web Origin’. The former will allow our SPA to ask Keycloak to redirect back to it after a successful login while the latter will allow the SPA to make HTTP calls to Keycloak from a different origin (think CORS). We use the same value for both - our SPA client app will run locally on port 3000.

As we’ve disabled the Direct Access Grant (Password Grant) there isn’t an easy way to generate a JWT token for this client as a test of what we just created. It is worth visiting the following URL at this stage to verify we’ve configured the basics correctly: http://localhost:8080/realms/lydtech/.well-known/openid-configuration - the JSON response will contain configuration information that can be used for discovery purposes.

Creating a Spring Boot Resource Server

Create the project

Now that we have a fully configured Authorisation Server, we can create our Resource Server. For this we will use Spring Boot to expose a simple REST API with 3 endpoints. We will use Spring Security to protect the 3 endpoints.

Visit start.spring.io to create the template for the Resource Server. Use settings such as those in the image below but be sure to include the following dependencies:

  • Spring Web - Needed to provide the REST API
  • Lombok - Helps to reduce boilerplate code
  • OAuth2 Resource Server - pulls in the required dependencies to configure the service as an OAuth Resource Server

Click Generate to download a zip file containing the project, unzip this project in a suitable directory and open it in your favourite IDE.

Create REST endpoints

First let's create our single Controller class which will expose 3 endpoints as follows:

  • GET /v1/api/public - any user will be able to call this, even if they are unauthenticated (anonymous)
  • GET /v1/api/user - any admin or user role will be able to call this
  • GET /v1/api/admin - only admin roles will be able to call this

Complete controllers/MyController.java file available in the Github repository

package dev.lydtech.security.simpleresourceserver.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;
import java.util.Optional;

@RestController
@RequestMapping("/api/v1")
@Slf4j
@CrossOrigin(origins = "http://localhost:3000")
public class MyController {
    @GetMapping("/public")
    public ResponseEntity<String> publicEndpoint(Principal principal) {
        log.info("/public called");

        String message = getPrincipalName(principal);
        return ResponseEntity.ok("Hello from Public endpoint - " + message);
    }

    @GetMapping("/user")
    public ResponseEntity<String> userEndpoint(Principal principal) {
        log.info("/user called");

        String message = getPrincipalName(principal);
        return ResponseEntity.ok("Hello from User endpoint - " + message);
    }

    @GetMapping("/admin")
    public ResponseEntity<String> adminEndpoint(Principal principal) {
        log.info("/admin called");

        String message = getPrincipalName(principal);
        return ResponseEntity.ok("Hello from Admin endpoint - " + message);
    }

    private static String getPrincipalName(Principal principal) {
        return Optional.ofNullable(principal)
                .map(Principal::getName)
                .orElse("Not available");
    }
}

There isn’t a great deal to explain here for those familiar with Spring, but a few key points:

  1. The @RestController and @RequestMapping("/api/v1") annotations are used to expose a REST API.
  2. @GetMapping is used for each of the 3 endpoints. The 3 endpoints have a single method parameter of type java.security.Principal - this will contain details of the authenticated user.
  3. @CrossOrigin is required to allow the SPA client to call the endpoint from a different host/port origin.
  4. Each endpoint will return a simple string containing a message and the authenticated username (if present).

Extract role details from Keycloak JWT

The following class is needed to customise some of the information that is extracted from the JWT:

Complete JwtAuthConverter.java file available in the Github repository

package dev.lydtech.security.simpleresourceserver;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Component
public class JwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {
    private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();

    @Override
    public AbstractAuthenticationToken convert(Jwt jwt) {
        Collection<GrantedAuthority> authorities = Stream.concat(
                jwtGrantedAuthoritiesConverter.convert(jwt).stream(),
                extractRealmRoles(jwt).stream()).collect(Collectors.toSet());
        return new JwtAuthenticationToken(jwt, authorities, jwt.getClaim("preferred_username"));
    }

    private Collection<? extends GrantedAuthority> extractRealmRoles(Jwt jwt) {
        Map<String, Object> realmAccess = jwt.getClaim("realm_access");
        Collection<String> realmRoles;
        if (realmAccess == null
                || (realmRoles = (Collection<String>) realmAccess.get("roles")) == null) {
            return Set.of();
        }
        return realmRoles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toSet());
    }
}

This code perform 2 functions:

  1. Use the preferred_username claim from the JWT for the principal name. Without this the principal name would be the ‘sub’ claim which is a UUID
  2. Extract the Keycloak ‘Realm Roles’ from the JWT token and map to a collection of GrantedAuthorities stored against the principal. The last section of this article shows some examples of JWT access_tokens issued by Keycloak and calls out where role information is contained.

Configure Security

We need to include a Spring configuration class in order to configure the Security Filter Chain that will secure the 3 endpoints. Some points to note:

  1. .requestMatchers is used to allow the relevant roles for the correct URL patterns.
  2. The JWTAuthConverter bean we created above is registered with Spring Security
  3. HTTP sessions are disabled (this is a stateless REST API that does not require sessions)
  4. A CORS filter is added to allow the API to be called from a different origin (the SPA app)

Complete WebSecurityConfig.java file available in the Github repository

package dev.lydtech.security.simpleresourceserver;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
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.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    private final JwtAuthConverter jwtAuthConverter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        //Don't have to be authenticated to call anything under public
                        .requestMatchers(HttpMethod.GET, "/api/v1/public", "/api/v1/public/**").permitAll()
                        //Only admins can access anything under admin
                        .requestMatchers(HttpMethod.GET, "/api/v1/admin", "/api/v1/admin/**").hasRole("admin")
                        //Users and admins can access user endpoints
                        .requestMatchers(HttpMethod.GET, "/api/v1/user").hasAnyRole("admin", "user")
                        .anyRequest().authenticated()
                ).oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(jwt -> jwt
                                .jwtAuthenticationConverter(jwtAuthConverter)
                        )
                ).sessionManagement(sm -> sm
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                ).cors(withDefaults());
        return http.build();
    }
}

Configure Spring Security

The final step to complete the simple Resource Server is to replace the contents of src/main/resources/application.properties with application.properties as per the extract below

server.port=8081

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/lydtech
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs

logging.level.org.springframework.security=trace

Some points to note:

  1. The server port is set to 8081, the default of 8080 will clash with our Keycloak instance.
  2. The spring.security.oauth2.resourceserver.jwt values are how we tell Spring Security how to validate the JWTs for this service. We are pointing it at the Keycloak realm we created and allowing it to get the set of public keys that Keycloak can use to sign the JWTs it creates.
  3. The org.spring.security log level is set to trace which can be really helpful in showing exactly what is happening in the security filters.

Testing the resource server

As a quick test of our resource server, start it up either by using the command

./mvnw spring-boot:run

or running the SimpleResourceServerApplication in your IDE.

Once the service has started, prove that it is running and that the public endpoint does not require an access token by hitting with curl:

curl -v http://localhost:8081/api/v1/public

Important points in the output:

  • < HTTP/1.1 200 - a HTTP 200 response
  • Hello from Public endpoint - Not available - shows that the response was returned but there is no user associated

Finally lets prove that the other endpoints do require an auth token

curl -v http://localhost:8081/api/v1/user

Note the < HTTP/1.1 401 response which shows that it requires an authenticated user to call it.

We’ll see the secured endpoints in action shortly

Creating an SPA client

The final piece in our Auth Jigsaw is the client itself. For this we’ll produce a very simple React Single Page Application (SPA) that can perform the Authorisation Grant flow dance and call our Resource Server with the generated Access Token.

Create a React app

Use create-react-app to create the project template

npx create-react-app simple-client-example --template typescript

Install libraries

From the newly created project root directory, run the following to install some required libraries

npm install axios react-oauth2-code-pkce
  • react-oauth2-code-pkce - a handy library that supplies the code required to perform the OAuth Authorisation Code flow dance as a client
  • axios - required to make HTTP calls to the REST API we created earlier

Code the app

Remove src/App.tsx, src/App.test.tsx and overwrite src/index.tsx with the following. Complete src/index.tsx file available in the Github repository but included here in full with descriptions for each section.

Import the required libraries

import React, {useContext, useState} from 'react';
import ReactDOM from 'react-dom/client';
import axios from 'axios';
import {AuthContext, AuthProvider, IAuthContext, TAuthConfig, TRefreshTokenExpiredEvent} from "react-oauth2-code-pkce"

Configure the Auth config which includes

  • The clientId to the Keycloak client we created earlier.
  • The 2 OAuth endpoints that we introduced in Part 1: /auth and /token
  • The logout URL of the Keycloak server and realm
  • The redirectUri to return to following successful authentication
const authConfig: TAuthConfig = {
    clientId: 'lydtech-public-client',
    authorizationEndpoint: 'http://localhost:8080/realms/lydtech/protocol/openid-connect/auth',
    tokenEndpoint: 'http://localhost:8080/realms/lydtech/protocol/openid-connect/token',
    logoutEndpoint: 'http://localhost:8080/realms/lydtech/protocol/openid-connect/logout',
    redirectUri: 'http://localhost:3000',
    scope: 'openid',
    onRefreshTokenExpire: (event: TRefreshTokenExpiredEvent) => window.confirm('Session expired. Refresh page to continue using the site?') && event.login(),
}

A function to make calls to the REST Endpoints we configured earlier, using Axios

const callEndpoint = async (url: string, token: string) => {
    const currentdate = new Date();
    const datetime = currentdate.getDate() + "/"
        + (currentdate.getMonth()+1)  + "/"
        + currentdate.getFullYear() + " @ "
        + currentdate.getHours() + ":"
        + currentdate.getMinutes() + ":"
        + currentdate.getSeconds();

    var message;

    try {
        const response = await axios.get(url, {headers: {'Authorization': `Bearer ${token}`}});
        message = response.data;
    } catch (error) {
        if (axios.isAxiosError(error)) {
            console.log(`error calling endpoint ${url}: ${error}`)
            if (error.response) {
                message = `Error: ${error.response.status}`
            } else {
                message = error;
            }
        } else {
            throw error;
        }
    }
    return `called at ${datetime} - ${message}`;
}

Now we declare a React component that

  • Prints the full JWT access token to the screen
  • Prints the decoded JWT access token to the screen
  • Has a button to call each of the Resource Server endpoints
  • Has a logout button
const UserInfo = (): JSX.Element => {
    const {token, tokenData, logOut} = useContext(AuthContext)
    const [publicResponse, setPublicResponse] = useState("Not called yet");
    const [adminResponse, setAdminResponse] = useState("Not called yet");
    const [userResponse, setUserResponse] = useState("Not called yet");

    const callPublic = async () => {
        const message = await callEndpoint('http://localhost:8081/api/v1/public', token);
        setPublicResponse(message);
    }

    const callAdmin = async () => {
        const message = await callEndpoint('http://localhost:8081/api/v1/admin', token);
        setAdminResponse(message);
    }

    const callUser = async () => {
        const message = await callEndpoint('http://localhost:8081/api/v1/user', token);
        setUserResponse(message);
    }

    return <>
        <h4>Your complete Access Token</h4>
        <pre>{token}</pre>

        <h4>Your decoded JWT payload </h4>
        <pre>{JSON.stringify(tokenData, null, 2)}</pre>

        <br/>
        <button onClick={callPublic}>Call /api/v1/public</button>
        <p>{publicResponse}</p>

        <br/>
        <button onClick={callUser}>Call /api/v1/user</button>
        <p>{userResponse}</p>

        <br/>
        <button onClick={callAdmin}>Call /api/v1/admin</button>
        <p>{adminResponse}</p>

        <br/>
        <button onClick={() => logOut()}>Log out</button>

    </>
}

The React boilerplate code to bootstrap the application. Note that the UserInfo component is wrapped inside an AuthProvider, which is supplied by the react-oauth2-code-pkce library - this is what will take care of redirecting the user to Keycloak for authentication and then converting the authorisation code to an access token.

const root = ReactDOM.createRoot(
    document.getElementById('root') as HTMLElement
);
root.render(
    <AuthProvider authConfig={authConfig}>
        <UserInfo/>
    </AuthProvider>
);

Test the client runs

Run the client app using npm start (note that npm install will be required if you haven't followed the steps above in this guide) and make sure it starts successfully and that there are no errors.

Testing the end result

Start the applications

Ensure the following are started:

  • Keycloak in Docker
  • The Resource Server REST API via Maven or in your IDE
  • The client application using npm

Test an Admin user

Visit http://localhost:3000 in a browser. The application should immediately redirect you to the Keycloak login page as per the screenshot below

Sign in using the admin@example.com user that we created earlier. Upon successful login you should be shown the main app page similar to the image below.

Observe the following:

  1. The complete access token at the top
  2. The decoded JWT payload showing all the claims
  3. Note the ‘admin’ entry in realm_access.roles - this shows that the user has the admin role that we created earlier
  4. The user’s username (email address in this case) is available in the preferred_username claim

Scrolling down will show the 3 buttons to call each of the API endpoints

The ‘Not called yet’ placeholder shows the endpoints have not been called yet. Click each of the 3 buttons and see that each of the successful responses is shown on screen, also note that each response contains the user’s username - showing that the Principal was available in the Spring Boot app

Test a non Admin user

Use the ‘Log out’ button to log out as the admin user. You should be redirected to the Keycloak login screen again. This time log in as the user@example.com user we created.

Once logged in you will see from the JWT contents that this user only has the ‘user’ role, not the ‘admin’ role

Now click each of the 3 buttons to attempt to call the 3 endpoints. This time you should see something like the following.

Notice that /public and /user were successful but /admin resulted in a 403 proving that this user is not in the ‘admin’ role and therefore is not allowed to access the endpoint.

Following the Authorisation Code flow

Now that we’ve seen it work, let’s look at each step in the flow to be able to link it to the theory in Part 1. Using Chrome DevTools we can capture each of the interesting HTTP requests that make up the flow. The sequence diagram below shows each call, labelled with a number, which is expanded on below:



  • Authorisation Request

Request

Upon loading the application into the browser (from http://localhost:3000), the react-oauth2-code-pkce realises that the user is not logged in and therefore has no access_token so redirects to the Keycloak login screen. Note, as you will see from step 1 in the sequence diagram above, this isn't via a HTTP redirect but rather by asking the browser to take the user to the Keycloak login screen via Javascript code.

Method http://localhost:8080/realms/lydtech/protocol/openid-connect/auth
URL GET
Query Param Name Value Description
response_type code Indicates that we want to user the Authorisation Code Grant Type
client_id lydtech-public-client The ID we used for the client that we created in Keycloak
scope openid Openid is a default scope that is always available when using OIDC. It will give us details about the user and an ID token. Additional scopes could be included here and the user could be asked to authorise them as part of login, depending on Keycloak configuration
redirect_uri http://localhost:3000/ Ask Keycloak to redirect back to our SPA after a successful login
code_challenge vt-QeJzGaDrfCnKklXRtmuF85ZYBVhUuJ1zWSoi15Z4 1 of the 2 PKCE values that the client has created. This is the generated hash of the code_verifier that is passed across later. The key point here is that due to the hashing algorithm used, this value could only have been created by one specific code_verifier value and the code_verifier can never be calculated back from this code_challenge.
code_challenge_method S256 Indicates that SHA256 was used for the PKCE hashing algorithm

Response

The response from Keycloak is a HTML page containing a login page with a username & password field and a ‘Log in’ button

HTTP Response Code 200 OK
Response Body HTML containing a Login screen

  • User authenticates with Keycloak

Request

The user types in their username & password then clicks Log In

Method POST
URL http://localhost:8080/realms/lydtech/login-actions/authenticate
Query Param Value Description
session_code IqKHp-fht0QzzuB9c_pOljSm4LjdpnkR1ZUz_KFpB_E Keycloak specific field. Not part of the OIDC/OAuth specs
execution 4e660877-3b05-4f02-9271-46c467547f11 Keycloak specific field. Not part of the OIDC/OAuth specs
client_id lydtech-public-client ID of the client we created in Keycloak
tab_id VWATrPwozZc Keycloak specific field. Not part of the OIDC/OAuth specs
Body Key Body Value Description
username asmin@example.com Admin user username we created in Keycloak
password secret123 Admin user password we created in Keycloak

Response

Upon a successful log in, Keycloak redirects back to our SPA app as requested

HTTP Response code 302 (redirect)
Location header http://localhost:3000/?session_state=4754a503-d139-453f-8db3-804199c89632&code=6857770a-06db-4131-b91e-822989dbfa93.4754a503-d139-453f-8db3-804199c89632.bb23d35e-75d5-4f95-a104-deacb4ffd9db The URL for our SPA app with a set of OAuth related parameters
Redirect URL Query params Value Description
session_state 4754a503-d139-453f-8db3-804199c89632 a salted cryptographic hash
code 6857770a-06db-4131-b91e-822989dbfa93.4754a503-d139-453f-8db3-804199c89632.bb23d35e-75d5-4f95-a104-deacb4ffd9db The authorisation code issued by Keycloak. This will be exchanged for an access token.

  • Token request

Request

The SPA application can then exchange the authorisation code for an access_token with Keycloak

Method POST
URL http://localhost:8080/realms/lydtech/protocol/openid-connect/token
Body Key Body Value Description
grant_type authorization_code Indicates that we want to exchange an authorisation_code
code 6857770a-06db-4131-b91e-822989dbfa93.4754a503-d139-453f-8db3-804199c89632.bb23d35e-75d5-4f95-a104-deacb4ffd9db The authorisation code returned by Kaycloak in the authorise step above
scope openid The same scope as we requested in the authorisation step
client_id lydtech-public-client ID of the client we created in Keycloak
redirect_uri http://localhost:3000/ Where to redirect to upon completion
code_verifier 3pExtrdNnu4SrxmsnjyzhwCUQQbvyciLhgl0dBg7IoRVKAiqhYQaeWb8X7BepnLgcVGkHxl4dDRSVNkY0TJNzB3E7HFjImBY The original value used in the PKCE process that the code_challenge was calculated from. If the hash of this does not match the code_challenge that was sent at the authorise step then the Authorisation Server will reject this request

Response

This includes an access token, refresh token and (because we requested the oidc scope) an ID token (all in the form of JWT tokens). Note the access tokens have been truncated for readability. They are included in full below.

HTTP Response Code 200 (OK)
Body Key Body Value Description
access_token eyJ...xGfA (truncated) Access token in the form of a JWT. Signed by the Authorisation Server. Used as a Bearer token for any subsequent calls to the Resource Server
expires_in 300 Number of seconds that the access token is valid for (5 minutes in this case). Note the actual expiry time is in the access_token JWT itself.
id_token eyJ...9bCQ (truncated) An OIDC ID token in the form of a JWT. May contain extra claims about the users identity other than name and email address.
not-before-policy 0 Can be used to indicate a not-before-policy
refresh-expires-in 1800 Number of seconds that this refresh token is valid for (30 minutes in this case)
refresh_token eyJ...QhsE (truncated) Can be used to exchange for a new access_token via the Authorisation Server’s /token endpoint. This is typically used once the access_token expires and avoids the user re-authenticating every 5 minutes.
scope openid email profile Default OIDC scopes configured in the Authorisation Server. Could contain more OAuth scopes if these were requested at the authorisation stage.
session_state a3e0b1fe-3edb-4fc4-9942-12083334389d Indicates the Keycloak session
token_type Bearer Indicates that this token can be used as a bearer token.

Note you could copy any of these tokens into jwt.io to view the full contents.

access_token

eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJqanoyd0ZEVXhzUGdZYWowcV9FbzUtdEgxWlAzMldwMldNdDltY3RCal9RIn0.eyJleHAiOjE2ODQ4MzEyNjUsImlhdCI6MTY4NDgzMDk2NSwiYXV0aF90aW1lIjoxNjg0ODMwOTY1LCJqdGkiOiI0OTEyZGI5YS01NDIxLTRiMDQtOWIzNC1hYjVhMTg2MDI4ZmIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL2x5ZHRlY2giLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiOWRlZGQzMmYtYjIxMi00MWVjLTkyOTEtZDdhY2JiM2YxNDdmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibHlkdGVjaC1wdWJsaWMtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImEzZTBiMWZlLTNlZGItNGZjNC05OTQyLTEyMDgzMzM0Mzg5ZCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDozMDAwIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWx5ZHRlY2giLCJvZmZsaW5lX2FjY2VzcyIsImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIiwic2lkIjoiYTNlMGIxZmUtM2VkYi00ZmM0LTk5NDItMTIwODMzMzQzODlkIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJXaWxsaWFtIEJvc3MiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbkBleGFtcGxlLmNvbSIsImdpdmVuX25hbWUiOiJXaWxsaWFtIiwiZmFtaWx5X25hbWUiOiJCb3NzIiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSJ9.rXEp7F6JGBQ8_xCLs0A39KUVuO3AyYKslArIZQs2BFCKPOEvi9xW4SQEMToYPnX-qw0nEssgkBgvbaJQcCicJ9TA8D_v-TiLs_KqcnYgiIvt7yQtLBp57Fad0L_foiaCrDj8Nq9WqqjujsGbrXlDXG6_2EoqUCmtf1nKLkCYWY3ySsLAb2kVwkp1diXQFiIs8oTIKtnAPgZ-fMM5HoQ2zZ1fbFgY3vGWF_xwe5UpVUYsAEi2BIqmUPRnCnfnzskbnZfd7LQ5eV_Y0-54F8CuszCo9vi1R5tMb3mJuHbVeyEVRdlOiq6RParoZsDPGXY23Aq4ivbuxQvg0PmP9WxGfA

id_token

eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJqanoyd0ZEVXhzUGdZYWowcV9FbzUtdEgxWlAzMldwMldNdDltY3RCal9RIn0.eyJleHAiOjE2ODQ4MzEyNjUsImlhdCI6MTY4NDgzMDk2NSwiYXV0aF90aW1lIjoxNjg0ODMwOTY1LCJqdGkiOiI0ZDI4YzdmNy03YzhkLTQ2MTItOGY0ZS00NzFmZTliNzc1MTciLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL2x5ZHRlY2giLCJhdWQiOiJseWR0ZWNoLXB1YmxpYy1jbGllbnQiLCJzdWIiOiI5ZGVkZDMyZi1iMjEyLTQxZWMtOTI5MS1kN2FjYmIzZjE0N2YiLCJ0eXAiOiJJRCIsImF6cCI6Imx5ZHRlY2gtcHVibGljLWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiJhM2UwYjFmZS0zZWRiLTRmYzQtOTk0Mi0xMjA4MzMzNDM4OWQiLCJhdF9oYXNoIjoid2RfMFpYdVdCRll3T1otdzBDV3FtUSIsImFjciI6IjEiLCJzaWQiOiJhM2UwYjFmZS0zZWRiLTRmYzQtOTk0Mi0xMjA4MzMzNDM4OWQiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwibmFtZSI6IldpbGxpYW0gQm9zcyIsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluQGV4YW1wbGUuY29tIiwiZ2l2ZW5fbmFtZSI6IldpbGxpYW0iLCJmYW1pbHlfbmFtZSI6IkJvc3MiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIn0.I6fFM5cSJo5zi-kLZDX8jVsDsInHdyRK6LOTw4MN9liVYfi2-7VDAqtslON9ruqB0yoVUWe3hsFHUDOeEtRop5p2YM71y3d3kCGw9zuusBjDtb4LBsaWjnizf5FPCckKExbprpQ_l24fAg3Gj8UiwA2soSSX7HvqcHEGZWL2cqHwGu-hlSuNJrp2H3oKuV4fz_JaBMt4Xp5rO-H-citC-0Za3KArm_hzCIMOcaTL024KAwwlLWIHQRB7QYBbjw-ZgnVjF1KuEpj19myqEV_zIxWDSGzh3mjfzqlt1WyEjRSSnX9q7kpodeCiwoL-Pr1SIjWI3pUb55s_rtQU3P9bCQ

refresh_token

eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiYWI2ZTA3NS00NzZhLTRlZTktOWZkMC1mZjJiM2MyM2FkNzMifQ.eyJleHAiOjE2ODQ4MzI3NjUsImlhdCI6MTY4NDgzMDk2NSwianRpIjoiYjIyMmFkMTctNDI1Ny00N2MwLWIwNDctYWYxZWVkYmJjYWEwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9seWR0ZWNoIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9seWR0ZWNoIiwic3ViIjoiOWRlZGQzMmYtYjIxMi00MWVjLTkyOTEtZDdhY2JiM2YxNDdmIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6Imx5ZHRlY2gtcHVibGljLWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiJhM2UwYjFmZS0zZWRiLTRmYzQtOTk0Mi0xMjA4MzMzNDM4OWQiLCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIiwic2lkIjoiYTNlMGIxZmUtM2VkYi00ZmM0LTk5NDItMTIwODMzMzQzODlkIn0.rpS8agLUWBbHfI89MZ9_1Y_CHREIemo_YwgOtAEQhsE

  • Resource Server calls

Request

Any call to the Resource Server will contain the access token returned above in the Authorization HTTP header as a Bearer token

Method GET
URL http://localhost:8081/api/v1/user
HTTP Header Name Header Value Description
Authorization Bearer <access_token> Contains the access_token returned from Keycloak above as a Bearer token

Response

HTTP Response Code 200 (OK)
Body Content Description
Hello from User endpoint - admin@example.com Content returned by REST endpoint /v1/api/user - just echoes the username in this case

Summary

This article builds upon the theory introduced in Part 1 of this series. It created all the components of an architecture that leveraged OIDC & OAuth to provide authentication and authorisation via a ‘public’ client. We created and configured:

  • An Authorisation Server using Keycloak
  • A Resource Server in the shape of a REST API provided by Spring Boot
  • A client application in the form of a React SPA app
  • We showed these 3 things working together and followed the Authorisation Code Grant flow step by step, having looked at the theory for this in Part 1.

All the code is available in the following Github repositories:

Part 3 of this series shows how the Authorisation Code Grant flow can be used with a confidential client, using a traditional server side web application.

Part 4 Looks at replacing Keycloak for these projects with a custom Authorisation Server using Spring Authorisation Server project.