Skip to content
Open
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
3 changes: 3 additions & 0 deletions gateways/keycloak/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea
jwks.json
jwks.pem
14 changes: 14 additions & 0 deletions gateways/keycloak/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Simple Keycloak JWT-Bearer configuration

This configuration shows how to create a keycloak authentication
with jwt-bearer.

1- create a keypair
2- publish your public key on the web via https
in jwks format and with an `use` claim
3- configure your keycloak application to authenticate
via jwt-bearer using the sample configuration
4- request a token signing the request with your
private key
5- get the token
6- consume the token
110 changes: 110 additions & 0 deletions gateways/keycloak/jwt-client.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{
"clientId": "jwt-client",
"name": "JWT Client",
"description": "Sample JWT Client configuration.",
"rootUrl": "https://www.keycloak.org/app/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-jwt",
"redirectUris": [],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": false,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": true,
"authorizationServicesEnabled": true,
"publicClient": false,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"access.token.lifespan": "60",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"token.endpoint.auth.signing.alg": "RS256",
"use.jwks.url": "true",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "true",
"backchannel.logout.session.required": "false",
"client_credentials.use_refresh_token": "false",
"jwks.url": "https://www.robertopolli.it/doc/jwks.json",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {
"direct_grant": "619c6bcc-1740-47ff-9e7c-c577162e99f2"
},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"protocolMappers": [
{
"name": "Client IP Address",
"protocol": "openid-connect",
"protocolMapper": "oidc-usersessionmodel-note-mapper",
"consentRequired": false,
"config": {
"user.session.note": "clientAddress",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "clientAddress",
"jsonType.label": "String"
}
},
{
"name": "Client Host",
"protocol": "openid-connect",
"protocolMapper": "oidc-usersessionmodel-note-mapper",
"consentRequired": false,
"config": {
"user.session.note": "clientHost",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "clientHost",
"jsonType.label": "String"
}
},
{
"name": "Client ID",
"protocol": "openid-connect",
"protocolMapper": "oidc-usersessionmodel-note-mapper",
"consentRequired": false,
"config": {
"user.session.note": "clientId",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "client_id",
"jsonType.label": "String",
"access.tokenResponse.claim": "true"
}
}
],
"defaultClientScopes": [
"web-origins",
"role_list",
"roles",
"profile",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
107 changes: 107 additions & 0 deletions gateways/keycloak/test_jwt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import json
from os import environ
from pathlib import Path
from time import time
from uuid import uuid4

from jwcrypto import jwk, jws
from jwcrypto.common import json_encode
from requests import get, post
import yaml
from base64 import b64decode
from http.server import BaseHTTPRequestHandler, HTTPServer
from multiprocessing import Process

keypair_pem = Path("jwks.pem")


realm = "ioggstream"
client_id = "jwt-client"
client_secret = "b90d8bd8-22d6-4b7b-b8b1-eeea7821e7b2"
password = "secret"

realm_url = f"http://localhost:8080/auth/realms/{realm}"
url = f"{realm_url}/protocol/openid-connect/token"
headers = {"Content-Type": "application/x-www-form-urlencoded"}


def create_keypair_pem():
kp = jwk.JWK.generate(kty="RSA", size=4096)
keypair_pem.write_bytes(
kp.export_to_pem(private_key=True, password=None) + kp.export_to_pem()
)


def create_jwks(keypair, overwrite=False):
keypair_json = Path("jwks.json")
if keypair_json.exists() and not overwrite:
raise OSError(f"File already exists {keypair_json}")
jwks = {"keys": []}
my_key = keypair.export_public(as_dict=True)
my_key["use"] = "sig" # Keycloak wants a restricted key scope.
jwks["keys"].append(my_key)
keypair_json.write_text(json.dumps(jwks))


def setup_enviroment():
create_keypair_pem()
create_jwks()


def harn_request_token(formdata):
ret = post(url=url, data=formdata, headers=headers)
print(ret.content)

t = ret.json()["access_token"]
d = yaml.safe_load(b64decode((t.split(".")[1] + "===").encode()))
print(yaml.dump(d, indent=True))


def create_jwt(payload, keypair, kid=None):
now = int(time())
payload.update(
{"iat": now - 1, "exp": now + 1000,}
)
token = jws.JWS(json_encode(payload))
token.add_signature(
keypair,
None,
json_encode({"alg": "RS256", "kid": keypair.thumbprint()}),
)
sig = token.serialize(compact=True)
return sig


def test_client_jwt():
keypair = jwk.JWK.from_pem(Path("jwks.pem").read_bytes())

client_assertion = create_jwt(
{
"iss": client_id,
"sub": client_id,
"jti": str(uuid4()),
"aud": realm_url,
},
keypair=keypair,
)
formdata = {
"grant_type": "client_credentials",
"client_id": client_id,
# "scope": "email profile",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": client_assertion,
}
print(formdata)
harn_request_token(formdata)


def test_client_secret():

formdata = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
# "username": "ioggstream",
# "password": "test",
}
harn_request_token(formdata)