Build Authenticated Applications Locally with the Keycloak Extension for LocalStack

Learn how to use the Keycloak extension to add OAuth2/OIDC authentication to your AWS apps. Walk through a sample app with API Gateway, Lambda, and DynamoDB that uses Keycloak JWTs for role-based access control, all running locally without cloud dependencies.

Build Authenticated Applications Locally with the Keycloak Extension for LocalStack

Introduction

Testing authentication locally is awkward. You need an identity provider running somewhere, your Lambda functions need to validate tokens against it, and your API Gateway needs to enforce authorization. Most teams either point at a shared dev identity provider (which drifts and conflicts with other developers) or skip auth in local testing entirely.

We built a Keycloak extension for LocalStack that starts Keycloak alongside your emulated AWS services, pre-configures a realm and client, and registers Keycloak as an OIDC provider in IAM. Your Lambda authorizers validate JWTs from Keycloak, your API Gateway enforces token-based access, and you test the full authentication flow locally with no external dependencies.

In this post, we’ll install the extension, walk through how it works, and deploy a sample application: a user management API built with API Gateway, Lambda, and DynamoDB, protected by Keycloak JWT authorization with role-based access control.

What is Keycloak?

Keycloak is an open-source identity and access management solution maintained by Red Hat. It supports OAuth 2.0, OpenID Connect (OIDC), and SAML 2.0, and gives you a ready-made auth server with a management console, user and role management, client registration, and token issuance.

You create a realm (a tenant), define clients (your applications), assign roles, and Keycloak handles issuing and signing JWTs that your services can verify independently. It runs as a standalone server, which makes it a good fit for container-based development workflows.

Why run Keycloak as a LocalStack Extension?

LocalStack Extensions let you run additional services inside the same environment as your emulated AWS resources, sharing the network and lifecycle. Start LocalStack, and Keycloak comes up automatically. The extension creates a default realm (localstack), registers a client (localstack-client), assigns roles, and wires up Keycloak as an OIDC provider in LocalStack’s IAM. Lambda functions can reach Keycloak at keycloak.localhost.localstack.cloud:4566 while the rest of your app talks to API Gateway, DynamoDB, and other AWS services on the same local endpoint.

Running Keycloak as an extension gives you:

  • No separate Docker Compose or multi-container orchestration for auth. Install the extension once, and Keycloak runs whenever LocalStack runs.
  • Lambda authorizers and compute services reach Keycloak directly on the same local network.
  • Fresh Keycloak instance with each LocalStack restart, keeping integration tests deterministic.
  • Pre-configured realm, client, roles, and OIDC provider registration, so you can start issuing and validating tokens immediately.
  • Full offline development and testing of OAuth2/OIDC flows without external identity provider credentials.

How to use the Keycloak extension for LocalStack

Let’s set up the extension and deploy a sample application with JWT-based authorization.

Prerequisites

Before starting, make sure you have the following installed:

Start your LocalStack container with your LOCALSTACK_AUTH_TOKEN environment variable set:

Terminal window
export LOCALSTACK_AUTH_TOKEN=your-auth-token
localstack start

Step 1: Install the Keycloak extension for LocalStack

To install the extension, use the LocalStack CLI or the Extensions Library on the LocalStack Web Application.

With LocalStack running, run the following command:

Terminal window
localstack extensions install localstack-keycloak

After successful installation, you’ll see:

Terminal window
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
Name Summary Version Author Plugin name
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
localstack-keycloak LocalStack Extension: Keycloak for Identity 0.1.0 LocalStack Team keycloak
and Access Management
└──────────────────────┴──────────────────────────────────────────────┴─────────┴─────────────────┴─────────────┘

Alternatively, navigate to the Extensions Library on the LocalStack Web Application to install it via the UI.

Install the Keycloak extension for LocalStack

You can also install it directly when starting LocalStack using the EXTENSION_AUTO_INSTALL environment variable:

Terminal window
EXTENSION_AUTO_INSTALL=localstack-keycloak localstack start

Restart LocalStack after install. Once started, the extension will:

  1. Pull and launch Keycloak (v26.0 by default) as a Docker container
  2. Create a default realm called localstack with admin and user roles
  3. Register a client (localstack-client / localstack-client-secret) with service account roles
  4. Register Keycloak as an OIDC provider in LocalStack’s IAM

Keycloak is then available at:

  • Admin Console: http://localhost:8080/admin (credentials: admin / admin)
  • Token Endpoint: http://keycloak.localhost.localstack.cloud:4566/realms/localstack/protocol/openid-connect/token
  • Health Check: http://localhost:9000/health/ready

Step 2: Clone the sample application

We’ve prepared a sample application that uses API Gateway, Lambda, and DynamoDB with Keycloak for JWT-based authentication and role-based access control. Clone the repository and navigate to the sample app:

Terminal window
git clone https://github.com/localstack/localstack-extensions.git
cd localstack-extensions/keycloak/sample-app

The project structure looks like this:

sample-app/
├── cdk/
│ ├── app.py # CDK entry point
│ ├── stacks/
│ │ └── api_stack.py # API Gateway + Lambda + DynamoDB
│ └── requirements.txt
├── lambda/
│ ├── authorizer/
│ │ └── handler.py # JWT validation
│ └── users/
│ └── handler.py # User CRUD operations
└── README.md

The application is a user management API with CRUD endpoints (GET /users, POST /users, GET /users/{username}, PUT /users/{username}, DELETE /users/{username}), all protected by a Lambda authorizer that validates Keycloak JWTs.

