Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions example_configs/keycloak_oidc/Dockerfile

This file was deleted.

94 changes: 86 additions & 8 deletions example_configs/keycloak_oidc/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,92 @@
# Running a Local Keycloak Instance for Authentication

1. In this directory, run `docker compose up`.
This example demonstrates how to set up authentication using Keycloak (or any OAuth2-compliant provider). Two clients require authentication:
1. Tiled CLI (command-line client)
2. Tiled Web UI (FastAPI server and frontend)

This will start three services defined in the Docker Compose file:
- **Keycloak**: Handles authentication.
- **oauth2-proxy**: Acts as a proxy to authenticate users.
## Tiled CLI Authentication

2. Start the Tiled server using the configuration file located at `example_configs/keycloak_oidc/config.yaml`.
3. Open your browser and go to [http://localhost:4180](http://localhost:4180) (served by oauth2-proxy). You will be prompted to log in. Use `admin` for both the username and password.
The Tiled CLI uses the [device authorization flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow):

4. After logging in as `admin`, you will have access to all resources.
```mermaid
sequenceDiagram
actor User
participant CLI as Tiled CLI
participant Server as Tiled Server
participant IdP as Keycloak

> **Note:** This configuration exposes all secrets and passwords to make it easier to use as an example. **Do not use this setup in production.**
User->>CLI: client.login()
CLI->>Server: Request auth configuration (/api/v1)
Server-->>CLI: Device flow endpoints (client_id, auth-endpoint, token_endpoint)
CLI->>IdP: POST /device (client_id, scopes)
IdP-->>CLI: device_code, user_code, verification_uri, interval
CLI->>User: "Visit verification URL and enter user code"

par User Authentication
User->>IdP: Open verification URL and authenticate
IdP-->>User: Login successful
and CLI Polling
CLI->>IdP: Poll /token with device_code
IdP-->>CLI: "authorization_pending" (repeat until login)
end

IdP-->>CLI: access_token, refresh_token
CLI->>CLI: Store tokens (~/.cache/tiled)
CLI-->>User: "You have logged in with Proxied OIDC as external user."
```

After login, subsequent requests include the access token in the Authorization header. When the token expires (1-minute validity), the CLI automatically refreshes it. You must create a public client in Keycloak with OAuth 2.0 Device Authorization Grant enabled (named `tiled-cli` in this example).

## Tiled Web UI Authentication

The Tiled Web UI uses [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy/) as a reverse proxy to the Tiled server. OAuth2 Proxy handles all session management, including login, logout, and access token refresh.
The web server uses a confidential Keycloak client (named `tiled` in this example).
To logout, users are redirected to the Keycloak end_session_endpoint via OAuth2 Proxy's sign-out handler:

```
http://localhost:4180/oauth2/sign_out?rd=http%3A%2F%2Flocalhost%3A8080%2Frealms%2Fmaster%2Fprotocol%2Fopenid-connect%2Flogout

# The logout URL can be build as follow:-
end_session_endpoint = "http://localhost:8080/realms/master/protocol/openid-connect/logout"
# end_session_endpoint can be found at http://localhost:8080/realms/master/.well-known/openid-configuration
encoded_url = urllib.parse.quote_plus(end_session_endpoint)

logout_url= "http://localhost:4180" + "oauth2/sign_out?rd=" + encoded_url
```

For better user experience, you can configure a `/logout` endpoint to redirect to this URL automatically.

```mermaid
sequenceDiagram
actor User
participant OAuth2Proxy as OAuth2 Proxy
participant Keycloak
participant Tiled

User->>OAuth2Proxy: Request access to application
OAuth2Proxy->>Keycloak: Redirect user for authentication
activate Keycloak
Keycloak-->>OAuth2Proxy: Return JWT Access Token
deactivate Keycloak
OAuth2Proxy->>Tiled: Forward request with JWT Access Token
Tiled->>User: Provide resources if authenticated
```

## Getting Started

1. Run `docker compose up` in this directory. This starts:
- **Keycloak**: Identity Provider (IdP)
- **OAuth2-proxy**: Reverse-proxy to access OAuth2 secured Tiled

2. Start the Tiled server with `tiled server config example_configs/keycloak_oidc/config.yaml`. Tiled will make a call to Keycloak at startup to get the .well-known/openid-configuration.

3. Open http://localhost:4180 (OAuth2 proxy address) in your browser and log in with:
- Username: `admin`
- Password: `admin`

4. After authentication, you'll access all resources. Three additional test users are also available:
- **alice** (password: alice)
- **bob** (password: bob)
- **carol** (password: carol)

> **Note:** This example exposes secrets and passwords for demonstration only. **Do not use in production.**
19 changes: 8 additions & 11 deletions example_configs/keycloak_oidc/compose.yaml
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
services:

keycloak:
build:
context: .
dockerfile: Dockerfile
image: keycloak/keycloak:26.4
environment:
- KC_BOOTSTRAP_ADMIN_PASSWORD=admin
- KC_BOOTSTRAP_ADMIN_USERNAME=admin
- KC_HEALTH_ENABLED=true
- KC_METRICS_ENABLED=true
command: ["start-dev", "--import-realm"]
command: ["start-dev"]
volumes:
- ./realm-export.json:/opt/keycloak/data/import/realm-export.json
- ./keycloak_config/:/mnt
post_start:
- command: bash /mnt/startup.sh
ports:
- 8080:8080
- 9000:9000
healthcheck:
test: "curl --head -fsS http://localhost:9000/health/ready"
test: /opt/keycloak/bin/kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin --password admin
interval: 5s
timeout: 5s
retries: 10
start_period: 12s
start_period: 30s

oauth2-proxy:
network_mode: host
image: "quay.io/oauth2-proxy/oauth2-proxy"
image: "quay.io/oauth2-proxy/oauth2-proxy:v7.13.0"
volumes:
- ./oauth2-proxy.cfg:/opt/oauth2-proxy.cfg
- ./oauth2-alpha.yaml:/opt/oauth2-alpha.yaml
Expand Down
28 changes: 28 additions & 0 deletions example_configs/keycloak_oidc/keycloak_config/startup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash
export PATH=$PATH:/opt/keycloak/bin

# Wait for Keycloak to start up
sleep 30
while ! kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin --password admin; do
sleep 1
done

# Add users to Keycloak
for user in alice bob carol; do
kcadm.sh create users -r master -s username="$user" -s enabled=true
kcadm.sh set-password -r master --username "$user" --new-password "$user"
done

# Retrieve all allowed protocol mappers for client registration
allowed_protocol_mappers=$(kcadm.sh get components -q name="Allowed Protocol Mapper Types" --fields id --format csv --noquotes)

# Enable oidc-audience-mapper for all allowed protocol mappers to support tiled client registration
for mapper_id in $allowed_protocol_mappers; do
kcadm.sh update components/$mapper_id -s 'config.allowed-protocol-mapper-types=[ "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-audience-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper" ]'
done

kcreg.sh config credentials --server http://localhost:8080 --realm master --user admin --password admin

for client in tiled-cli tiled; do
kcreg.sh get $client >/dev/null 2>&1 || kcreg.sh create --file "/mnt/$client.json"
done
65 changes: 65 additions & 0 deletions example_configs/keycloak_oidc/keycloak_config/tiled-cli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"id" : "194bca53-a569-4411-925e-217d8d6d21cf",
"clientId" : "tiled-cli",
"name" : "",
"description" : "Tiled client for Device flow login ",
"rootUrl" : "",
"adminUrl" : "",
"baseUrl" : "",
"surrogateAuthRequired" : false,
"enabled" : true,
"alwaysDisplayInConsole" : false,
"clientAuthenticatorType" : "client-secret",
"defaultRoles" : [ ],
"redirectUris" : [ "/*" ],
"webOrigins" : [ "/*" ],
"notBefore" : 0,
"bearerOnly" : false,
"consentRequired" : false,
"standardFlowEnabled" : false,
"implicitFlowEnabled" : false,
"directAccessGrantsEnabled" : false,
"serviceAccountsEnabled" : false,
"publicClient" : true,
"frontchannelLogout" : true,
"protocol" : "openid-connect",
"attributes" : {
"standard.token.exchange.enabled" : "false",
"frontchannel.logout.session.required" : "true",
"post.logout.redirect.uris" : "+",
"oauth2.device.authorization.grant.enabled" : "true",
"backchannel.logout.revoke.offline.tokens" : "false",
"use.refresh.tokens" : "true",
"realm_client" : "false",
"oidc.ciba.grant.enabled" : "false",
"client.use.lightweight.access.token.enabled" : "false",
"backchannel.logout.session.required" : "true",
"client_credentials.use_refresh_token" : "false",
"tls.client.certificate.bound.access.tokens" : "false",
"require.pushed.authorization.requests" : "false",
"acr.loa.map" : "{}",
"display.on.consent.screen" : "false",
"token.response.type.bearer.lower-case" : "false"
},
"authenticationFlowBindingOverrides" : { },
"fullScopeAllowed" : true,
"nodeReRegistrationTimeout" : -1,
"protocolMappers" : [ {
"id" : "3a57ab6a-332e-4831-8583-820ae22cb830",
"name" : "tiled_aud",
"protocol" : "openid-connect",
"protocolMapper" : "oidc-audience-mapper",
"consentRequired" : false,
"config" : {
"id.token.claim" : "false",
"lightweight.claim" : "false",
"introspection.token.claim" : "true",
"access.token.claim" : "true",
"included.custom.audience" : "tiled_aud",
"userinfo.token.claim" : "false"
}
}
],
"defaultClientScopes" : [ "web-origins", "acr", "offline_access", "roles", "profile", "basic", "email" ],
"optionalClientScopes" : [ "address", "phone", "microprofile-jwt" ]
}
73 changes: 73 additions & 0 deletions example_configs/keycloak_oidc/keycloak_config/tiled.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"id" : "96265cb5-8046-4e06-9095-37ecf65d5bf6",
"clientId" : "tiled",
"name" : "Tiled",
"description" : "Standard flow client used for tiled authentication using oauth2-proxy",
"rootUrl" : "http://localhost:4180",
"adminUrl" : "http://localhost:4180",
"baseUrl" : "http://localhost:4180",
"surrogateAuthRequired" : false,
"enabled" : true,
"alwaysDisplayInConsole" : false,
"clientAuthenticatorType" : "client-secret",
"secret" : "secret",
"defaultRoles" : [ ],
"redirectUris" : [ "http://localhost:4180/*" ],
"webOrigins" : [ "http://localhost:4180/*" ],
"notBefore" : 0,
"bearerOnly" : false,
"consentRequired" : false,
"standardFlowEnabled" : true,
"implicitFlowEnabled" : false,
"directAccessGrantsEnabled" : false,
"serviceAccountsEnabled" : false,
"publicClient" : false,
"frontchannelLogout" : true,
"protocol" : "openid-connect",
"attributes" : {
"client.secret.creation.time" : "1756805685",
"request.object.signature.alg" : "any",
"request.object.encryption.alg" : "any",
"client.introspection.response.allow.jwt.claim.enabled" : "false",
"standard.token.exchange.enabled" : "false",
"post.logout.redirect.uris" : "http://localhost:4180/*",
"frontchannel.logout.session.required" : "true",
"oauth2.device.authorization.grant.enabled" : "false",
"use.jwks.url" : "false",
"backchannel.logout.revoke.offline.tokens" : "false",
"use.refresh.tokens" : "true",
"realm_client" : "false",
"oidc.ciba.grant.enabled" : "false",
"client.use.lightweight.access.token.enabled" : "false",
"backchannel.logout.session.required" : "true",
"request.object.required" : "not required",
"client_credentials.use_refresh_token" : "false",
"access.token.header.type.rfc9068" : "false",
"tls.client.certificate.bound.access.tokens" : "false",
"require.pushed.authorization.requests" : "false",
"acr.loa.map" : "{}",
"display.on.consent.screen" : "false",
"request.object.encryption.enc" : "any",
"token.response.type.bearer.lower-case" : "false"
},
"authenticationFlowBindingOverrides" : { },
"fullScopeAllowed" : true,
"nodeReRegistrationTimeout" : -1,
"protocolMappers" : [ {
"id" : "af58bf06-d7d3-4046-80eb-f58d5ae35aca",
"name" : "tiled_aud",
"protocol" : "openid-connect",
"protocolMapper" : "oidc-audience-mapper",
"consentRequired" : false,
"config" : {
"id.token.claim" : "false",
"lightweight.claim" : "false",
"introspection.token.claim" : "true",
"access.token.claim" : "true",
"included.custom.audience" : "tiled_aud",
"userinfo.token.claim" : "false"
}
} ],
"defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
}
3 changes: 2 additions & 1 deletion example_configs/keycloak_oidc/oauth2-alpha.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ injectRequestHeaders:
providers:
- provider: oidc
clientID: tiled
clientSecret: o8b7mIuYmzybNPVS8SSduhMdeqM8NDLp
clientSecret: secret
id: authn
oidcConfig:
audienceClaims: ["aud"]
extraAudiences: ["tiled_aud"]
emailClaim: sub
insecureAllowUnverifiedEmail: true
insecureSkipNonce: true
issuerURL: http://localhost:8080/realms/master
server:
BindAddress: localhost:4180
Expand Down
Loading
Loading