Skip to content

Commit 3489022

Browse files
committed
add initial version of keycloak extension
1 parent 6888e92 commit 3489022

21 files changed

Lines changed: 2348 additions & 0 deletions

File tree

.github/workflows/keycloak.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: LocalStack Keycloak Extension Tests
2+
3+
on:
4+
push:
5+
paths:
6+
- keycloak/**
7+
branches:
8+
- main
9+
pull_request:
10+
paths:
11+
- .github/workflows/keycloak.yml
12+
- keycloak/**
13+
workflow_dispatch:
14+
15+
env:
16+
LOCALSTACK_DISABLE_EVENTS: "1"
17+
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
18+
19+
jobs:
20+
integration-tests:
21+
name: Run Integration Tests
22+
runs-on: ubuntu-latest
23+
timeout-minutes: 15
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
28+
- name: Setup LocalStack and extension
29+
run: |
30+
cd keycloak
31+
32+
docker pull localstack/localstack-pro &
33+
docker pull quay.io/keycloak/keycloak:26.0 &
34+
pip install localstack
35+
36+
make install
37+
make lint
38+
make dist
39+
localstack extensions -v install file://$(ls ./dist/localstack_extension_keycloak-*.tar.gz)
40+
41+
DEBUG=1 localstack start -d
42+
localstack wait
43+
44+
- name: Run integration tests
45+
run: |
46+
cd keycloak
47+
make test
48+
49+
- name: Print logs
50+
if: always()
51+
run: |
52+
localstack logs
53+
localstack stop

keycloak/.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.venv/
2+
*.egg-info/
3+
dist/
4+
build/
5+
__pycache__/
6+
*.pyc
7+
.eggs/
8+
.pytest_cache/
9+
.ruff_cache/
10+
cdk.out/
11+
plan.md

keycloak/Makefile

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
VENV_BIN = python3 -m venv
2+
VENV_DIR ?= .venv
3+
VENV_ACTIVATE = $(VENV_DIR)/bin/activate
4+
VENV_RUN = . $(VENV_ACTIVATE)
5+
TEST_PATH ?= tests
6+
7+
usage: ## Shows usage for this Makefile
8+
@cat Makefile | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
9+
10+
venv: $(VENV_ACTIVATE)
11+
12+
$(VENV_ACTIVATE): pyproject.toml
13+
test -d .venv || $(VENV_BIN) .venv
14+
$(VENV_RUN); pip install --upgrade pip setuptools plux
15+
$(VENV_RUN); pip install -e .[dev]
16+
touch $(VENV_DIR)/bin/activate
17+
18+
clean:
19+
rm -rf .venv/
20+
rm -rf build/
21+
rm -rf .eggs/
22+
rm -rf *.egg-info/
23+
24+
install: venv ## Install dependencies
25+
$(VENV_RUN); python -m plux entrypoints
26+
27+
dist: venv ## Create distribution
28+
$(VENV_RUN); python -m build
29+
30+
publish: clean-dist venv dist ## Publish extension to pypi
31+
$(VENV_RUN); pip install --upgrade twine; twine upload dist/*
32+
33+
entrypoints: venv ## Generate plugin entrypoints for Python package
34+
$(VENV_RUN); python -m plux entrypoints
35+
36+
format: ## Run ruff to format the codebase
37+
$(VENV_RUN); python -m ruff format .; make lint
38+
39+
lint: ## Run ruff to lint the codebase
40+
$(VENV_RUN); python -m ruff check --output-format=full .
41+
42+
test: ## Run integration tests (requires LocalStack running with the Extension installed)
43+
$(VENV_RUN); pytest $(PYTEST_ARGS) $(TEST_PATH)
44+
45+
deploy: ## Deploy the sample app CDK stack
46+
cd sample-app/cdk && pip install -r requirements.txt && cdklocal bootstrap && cdklocal deploy --all --require-approval never
47+
48+
test-sample: ## Run sample app API tests (requires LocalStack running with deployed stack)
49+
@echo "Getting token from Keycloak..."
50+
@TOKEN=$$(curl -s -X POST \
51+
"http://keycloak.localhost.localstack.cloud:4566/realms/localstack/protocol/openid-connect/token" \
52+
-d "grant_type=client_credentials" \
53+
-d "client_id=localstack-client" \
54+
-d "client_secret=localstack-client-secret" | jq -r '.access_token') && \
55+
API_ID=$$(awslocal apigateway get-rest-apis --query 'items[0].id' --output text) && \
56+
API_URL="http://localhost:4566/_aws/execute-api/$${API_ID}/prod" && \
57+
echo "API URL: $${API_URL}" && \
58+
echo "\n=== List users ===" && \
59+
curl -s -H "Authorization: Bearer $${TOKEN}" "$${API_URL}/users" | jq . && \
60+
echo "\n=== Create user ===" && \
61+
curl -s -X POST "$${API_URL}/users" \
62+
-H "Authorization: Bearer $${TOKEN}" \
63+
-H "Content-Type: application/json" \
64+
-d '{"username": "testuser", "email": "test@example.com", "name": "Test User"}' | jq . && \
65+
echo "\n=== Get user ===" && \
66+
curl -s -H "Authorization: Bearer $${TOKEN}" "$${API_URL}/users/testuser" | jq . && \
67+
echo "\n=== Delete user ===" && \
68+
curl -s -X DELETE -H "Authorization: Bearer $${TOKEN}" "$${API_URL}/users/testuser" | jq . && \
69+
echo "\n=== Sample app tests completed ==="
70+
71+
clean-dist: clean
72+
rm -rf dist/
73+
74+
.PHONY: clean clean-dist dist install publish usage venv format test lint entrypoints deploy test-sample

keycloak/README.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# Keycloak on LocalStack
2+
3+
This repo contains a [LocalStack Extension](https://github.com/localstack/localstack-extensions) that runs [Keycloak](https://www.keycloak.org/) alongside LocalStack for identity and access management with local AWS applications.
4+
5+
This Extension:
6+
7+
- Spins up a Keycloak instance on LocalStack startup.
8+
- Auto-registers Keycloak as an OIDC identity provider in LocalStack IAM.
9+
- Ships with a default realm (`localstack`) ready for OAuth2/OIDC flows.
10+
- Exchanges Keycloak JWTs for temporary AWS credentials via `AssumeRoleWithWebIdentity`.
11+
12+
## Prerequisites
13+
14+
- Docker
15+
- LocalStack Pro
16+
- `localstack` CLI
17+
- `make`
18+
19+
## Installation
20+
21+
```bash
22+
localstack extensions install "git+https://github.com/localstack/localstack-extensions.git#egg=localstack-keycloak&subdirectory=keycloak"
23+
```
24+
25+
## Install local development version
26+
27+
To install the extension into LocalStack in developer mode, you will need Python 3.11, and create a virtual environment in the extensions project.
28+
29+
```bash
30+
make install
31+
```
32+
33+
Then, to enable the extension for LocalStack, run
34+
35+
```bash
36+
localstack extensions dev enable .
37+
```
38+
39+
You can then start LocalStack with `EXTENSION_DEV_MODE=1` to load all enabled extensions:
40+
41+
```bash
42+
EXTENSION_DEV_MODE=1 localstack start
43+
```
44+
45+
## Usage
46+
47+
Start LocalStack:
48+
49+
```bash
50+
localstack start
51+
```
52+
53+
Keycloak will be available at:
54+
55+
| Endpoint | URL |
56+
|----------|-----|
57+
| Admin Console | http://localhost:8080/admin |
58+
| Token Endpoint | http://keycloak.localhost.localstack.cloud:4566/realms/localstack/protocol/openid-connect/token |
59+
| JWKS URL | http://keycloak.localhost.localstack.cloud:4566/realms/localstack/protocol/openid-connect/certs |
60+
61+
Keycloak ports are exposed directly on the host for easy access:
62+
63+
- **Admin Console & HTTP (8080)**: `http://localhost:8080` - Use this for the admin UI and direct API access
64+
- **Management (9000)**: `http://localhost:9000` - Health and metrics endpoints (Keycloak 26+)
65+
66+
The gateway URL (`keycloak.localhost.localstack.cloud:4566`) is available for token endpoints and OIDC flows.
67+
68+
- **Default Admin Credentials**: `admin` / `admin`
69+
- **Health check**: `curl http://localhost:9000/health/ready`
70+
71+
### Get an Access Token
72+
73+
```bash
74+
TOKEN=$(curl -s -X POST \
75+
"http://keycloak.localhost.localstack.cloud:4566/realms/localstack/protocol/openid-connect/token" \
76+
-d "grant_type=client_credentials" \
77+
-d "client_id=localstack-client" \
78+
-d "client_secret=localstack-client-secret" | jq -r '.access_token')
79+
```
80+
81+
### Exchange Token for AWS Credentials
82+
83+
```bash
84+
# Create IAM role that trusts Keycloak
85+
cat > trust-policy.json << 'EOF'
86+
{
87+
"Version": "2012-10-17",
88+
"Statement": [{
89+
"Effect": "Allow",
90+
"Principal": {
91+
"Federated": "arn:aws:iam::000000000000:oidc-provider/keycloak.localhost.localstack.cloud:4566/realms/localstack"
92+
},
93+
"Action": "sts:AssumeRoleWithWebIdentity"
94+
}]
95+
}
96+
EOF
97+
98+
awslocal iam create-role \
99+
--role-name KeycloakAuthRole \
100+
--assume-role-policy-document file://trust-policy.json
101+
102+
# Exchange Keycloak token for AWS credentials
103+
awslocal sts assume-role-with-web-identity \
104+
--role-arn arn:aws:iam::000000000000:role/KeycloakAuthRole \
105+
--role-session-name test-session \
106+
--web-identity-token "$TOKEN"
107+
```
108+
109+
## Configuration
110+
111+
| Environment Variable | Default | Description |
112+
|---------------------|---------|-------------|
113+
| `KEYCLOAK_REALM` | `localstack` | Name of the default realm |
114+
| `KEYCLOAK_VERSION` | `26.0` | Keycloak Docker image version |
115+
| `KEYCLOAK_REALM_FILE` | - | Path to custom realm JSON file |
116+
| `KEYCLOAK_DEFAULT_USER` | - | Username for auto-created test user |
117+
| `KEYCLOAK_DEFAULT_PASSWORD` | - | Password for auto-created test user |
118+
| `KEYCLOAK_OIDC_AUDIENCE` | `localstack-client` | Audience claim for OIDC provider |
119+
| `KEYCLOAK_FLAGS` | - | Additional flags for Keycloak start command |
120+
121+
> **Note**: When using `localstack start`, prefix environment variables with `LOCALSTACK_` (e.g., `LOCALSTACK_KEYCLOAK_REALM`).
122+
123+
### Custom Realm Configuration
124+
125+
Use your own realm JSON file with pre-configured users, roles, and clients.
126+
127+
```bash
128+
# The path must be an absolute HOST path for Docker mount
129+
# Use LOCALSTACK_ prefix when running via CLI
130+
LOCALSTACK_KEYCLOAK_REALM_FILE=/path/to/my-realm.json localstack start
131+
```
132+
133+
See [`quickstart/sample-realm.json`](quickstart/sample-realm.json) for a realm template and [`quickstart/README.md`](quickstart/README.md) for a step-by-step guide.
134+
135+
### Create Test Users
136+
137+
```bash
138+
# Auto-create a test user on startup (use LOCALSTACK_ prefix with CLI)
139+
LOCALSTACK_KEYCLOAK_DEFAULT_USER=testuser LOCALSTACK_KEYCLOAK_DEFAULT_PASSWORD=password123 localstack start
140+
```
141+
142+
## Default Client
143+
144+
The extension creates a default client `localstack-client` with:
145+
146+
- **Client Secret**: `localstack-client-secret`
147+
- **Flows**: Authorization Code, Client Credentials, Direct Access Grants
148+
- **Service Account Roles**: `admin`, `user`
149+
150+
The service account for `localstack-client` is automatically assigned the `admin` realm role, enabling full access when using client credentials flow.
151+
152+
## Sample Application
153+
154+
See the `sample-app/` directory for a complete example demonstrating:
155+
156+
- API Gateway with Lambda Authorizer
157+
- JWT validation with Keycloak
158+
- Role-based access control
159+
- DynamoDB user management
160+
161+
## Troubleshooting
162+
163+
### Keycloak takes a long time to start
164+
165+
Keycloak typically takes 30-60 seconds to fully start. The extension waits for the health check to pass before marking LocalStack as ready.
166+
167+
### Health check endpoint returns 404
168+
169+
In Keycloak 26+, the health endpoint is on port 9000:
170+
171+
```bash
172+
curl http://localhost:9000/health/ready
173+
```
174+
175+
### View Keycloak logs
176+
177+
```bash
178+
docker logs ls-ext-keycloak
179+
```
180+
181+
## Development
182+
183+
```bash
184+
# Install dependencies
185+
make install
186+
187+
# Run tests (requires LocalStack with extension running)
188+
make test
189+
190+
# Format code
191+
make format
192+
193+
# Lint
194+
make lint
195+
```
196+
197+
## License
198+
199+
Apache License 2.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name = "localstack_keycloak"

0 commit comments

Comments
 (0)