The setup has two Lambda functions:

  • The authorizer extracts the JWT from the Authorization: Bearer <token> header, fetches Keycloak’s public keys from the JWKS endpoint, and validates the token signature, expiration, and audience. If the token is valid, it returns an IAM policy allowing the request and passes the user’s roles as context to the second Lambda.
  • The CRUD handler reads the authorization context (username, roles) and enforces role-based access control: any authenticated user can list or read users, but only admin role users can create, update, or delete them. Data is stored in DynamoDB.

The CDK stack in cdk/stacks/api_stack.py wires this together. The authorizer Lambda gets environment variables pointing at Keycloak (KEYCLOAK_URL, KEYCLOAK_REALM, EXPECTED_AUDIENCE), and the CRUD Lambda gets the DynamoDB table name.

Step 3: Deploy the CDK stack to LocalStack

Navigate to the CDK directory, install the Python dependencies, and deploy:

Terminal window
cd cdk
pip install -r requirements.txt
cdklocal bootstrap
cdklocal deploy --all --require-approval never

After deployment, you’ll see output like:

✅ KeycloakSampleApiStack
Outputs:
KeycloakSampleApiStack.KeycloakSampleApiEndpointF8636831 = https://<api-id>.execute-api.localhost.localstack.cloud:4566/prod/
Stack ARN:
arn:aws:cloudformation:us-east-1:000000000000:stack/KeycloakSampleApiStack/...

The stack creates a DynamoDB table (keycloak-sample-users), two Lambda functions (the JWT authorizer and the CRUD handler), and an API Gateway REST API with routes for user management.

CDK stack deployed

Step 4: Get a token and test the API

Now let’s test the API by getting a token and testing the endpoints. We’ll use the curl command to test the endpoints.

4.1: Get an access token from Keycloak

Request a JWT from Keycloak using the client credentials grant:

Terminal window
TOKEN=$(curl -s -X POST \
"http://keycloak.localhost.localstack.cloud:4566/realms/localstack/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=localstack-client" \
-d "client_secret=localstack-client-secret" | jq -r '.access_token')

You can inspect the token’s contents to see what roles and claims it carries:

Terminal window
PAYLOAD=$(echo $TOKEN | cut -d'.' -f2)
python3 -c "import sys,json,base64; p=sys.argv[1]; print(json.dumps(json.loads(base64.urlsafe_b64decode(p+'='*(4-len(p)%4))), indent=2))" "$PAYLOAD"

The output includes the realm_access.roles field with both admin and user roles (the default service account has the admin role):

{
"realm_access": {
"roles": [
"offline_access",
"admin",
"default-roles-localstack",
"uma_authorization",
"user"
]
},
"azp": "localstack-client",
"...": "..."
}

4.2: Set up the API URL

Grab the API Gateway ID and construct the base URL:

Terminal window
API_ID=$(awslocal apigateway get-rest-apis --query 'items[0].id' --output text)
API_URL="http://localhost:4566/_aws/execute-api/${API_ID}/prod"

4.3: Test the CRUD endpoints

Start by listing users (the table is empty at first):

Terminal window
curl -s -H "Authorization: Bearer $TOKEN" "${API_URL}/users" | jq .
{
"users": [],
"count": 0
}

Create a user (requires the admin role):

Terminal window
curl -s -X POST "${API_URL}/users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"username": "john", "email": "john@example.com", "name": "John Doe"}' | jq .
{
"username": "john",
"email": "john@example.com",
"name": "John Doe",
"created_at": "2026-04-14T09:58:14.841752",
"created_by": "service-account-localstack-client"
}

The created_by field is set to service-account-localstack-client, the identity extracted from the JWT by the authorizer and passed to the CRUD Lambda through API Gateway context.

Retrieve, update, and delete the user:

Terminal window
# Get user
curl -s -H "Authorization: Bearer $TOKEN" "${API_URL}/users/john" | jq .
# Update user (admin role required)
curl -s -X PUT "${API_URL}/users/john" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "john.doe@example.com"}' | jq .
# Delete user (admin role required)
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "${API_URL}/users/john" | jq .

After deletion, a GET on the same user returns a 404:

{
"error": "User 'john' not found"
}

4.4: What happens without a token

Without a token, the authorizer returns a Deny policy and API Gateway blocks the request:

Terminal window
curl -s "${API_URL}/users" | jq .
{
"message": "Unauthorized"
}

Step 5: Explore the Keycloak Admin Console

The extension exposes the Keycloak Admin Console at http://localhost:8080/admin. Log in with admin / admin to browse the setup.

From the console, you can:

  • View the localstack realm and its settings
  • See the admin and user realm roles under Realm roles
  • Browse the localstack-client configuration under Clients, including its service account roles
  • Create additional users, assign roles, and test different access patterns

Keycloak Admin Console

Conclusion

In this tutorial, we deployed a user management API with API Gateway, Lambda, and DynamoDB, then walked through the full OAuth2 flow: token issuance from Keycloak, JWT validation in a Lambda authorizer, and role-based access control in the CRUD handler.

Any application that uses JWTs for authorization, whether through API Gateway authorizers, AssumeRoleWithWebIdentity, or direct token validation in your code, can use this extension to test those flows locally. You get a real Keycloak instance with real tokens and real signature validation, not mocked auth that diverges from production behaviour.

Learn More


Harsh Mishra
Harsh Mishra
Engineer at LocalStack
Harsh Mishra is an Engineer at LocalStack and AWS Community Builder. Harsh has previously worked at HackerRank, Red Hat, and Quansight, and specialized in DevOps, Platform Engineering, and CI/CD pipelines.