Skip to main content

Securing Keycloak with OIDC SPA and Phase Two

· 6 min read
Phase Two
Hosted Keycloak and Keycloak Support
OIDC SPA Logo

Our pal over at Keycloakify has been working on creating a simple OpenId Connect (OIDC) library called, OIDC Spa. As with Joseph's usual approach to user friendliness, OIDC SPA simplifies a lot of the integration work than can come with adding an Authentication and Authorization layer to your application. Follow along as we show you how to integrate OIDC SPA with a Phase Two's free Keycloak instance.

We're going to work through an example of how to add OIDC SPA to a React application. If you just want to skip to code, check out our React example.

Setting up a Keycloak Instance

Instructions
tip

If you already have a functioning Keycloak instance, you can skip to the next section.

Rather than trying to set up a "from scratch" instance of Keycloak, we're going to short-circuit that process by leveraging a free Phase Two Starter instance. The Starter provides a free hosted instance of Phase Two's enhanced Keycloak ready for light production use cases.

  • Visit the sign-up page.

  • Enter an email, use a Github account, or use an existing Google account to register.

    Phase Two Register

  • Follow the register steps. This will include a sign-in link being sent to your email. Use that for password-less login.

    Phase Two Email Magic Link Register

  • After creating an account, a realm is automatically created for you with all of the Phase Two enhancements. You need to create a Deployment in the Shared Phase Two infrastructure in order to gain access to the realm. Without a deployment created, the Create Shared Deployment modal will automatically pop up.

  • Create a Shared Deployment by providing a region (pick something close to your existing infrastructure), a name for the deployment, and selecting the default organization that was created for you upon account creation. Hit "Confirm" when ready. Standby while our robots get to work generating your deployment. This can take a few seconds.

    Phase Two Create Shared Deployment

  • After the deployment is created and active, you can access the Keycloak Admin console by clicking "Open Console" for that deployment. Open it now to see the console.

    Phase Two Open Console Keycloak Admin UI

At this point, move on to the next step in the tutorial. We'll be coming back to the Admin Console when its time to start connecting our App to the Keycloak instance.

Setting up an OIDC Client

Instructions

We need to create a OpenID Connect Client in Keycloak for the app to communicate with. Keycloak's docs provide steps for how to create an OIDC client and all the various configurations that can be introduced. Follow the steps below to create a client and get the right information necessary for app configuration.

  1. Open the Admin UI by clicking Open Console in the Phase Two Dashboard.

  2. Click Clients in the menu.

  3. Click Create client.

  4. Leave Client type set to OpenID Connect.

  5. Enter a Client ID. This ID is an alphanumeric string that is used in OIDC requests and in the Keycloak database to identify the client.

  6. Supply a Name for the client.

  7. Click Next.

    Keycloak OIDC Create Client General Settings

  8. Under the Capability Config section, leave the defaults as selected. This can be configured further later.

    • Client authentication to Off.
    • Authorization to Off.
    • Standard flow checked. Direct access grants checked. All other items unchecked.
  9. Click Next.

    Keycloak OIDC Create Client Capability Config

  10. Under Login settings we need to add a redirect URI and Web origin in order. Assuming you are using the example applicaiton:

    Valid redirect URI (allows redirect back to application)

    http://localhost:3000/*

    Web origins (allows for Token auth call)

    http://localhost:3000
    URI and Origin Details

    The choice of localhost is arbitrary. If you are using an example application running locally, this will apply. If you are using an app that you actually have deployed somewhere, then you will need to substitute the appropriate URI for that.

  11. Click Save

    Keycloak OIDC Create Login Settings

OIDC Config

We will need values to configure our application. To get these values follow the instructions below.

  1. Click Clients in the menu.

  2. Find the Client you just created and click on it. In the top right click the Action dropdown and select Download adapter config.

  3. Select Keycloak OIDC JSON in the format option. The details section will populate with the details we will need.

    • Note the realm, auth-server-url, and resource values.

    Keycloak OIDC Create Client Adapter Config

Adding a Non-Admin User

Instructions
tip

It is bad practice to use your Admin user to sign in to an Application.

Since we do not want to use our Admin user for signing into the app we will build, we need to add a another non-admin user.

  1. Open the Admin UI by clicking Open Console in the Phase Two Dashboard.
  2. Click Users in the menu.
  3. Click Add user.
  4. Fill out the information for Email, First name, and Last name. Click Create.
  5. We will now set the password for this user manually. Click Credentials (tab) and click Set Password. Provide a password for this user. For our use case, as a tutorial, you can leave "Temporary" set to "Off".
  6. Click Save and confirm the password by clicking Save password

Setting up a ReactJS Project

As this is a more interactive project, we're going to walk through a bit more integration. We've got a very basic starter template which you can find here. If you'd like to see more examples, check out the oidc-spa repo

  1. Clone the starter repo and install the OIDC SPA library:
npm install oidc-spa
  1. Add an OIDC Provider in index.tsx. We'll be using the params from the Keycloak instance we set up earlier (update issuerUri and clientId as needed):
import { createReactOidc } from "oidc-spa/react";

export const { OidcProvider, useOidc, getOidc } = createReactOidc({
// NOTE: If you don't have the params right away see note below.
issuerUri: "https://app.phasetwo.io/auth/realms/p2examples",
clientId: "reactjs-example",
/**
* Vite: `publicUrl: import.meta.env.BASE_URL`
* CRA: `publicUrl: process.env.PUBLIC_URL`
* Other: `publicUrl: "/"` (Usually)
*/
publicUrl: process.env.BASE_URL,
});

Once that is added, we can render the App within the OidcProvider:

ReactDOM.createRoot(document.getElementById("root")!).render(
<OidcProvider
// Optional
fallback={<>Checking authentication ⌛️</>}
>
<App />
</OidcProvider>
);
  1. Use the useOidc hook to access the OIDC context from index.tsx in the Auth.tsx file. This will allow you to check if the user is logged in, log them in, log them out, and access the OIDC tokens:
import { useOidc } from "./index";

const Auth = () => {
const { isUserLoggedIn, login, logout, oidcTokens } = useOidc();
...
};

export default Auth;

Looking at what comes back, we have a few main items:

  • isUserLoggedIn - A boolean value that tells you if the user is logged in.
  • login - A function that will log the user in.
  • logout - A function that will log the user out.
  • oidcTokens - An object that contains the OIDC tokens. From this we can grab the decodedIdToken and to see information about the user. There are other tokens within this like accessToken, refreshToken, and idToken, which can be used for various purposes.
info

One of the nicest things that OIDC SPA does well is handling your token refresh for you. This is a common issue with OIDC libraries and OIDC SPA has a nice solution.

Let's move forward with building out this component and adding some logic to handle the user's authentication state. Some of the mark is purely for layout and styling, but the logic is what we're after:

  • oidcTokens.decodedIdToken?.email as string we type this as since the decodedIdToken isn't defined. If you want to pretype the decodedIdToken in the createReactOidc function, do so by providing a decodedIdTokenSchema key and optionally validate it with zod.
let content;

if (isUserLoggedIn) {
content = (
<div>
<div className="mb-2 text-p2blue-700 text-2xl">Authenticated</div>
<div className="mb-6 text-p2blue-700 text-md">
<div>{oidcTokens.decodedIdToken?.email as string}</div>
<div>{oidcTokens.decodedIdToken?.sub as string}</div>
</div>
<button
className="rounded-md bg-indigo-500 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
onClick={() => logout({ redirectTo: "current page" })}
>
Log out
</button>
<Token />
</div>
);
} else {
content = (
<div>
<div className="mb-6 text-p2blue-700 text-2xl">Not authenticated.</div>
<button
className="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
onClick={() => login({ doesCurrentHrefRequiresAuth: true })}
>
Log in
</button>
</div>
);
}

return (
<div>
<div className="text-xl pb-8 italic">Your current status is:</div>
{content}
</div>
);
  1. Run the application:
npm run start
  1. Test the login and logout functionality. You should see the user's email and sub displayed when logged in. If you're using the Keycloak instance we set up earlier, you can use the non-admin user we created.

Bonus

A very common use case is security API calls. An example api.ts file below shows how you would intercept all requests and append the accessToken to the headers.

This example is provided with Axios by Keycloakify. We'll implement it in fetch as an alternative.

import { getOidc } from "oidc";

type Api = {
getTodos: () => Promise<{ id: number; title: string }[]>;
addTodo: (todo: { title: string }) => Promise<void>;
};

const baseURL = import.meta.env.API_URL;

const fetchWithAuth = async (url: string, options: RequestInit = {}) => {
const oidc = await getOidc();

if (!oidc.isUserLoggedIn) {
throw new Error(
"We made a logic error: The user should be logged in at this point"
);
}

const headers = {
...options.headers,
Authorization: `Bearer ${oidc.getTokens().accessToken}`,
"Content-Type": "application/json",
};

const response = await fetch(`${baseURL}${url}`, { ...options, headers });

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

return response.json();
};

export const api: Api = {
getTodos: () => fetchWithAuth("/todo"),
addTodo: (todo) =>
fetchWithAuth("/todo", {
method: "POST",
body: JSON.stringify(todo),
}),
};

We aren't setting this actual backend up, but you can see how you would use the api.ts file to make authenticated requests.

Conclusion

This only covers the most very basic installation and usage of this library. There are a lot of different ways you leverage the tool and we invite you to investigate further. Some of which are:

  • Auto Logout (due to inactivity)
  • Error Management (when login fails)
  • Globally enforced authentication (every route requires authentication)
  • Usage with routing libraries

We'd love to hear how you're using OIDC SPA in your applications. If you have any questions or need help, feel free to reach out to us at Phase Two. We're always happy to help.

Phase Two's enhanced Keycloak provides many ways to quickly control and tweak the log in and user management experience. Our blog has many use cases from customizing login pages, setting up magic links (passwordless sign in), and Organization workflows.