Understanding OIDC Authentication Flows
In this article, we'll discuss authentication flows in OpenID Connect (OIDC). We'll work through a hands-on example using Keycloak to demonstrate how OIDC works in practice. More information on Keycloak's authorization flows can be found here or in the OIDC Spec.
Set Up
Keycloak Server
To begin, we chose to use Keycloak as our Identity Provider (IDP) and OpenID Connect (OIDC) provider. Keycloak is an open source identity and access management solution that makes it easy to secure applications and services with little to no code. It is a single sign-on provider that allows you to authenticate users across all applications and services in your organization.
It's also free and open source. For that reason we chose to stand it up on Docker, which allowed us to quickly stand up a demo environment that allowed use to share all the configuration details, URLs, tokens, etc. in this post without having to worry about exposing any sensitive information. We were then to simply tear down the environment at the end of this effort.
version: '3.2'
name: 'lab'
services:
keycloak:
image: quay.io/keycloak/keycloak:20.0.3
container_name: keycloak.lab
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
KC_HEALTH_ENABLE: true
command: start-dev --log-level=INFO
Note that we are running Keycloak in dev mode in this example. We are also using insecure credentials and serving content with HTTP not HTTPS. We did this to make it easier to quickly demonstrate key concepts without getting bogged down in configuration details.
Do not run in dev mode in production.
After this, we can simple run docker compose up
to run the Keycloak server.
Configuring Keycloak
Step 1. Create a Realm
First we need to create a realm. Realms are Keycloaks way of grouping and isolating users, applications, and configuration. In this case we are creating a realm called "Demo".
Step 2. Create a User
Next we need to create a user. This user will be used to log into our client application. We can click on the "Users" tab and then click "Add User" to create a new user.
We also need to set default credentials for that user. On the "Credentials" tab we can set a new password.
In this case we are using the username demo
and the password 1234
. Make sure to uncheck the
"Temporary" checkbox so that the password is not reset on the next login.
Step 3. Create a Client
Next we need to create a client. This client will represent our client application. We can click on the "Clients" tab and then click "Create" to create a new client.
In this case will use the default settings initially, but we'll need to update the "Valid Redirect
URIs" to include the URL of our client application. In this case we are using http://localhost:3000/*
.
In the client settings we need to set the "Valid Redirect URIs" field and "Web Origins" field.
We use "*" for web origins to allow all hosts to use this client. We set the valid redirect URIs
to http://localhost*
to support our local React application.
Note that we are not using a secure configuration in this example. We are redirecting to localhost using HTTP. We also allow all hosts to use this client. We did this to make it easier to quickly demonstrate key concepts without getting distracted by configuration details.
Do not use this in production.
The Client Application
To complete our setup, we need to create a client application that will authenticate with Keycloak. In this case we are using a React application, but the same principles apply to any application. We'll glance past much of the detail here, but the core elements are the keycloak configuration and the user login.
We've created a simple keycloak.js file that exports a keycloak client. This client is configured with the realm, url, and client id that we created in the previous step.
import Keycloak from "keycloak-js";
const keycloak = new Keycloak({
realm: "Demo",
url: "http://localhost:8080",
clientId: "demo",
});
export default keycloak;
We then use this client in our main React app. The below code is a simplification of our app the demonstrates how we use the keycloak client to authenticate the user and retrieve the user's profile and token. We then set this user information as a state variable in our app and pass it to other components via a context provider.
import keycloak from './keycloak';
function App() {
const [user, setUser] = useState<UserIF | undefined>()
const initKeycloak = useCallback(async() => {
let token: string | undefined = keycloak.token
if (token) {
return
}
const res = await keycloak.init({
onLoad: 'login-required',
checkLoginIframe: false,
enableLogging: true
})
// If authenticated, set the user state
if (res) {
setUser({
profile: { ...keycloak.tokenParsed }
token: keycloak.token
})
}
// Otherwise, clean up state
else {
setUser(null)
}
}, [])
useEffect(() => {
if (!keycloak.token) {
initKeycloak()
}
}, [initKeycloak])
return (
<UserContext.Provider value={user}>
/*...*/
</UserContext.Provider>
)
}
In summary, the React app checks if the user is already authenticated by checking for a token.
If the token does not exist yet, it will call keycloak.init()
to initialize keycloak which will
trigger the OIDC flow to authenticate the user. The next section describes this flow in more detail.
Authorization Code Flow
We'll now walk through the authorization code flow to understand how it works. Authorization Code Flow is one type of authentication flow in OIDC, but it is the most common.
Once we go to http://localhost:3000
to access our web application, the first thing that happends is
we are redirected to the Keycloak server to authenticate.
This is an important part of the authorization code flow because the client application never sees the user's credentials. Instead, the user is redirected to the IDP to authenticate. This is a key security feature of OIDC. The redirect URI is provided to the IDP as part of the authentication request so that the OIDC provider knows where to redirect the user back to after authentication.
Let's take a look at the network requests that occur once we log in. The first request is to the Keycloak
server to authenticate. This is a POST request to http://localhost:8080/realms/Demo/login-actions/authenticate
.
The user's credentials are passed to the server as part of the request body.
Notice we get a 302 redirect response back from the server. This is the server redirecting us to the
redirect URL that we provided in the authentication request. In this case, the callback URL is
http://localhost:3000/
. We can see this in the Location
response header.
Also note the Set-Cookie
header. That sets the KEYCLOAK_IDENTITY
token in the browser. This is
critical for the next step in the flow.
Next, we are redirected back to the client application, specifically the redirect URI.
This is the callback URL that we provided in the authentication request. We get a 200 OK
response
and next the React JS bundle is loaded (e.g. bundle.js
). This contains all our client application
code that logs in the user.
Finally, the client application makes another request to Keycloak to get the user's token.
The code that was provided to the client application in the callback URL is passed to the Keycloak server as part of the request body as well as the identity token as a cookie.
Together these are used to authenticate the request and return the user's token. The server then
responds with a 200 OK
response and the user's identity, access, and refresh tokens.
Now the client application has access to the tokens with it can use to authenticate future requests to the server. The access token can now be passed as an authentication header when making API calls to a backend service. The backend service can then validate the token with the IDP to authenticate the user.
Implicit Flow
Without going through all the details again, we'll briefly discuss the implicit flow. The implicit flow is similar to the authorization code flow, but does not require the extra step to request the token.
In implicit flow, the IDP server includes the token as part of the redirect.
Direct Grant
The direct grant is simpler, but less secure. With direct grant, the client application sends the user's credentials directly to the IDP server and gets the token back in response.
Direct Grant is Keycloak's term for this flow. The OAuth 2.0 spec refers to this as the Resource Owner Password Credentials Grant. The spec is available here.
It's simpler because it does not require the extra redirect step. But it's less secure because the client application has access to the user's credentials. It also means the application needs to have a log in page.
Summary
In this article, we discussed authentication flows in OpenID Connect (OIDC). We provided a hands-on example of Authorization Code Flow using Keycloak with a simple React application. We also briefly discussed the Implicit Flow and Direct Grant.
Use the examples above to try out the different flows yourself and explore the topic further.