This repository contains a simple WSGI-based proxy server that handles HTTP Basic Authentication using Keycloak as the identity provider. It leverages Keycloak's access tokens to authenticate requests and forwards authenticated requests to an upstream server. The proxy supports both direct username/password authentication and token-based authentication via introspection.
sequenceDiagram
participant C as Client
participant P as Proxy
participant K as Keycloak
participant U as Upstream
%% Step 1: Unauthenticated Access
C->>P: Request (No Auth)
P-->>C: 401 Unauthorized (WWW-Authenticate: Basic)
%% Step 2: Token Issuance
C->>P: Request (Basic Auth: username/password)
P->>K: POST /token (credentials)
alt Token Issued
K-->>P: Access Token
else Invalid Credentials
K-->>P: Error
P-->>C: 401 Unauthorized (Invalid credentials)
end
%% Step 3: Successful Authentication
P->>U: Forward Request
U-->>P: Response
P-->>C: Response + Set-Cookie (token)
%% Step 4: Token Introspection
C->>P: Request (Basic Auth: __token__/token)
P->>K: POST /introspect (token)
alt Token Active
K-->>P: {active: true}
P->>U: Forward Request
U-->>P: Response
P-->>C: Response + Set-Cookie (token)
else Token Inactive
K-->>P: {active: false}
P-->>C: 401 Unauthorized (Invalid token)
end
%% Step 5: Token Validation
C->>P: Request (Cookie: token)
alt Token Valid (not expired)
P->>U: Forward Request
U-->>P: Response
P-->>C: Response
else Token Expired
P-->>C: 401 Unauthorized
end
The proxy server performs authentication as follows:
- Unauthenticated Access: On unauthenticated requests, it returns a
401 Unauthorizedresponse with aWWW-Authenticatechallenge to prompt the user for credentials. These credentials are expected to correspond to users configured in the Keycloak client. - Token Issuance: Using the provided credentials, the server attempts to issue an access token by calling the Keycloak token endpoint (
/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token). - Successful Authentication: If the token is issued successfully, the server forwards the request to the upstream server, sets a cookie (
Set-Cookieheader) containing the access token, and returns the upstream response. Subsequent requests are authenticated using this cookie until the token expires. - Token Introspection: Optionally, if the username matches a configurable value (
__token__by default), the server assumes the password is an already-issued Keycloak token. It introspects this token by calling the Keycloak introspection endpoint (/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token/introspect). This feature allows clients to use tokens directly without exposing plaintext passwords. - Token Validation: For requests with a valid cookie, the server checks the token's expiration (extracted from the JWT payload) to ensure it remains valid before proxying the request.
- Python 3.6+
- A running Keycloak instance with a configured realm, client, and users.
- An upstream service to proxy requests to.
- Docker (optional, for containerized deployment).
The proxy relies on the following environment variables for configuration:
| Variable | Description | Required | Default Value |
|---|---|---|---|
KEYCLOAK_BASE_URL |
Base URL of the Keycloak server (e.g., https://keycloak.mydomain.com). |
Yes | N/A |
KEYCLOAK_REALM |
Keycloak realm name. | Yes | N/A |
KEYCLOAK_CLIENT_ID |
Keycloak client ID for authentication. | Yes | N/A |
KEYCLOAK_CLIENT_SECRET |
Keycloak client secret for authentication. | Yes | N/A |
PROXY_UPSTREAM_URL |
URL of the upstream service to forward authenticated requests to. | Yes | N/A |
PROXY_AUTH_COOKIE_NAME |
Name of the cookie storing the access token. | Yes | N/A |
PROXY_AUTHORIZATION |
How to forward authorization. One of basic, bearer or None |
No | None |
PROXY_TOKEN_USERNAME |
Username indicating the password is a token for introspection. | No | __token__ |
-
Build the Docker Image:
$ docker build -t keycloak-basic-auth-proxy:0.1.0 . -
Run the Docker Container: Replace the placeholder values with your actual configuration:
$ docker run -d \ -p 8000:8000 \ -e KEYCLOAK_BASE_URL=https://keycloak.mydomain.com \ -e KEYCLOAK_REALM=<keycloak-realm> \ -e KEYCLOAK_CLIENT_ID=<keycloak-client-id> \ -e KEYCLOAK_CLIENT_SECRET=<keycloak-client-secret> \ -e PROXY_UPSTREAM_URL=http://myservice.mydomain.com \ -e PROXY_AUTH_COOKIE_NAME=myservice:auth_token \ keycloak-basic-auth-proxy:0.1.0- The proxy will listen on port
8000by default. - Ensure the upstream service (
PROXY_UPSTREAM_URL) is accessible from the container.
- The proxy will listen on port
-
Using Docker Compose: For a more complex setup (e.g., integrating with other services), refer to the example in
./examples/pypiserver/docker-compose.yaml.
-
Install Dependencies: The script uses only Python standard library modules, so no additional dependencies are required.
-
Set Environment Variables: Export the required variables in your shell:
$ export KEYCLOAK_BASE_URL=https://keycloak.mydomain.com $ export KEYCLOAK_REALM=<keycloak-realm> $ export KEYCLOAK_CLIENT_ID=<keycloak-client-id> $ export KEYCLOAK_CLIENT_SECRET=<keycloak-client-secret> $ export PROXY_UPSTREAM_URL=http://myservice.mydomain.com $ export PROXY_AUTH_COOKIE_NAME=myservice:auth_token
-
Run the Server: Start the server using the built-in WSGI server (for testing):
$ python proxy.py
The server will start on
0.0.0.0:8000. Note that this is a single-threaded server (wsgiref) and not suitable for production use.
For production, deploy the application behind a WSGI server like Gunicorn or uWSGI, and consider adding a reverse proxy (e.g., Nginx) for load balancing and SSL termination.
Example with Gunicorn:
$ pip install gunicorn
$ gunicorn --bind 0.0.0.0:8000 proxy:applicationThe proxy uses Python's logging module with the INFO level by default. Logs include timestamps, levels, and messages (e.g., errors from Keycloak requests). Logs are output to the console.
- The access token is stored in an
HttpOnlycookie to prevent client-side access via JavaScript. - Token expiration is validated on each request using the JWT
expclaim. - Ensure
KEYCLOAK_CLIENT_SECRETis kept secure and not exposed in logs or version control.
- The built-in
wsgirefserver is single-threaded and intended for testing only. - Error handling assumes a
502 Bad Gatewayresponse for upstream failures, which may need customization.
Feel free to submit issues or pull requests to enhance functionality, improve security, or add documentation.