forked from Github-Mirrors/canaille
Use private/public keys to sign JWTs
This commit is contained in:
parent
1f225e92bf
commit
0ae8a5a0f5
8 changed files with 76 additions and 25 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -16,3 +16,6 @@ dist
|
|||
python-ldap-test*
|
||||
conf/oauth-authorization-server.json
|
||||
conf/openid-configuration.json
|
||||
conf/*.pem
|
||||
conf/*.pub
|
||||
conf/*.key
|
||||
|
|
|
@ -25,9 +25,10 @@ USER_FILTER = "(|(uid={login})(cn={login}))"
|
|||
ADMIN_FILTER = "cn=Jane Doe"
|
||||
|
||||
[JWT]
|
||||
KEY = "secret-key"
|
||||
ALG = "HS256"
|
||||
ISS = "http://mydomain.tld"
|
||||
PUBLIC_KEY = "conf/public.pem"
|
||||
PRIVATE_KEY = "conf/private.pem"
|
||||
KTY = "RSA"
|
||||
ALG = "RS256"
|
||||
EXP = 3600
|
||||
|
||||
[JWT.MAPPING]
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
["client_secret_basic", "private_key_jwt",
|
||||
"client_secret_post", "none"],
|
||||
"token_endpoint_auth_signing_alg_values_supported":
|
||||
["RS256", "ES256"],
|
||||
["RS256"],
|
||||
"userinfo_endpoint":
|
||||
"https://mydomain.tld/oauth/userinfo",
|
||||
"check_session_iframe":
|
||||
|
|
|
@ -3,6 +3,9 @@ import ldap.ldapobject
|
|||
import os
|
||||
import pytest
|
||||
import slapdtest
|
||||
from cryptography.hazmat.primitives import serialization as crypto_serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.backends import default_backend as crypto_default_backend
|
||||
from flask_webtest import TestApp
|
||||
from werkzeug.security import gen_salt
|
||||
from web import create_app
|
||||
|
@ -28,6 +31,37 @@ class CustomSlapdObject(slapdtest.SlapdObject):
|
|||
return config
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def keypair():
|
||||
key = rsa.generate_private_key(
|
||||
backend=crypto_default_backend(), public_exponent=65537, key_size=2048
|
||||
)
|
||||
private_key = key.private_bytes(
|
||||
crypto_serialization.Encoding.PEM,
|
||||
crypto_serialization.PrivateFormat.PKCS8,
|
||||
crypto_serialization.NoEncryption(),
|
||||
)
|
||||
public_key = key.public_key().public_bytes(
|
||||
crypto_serialization.Encoding.OpenSSH, crypto_serialization.PublicFormat.OpenSSH
|
||||
)
|
||||
return private_key, public_key
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def keypair_path(keypair, tmp_path):
|
||||
private_key, public_key = keypair
|
||||
|
||||
private_key_path = os.path.join(tmp_path, "private.pem")
|
||||
with open(private_key_path, "wb") as fd:
|
||||
fd.write(private_key)
|
||||
|
||||
public_key_path = os.path.join(tmp_path, "public.pem")
|
||||
with open(public_key_path, "wb") as fd:
|
||||
fd.write(public_key)
|
||||
|
||||
return private_key_path, public_key_path
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def slapd_server():
|
||||
slapd = CustomSlapdObject()
|
||||
|
@ -75,15 +109,15 @@ def slapd_connection(slapd_server):
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def app(slapd_server):
|
||||
def app(slapd_server, keypair_path):
|
||||
os.environ["AUTHLIB_INSECURE_TRANSPORT"] = "true"
|
||||
private_key_path, public_key_path = keypair_path
|
||||
|
||||
app = create_app(
|
||||
{
|
||||
"SECRET_KEY": gen_salt(24),
|
||||
"OAUTH2_METADATA_FILE": "conf/oauth-authorization-server.sample.json",
|
||||
"OIDC_METADATA_FILE": "conf/openid-configuration.sample.json",
|
||||
|
||||
"LDAP": {
|
||||
"ROOT_DN": slapd_server.suffix,
|
||||
"URI": slapd_server.ldap_uri,
|
||||
|
@ -93,9 +127,10 @@ def app(slapd_server):
|
|||
"ADMIN_FILTER": "uid=admin",
|
||||
},
|
||||
"JWT": {
|
||||
"KEY": "secret-key",
|
||||
"ALG": "HS256",
|
||||
"ISS": "https://mydomain.tld",
|
||||
"PUBLIC_KEY": public_key_path,
|
||||
"PRIVATE_KEY": private_key_path,
|
||||
"ALG": "RS256",
|
||||
"KTY": "RSA",
|
||||
"EXP": 3600,
|
||||
"MAPPING": {
|
||||
"SUB": "uid",
|
||||
|
|
|
@ -42,7 +42,7 @@ def test_oauth_hybrid(testclient, slapd_connection, user, client):
|
|||
assert {"foo": "bar"} == res.json
|
||||
|
||||
|
||||
def test_oidc_hybrid(testclient, slapd_connection, logged_user, client):
|
||||
def test_oidc_hybrid(testclient, slapd_connection, logged_user, client, keypair):
|
||||
res = testclient.get(
|
||||
"/oauth/authorize",
|
||||
params=dict(
|
||||
|
@ -69,7 +69,7 @@ def test_oidc_hybrid(testclient, slapd_connection, logged_user, client):
|
|||
assert token is not None
|
||||
|
||||
id_token = params["id_token"][0]
|
||||
claims = jwt.decode(id_token, "secret-key")
|
||||
claims = jwt.decode(id_token, keypair[1])
|
||||
assert logged_user.uid[0] == claims["sub"]
|
||||
assert logged_user.cn[0] == claims["name"]
|
||||
assert [client.oauthClientID] == claims["aud"]
|
||||
|
|
|
@ -47,7 +47,7 @@ def test_oauth_implicit(testclient, slapd_connection, user, client):
|
|||
client.save(slapd_connection)
|
||||
|
||||
|
||||
def test_oidc_implicit(testclient, slapd_connection, user, client):
|
||||
def test_oidc_implicit(testclient, keypair, slapd_connection, user, client):
|
||||
client.oauthGrantType = ["token id_token"]
|
||||
client.oauthTokenEndpointAuthMethod = "none"
|
||||
|
||||
|
@ -83,7 +83,7 @@ def test_oidc_implicit(testclient, slapd_connection, user, client):
|
|||
assert token is not None
|
||||
|
||||
id_token = params["id_token"][0]
|
||||
claims = jwt.decode(id_token, "secret-key")
|
||||
claims = jwt.decode(id_token, keypair[1])
|
||||
assert user.uid[0] == claims["sub"]
|
||||
assert user.cn[0] == claims["name"]
|
||||
assert [client.oauthClientID] == claims["aud"]
|
||||
|
|
21
web/oauth.py
21
web/oauth.py
|
@ -1,4 +1,4 @@
|
|||
from authlib.common.encoding import urlsafe_b64encode
|
||||
from authlib.jose import jwk
|
||||
from authlib.oauth2 import OAuth2Error
|
||||
from flask import Blueprint, request, session, redirect
|
||||
from flask import render_template, jsonify, flash, current_app
|
||||
|
@ -71,8 +71,19 @@ def revoke_token():
|
|||
|
||||
@bp.route("/jwks.json")
|
||||
def jwks():
|
||||
# TODO: Do not share secrets here!
|
||||
key = urlsafe_b64encode(current_app.config["JWT"]["KEY"].encode("utf-8")).decode(
|
||||
"utf-8"
|
||||
with open(current_app.config["JWT"]["PUBLIC_KEY"]) as fd:
|
||||
pubkey = fd.read()
|
||||
|
||||
obj = jwk.dumps(pubkey, current_app.config["JWT"]["KTY"])
|
||||
return jsonify(
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kid": None,
|
||||
"use": "sig",
|
||||
"alg": current_app.config["JWT"]["ALG"],
|
||||
**obj,
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
return jsonify({"keys": [{"kid": None, "kty": "oct", "k": key}]})
|
||||
|
|
|
@ -28,12 +28,13 @@ def exists_nonce(nonce, req):
|
|||
|
||||
|
||||
def get_jwt_config(grant):
|
||||
return {
|
||||
"key": current_app.config["JWT"]["KEY"],
|
||||
"alg": current_app.config["JWT"]["ALG"],
|
||||
"iss": current_app.config["JWT"]["ISS"],
|
||||
"exp": current_app.config["JWT"]["EXP"],
|
||||
}
|
||||
with open(current_app.config["JWT"]["PRIVATE_KEY"]) as pk:
|
||||
return {
|
||||
"key": pk.read(),
|
||||
"alg": current_app.config["JWT"]["ALG"],
|
||||
"iss": authorization.metadata["issuer"],
|
||||
"exp": current_app.config["JWT"]["EXP"],
|
||||
}
|
||||
|
||||
|
||||
def generate_user_info(user, scope):
|
||||
|
@ -253,7 +254,7 @@ class IntrospectionEndpoint(_IntrospectionEndpoint):
|
|||
"scope": token.get_scope(),
|
||||
"sub": token.oauthSubject,
|
||||
"aud": token.oauthClientID,
|
||||
"iss": current_app.config["JWT"]["ISS"],
|
||||
"iss": authorization.metadata["issuer"],
|
||||
"exp": token.get_expires_at(),
|
||||
"iat": token.get_issued_at(),
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue