App-to-App Authentication with Keycloak – Step by Step Guide

In this step-by-step guide, we will explore how to set up Keycloak for app-to-app authentication.

Project

In this demo, we are going to create a project named “Cool Project” . It has a FastAPI-based API server and client app. For app-to-app authentication/authorization, we will use the OAuth2 client credentials flow.

OAuth 2.0 Client Credentials Flow Specification – RFC 6749

The components of our project are :

  • Resource server – FastAPI server
  • Client – Client app ( For the simplicity of this demo, we will use Postman to call the APIs).
  • Authorization Server – Keycloak

We will use the client ID and client secret of the client app to get an access token from the Keycloak authorization server. We will use this access token to call the FastAPI server APIs.

App to app authentication keycloak system

Keycloak

Keycloak is an open-source identity and access management solution that provides single sign-on (SSO), authentication, and authorization for web and mobile applications. It supports modern security protocols like OAuth2 and OpenID Connect, integrates with LDAP and social logins, and offers centralized user management and customization. Keycloak supports the OAuth2 Client Credentials flow, which can be used for app-to-app authentication and authorization.

Start Keycloak

First, we need to download Keycloak.

Download the Keycloak zip file from here and extract the zip file. We are using Keycloak version 26.3.

After extracting the zip, enter the keycloak directory and run the following command in the command prompt:

bin\kc.bat start-dev

Please note that we are running Keycloak in development mode. Open the URL http://localhost:8080/ in a browser. For this demo, we’re going to create an admin user.

Create an admin user with a username admin and password as adminpass Then log in to Keycloak using the admin user.

Create a realm for the project

Next, we need to create a realm for our project. A realm in Keycloak is an isolated security and administrative domain where you manage users, applications, roles, and groups. In simple terms, we can think of it as a project with all the details.. Each realm controls authentication and authorization for its contained resources, ensuring that users and configurations are separated from other realms, enabling multi-tenancy and organized management.

Let’s create a cool project realm. Click on “Manage realms” on the left sidebar and create a realm as shown below.

Keycloak realm

Create client

Next, we need to create a client in the coolproject realm.

Create keycloak client

Click next, and on the next page, we need to turn on “Client authentication” and “Service account roles” as shown below :

Create keycloak client service account

The above settings enable us to use the OAuth2 client credentials flow.

Click next and leave everything as it is, and click save. Now, click on clients again on the left sidebar, as we can see the newly created client “client-app” listed as shown below :

Create keycloak clients

Client ID and Client Secret

In the OAuth2 Client credentials flow, we need the client ID and client secret.

Open the client-app from the left sidebar and we can see that Client ID is “client-app” as shown below :

Create keycloak client ID

Next, we need the client secret. Click the credentials tab.

Create keycloak client secret

Open the http://localhost:8080/realms/coolproject/.well-known/openid-configuration in a browser. We can see the token endpoint as shown below. We will use this URL in Postman to get an access token for the client app.

Keycloak OpenID endpoints

Add audience claim

In the Client Credentials flow, there is no user; it’s an application-to-application (machine-to-machine) authentication. The audience (aud) claim in the access token typically refers to the resource server, that is, the application or API that will receive and validate the token.

We need to add the audience claim to the access token payload. Open clients, then client scopes, and click one client-app-dedicated (a client scope that is ending dedicated).

Add audience claim

Then turn off “Full scope allowed” (It will remove the default audience “account” ) as shown below.

Disable fullscope

Next, we will create a new client scope for the audience with value as the resource server name.

Add new audience
Open the api-audience client scope and click Mapper, and create a new mapper for Audience. In the “included Custom Audience” section, we can give the REST API server name(apiserver). See the screenshots below for reference.
Add new audience client scope mapper

Next, open the client and add the newly created “api-audience” scope to the client-app. Open clients and client scopes, add api-audience as shown below:

Add audience client scope

Ensure that api-audience assigned type is “Default” as shown below.

Now our client app configuration is ready.

Start Resource Server

Next, we need to start the resource server. In this example, we are going to use a simple FastAPI application.

from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt
import httpx

app = FastAPI()
security = HTTPBearer()


KEYCLOAK_REALM = "coolproject"
KEYCLOAK_URL = "http://localhost:8080"
KEYCLOAK_OPENID_CONFIG = (
    f"{KEYCLOAK_URL}/realms/{KEYCLOAK_REALM}/.well-known/openid-configuration"
)

JWKS = None
from jose.utils import base64url_decode
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend


async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    token = credentials.credentials
    jwks = await get_public_key()

    try:
        unverified_header = jwt.get_unverified_header(token)
        kid = unverified_header.get("kid")
        if not kid:
            raise Exception("Missing 'kid' in token header")

        key = next(k for k in jwks["keys"] if k["kid"] == kid)

        n = int.from_bytes(base64url_decode(key["n"].encode()), byteorder="big")
        e = int.from_bytes(base64url_decode(key["e"].encode()), byteorder="big")

        public_key = rsa.RSAPublicNumbers(e, n).public_key(default_backend())

        payload = jwt.decode(
            token,
            key=public_key,
            algorithms=["RS256"],
            audience="apiserver",
            issuer=f"{KEYCLOAK_URL}/realms/{KEYCLOAK_REALM}",
        )
        print(payload)
        return payload

    except Exception as e:
        raise HTTPException(status_code=401, detail=f"Invalid token: {str(e)}")


async def get_public_key():
    global JWKS
    if JWKS is None:
        async with httpx.AsyncClient() as client:
            r = await client.get(KEYCLOAK_OPENID_CONFIG)
            jwks_uri = r.json()["jwks_uri"]
            r2 = await client.get(jwks_uri)
            JWKS = r2.json()
    return JWKS


@app.get("/secure-data")
async def secure_data(user=Depends(verify_token)):
    return {"message": "You have access!", "client": user["sub"]}


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("app:app", host="127.0.0.1", port=8000, reload=True)

Create a requirements.txt and install it

fastapi
httpx
python-jose[cryptography]
uvicorn

In the JWT decode, we need to give the audience as the “apiserver” that we configured

payload = jwt.decode(
            token,
            key=public_key,
            algorithms=["RS256"],
            audience="apiserver",
            issuer=f"{KEYCLOAK_URL}/realms/{KEYCLOAK_REALM}",
        )

Start the FastAPI server. Next, we will get an access token using the /token request as shown below :

Postman token request

Finally, we will use this access token to call the APIs as shown below :

Postman access resource using access token

We can see that the access token payload contains the “aud” claim.

Aud claim

Conclusion

In conclusion, it is easier to set up an authorization server using client credential flow in Keycloak. You can read about How to serve LLM using the OpenVINO Model Server on Windows.