From 98719022234bd0289c60152330fe51b847c1c49c Mon Sep 17 00:00:00 2001 From: Roberto Polli Date: Mon, 22 Mar 2021 11:47:45 +0100 Subject: [PATCH 1/2] Simple keycloak --- gateways/keycloak/jwt-client.json | 110 ++++++++++++++++++++++++++++++ gateways/keycloak/test_jwt.py | 84 +++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 gateways/keycloak/jwt-client.json create mode 100644 gateways/keycloak/test_jwt.py diff --git a/gateways/keycloak/jwt-client.json b/gateways/keycloak/jwt-client.json new file mode 100644 index 0000000..03dd0da --- /dev/null +++ b/gateways/keycloak/jwt-client.json @@ -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 + } +} diff --git a/gateways/keycloak/test_jwt.py b/gateways/keycloak/test_jwt.py new file mode 100644 index 0000000..5dd0ef3 --- /dev/null +++ b/gateways/keycloak/test_jwt.py @@ -0,0 +1,84 @@ +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 + +keypair_pem = Path("jwks.pem") + + +realm = "ioggstream" +client_id = "jwt-client" +client_secret = "b90d8bd8-22d6-4b7b-b8b1-eeea7821e7b2" +password = "test" + +url = f"http://localhost:8080/auth/realms/{realm}/protocol/openid-connect/token" + +headers = {"Content-Type": "application/x-www-form-urlencoded"} + + +def harn_request_token(formdata): + ret = post(url=url, data=formdata, headers=headers) + print(ret.content) + + t = ret.json()["access_token"] + d = yaml.load(b64_decode((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"}), + json_encode({"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": "antani", + "sub": client_id, + "jti": str(uuid4()), + "aud": "http://localhost:8080/auth/realms/ioggstream", + }, + keypair=keypair, + ) + formdata = { + "grant_type": "client_credentials", + "client_id": "myclient", + # "scope": "email profile", + # "resource": "https://localhost:8080/", + "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, + "scope": "email", + "resource": "https://localhost:8080/" + # "username": "ioggstream", + # "password": "test", + } + harn_request_token(formdata) From 50bf18938274d43936efd131f33b46972fe5d554 Mon Sep 17 00:00:00 2001 From: Roberto Polli Date: Wed, 7 Apr 2021 16:56:38 +0200 Subject: [PATCH 2/2] add keycloak test --- gateways/keycloak/.gitignore | 3 +++ gateways/keycloak/README.md | 14 +++++++++++ gateways/keycloak/test_jwt.py | 47 ++++++++++++++++++++++++++--------- 3 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 gateways/keycloak/.gitignore create mode 100644 gateways/keycloak/README.md diff --git a/gateways/keycloak/.gitignore b/gateways/keycloak/.gitignore new file mode 100644 index 0000000..3551274 --- /dev/null +++ b/gateways/keycloak/.gitignore @@ -0,0 +1,3 @@ +.idea +jwks.json +jwks.pem diff --git a/gateways/keycloak/README.md b/gateways/keycloak/README.md new file mode 100644 index 0000000..7502131 --- /dev/null +++ b/gateways/keycloak/README.md @@ -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 diff --git a/gateways/keycloak/test_jwt.py b/gateways/keycloak/test_jwt.py index 5dd0ef3..a7ec3bd 100644 --- a/gateways/keycloak/test_jwt.py +++ b/gateways/keycloak/test_jwt.py @@ -7,6 +7,10 @@ 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") @@ -14,19 +18,42 @@ realm = "ioggstream" client_id = "jwt-client" client_secret = "b90d8bd8-22d6-4b7b-b8b1-eeea7821e7b2" -password = "test" - -url = f"http://localhost:8080/auth/realms/{realm}/protocol/openid-connect/token" +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.load(b64_decode((t.split(".")[1] + "===").encode())) + d = yaml.safe_load(b64decode((t.split(".")[1] + "===").encode())) print(yaml.dump(d, indent=True)) @@ -39,8 +66,7 @@ def create_jwt(payload, keypair, kid=None): token.add_signature( keypair, None, - json_encode({"alg": "RS256"}), - json_encode({"kid": keypair.thumbprint()}), + json_encode({"alg": "RS256", "kid": keypair.thumbprint()}), ) sig = token.serialize(compact=True) return sig @@ -51,18 +77,17 @@ def test_client_jwt(): client_assertion = create_jwt( { - "iss": "antani", + "iss": client_id, "sub": client_id, "jti": str(uuid4()), - "aud": "http://localhost:8080/auth/realms/ioggstream", + "aud": realm_url, }, keypair=keypair, ) formdata = { "grant_type": "client_credentials", - "client_id": "myclient", + "client_id": client_id, # "scope": "email profile", - # "resource": "https://localhost:8080/", "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion": client_assertion, } @@ -76,8 +101,6 @@ def test_client_secret(): "grant_type": "client_credentials", "client_id": client_id, "client_secret": client_secret, - "scope": "email", - "resource": "https://localhost:8080/" # "username": "ioggstream", # "password": "test", }