forked from Github-Mirrors/canaille
customize jwt claims with format string in config file
This commit is contained in:
parent
c8759fe088
commit
cefeac4e5b
5 changed files with 145 additions and 47 deletions
|
@ -138,17 +138,18 @@ EXP = 3600
|
|||
|
||||
[JWT.MAPPING]
|
||||
# Mapping between JWT fields and LDAP attributes from your
|
||||
# User objectClass. Default values fits inetOrgPerson.
|
||||
SUB = "uid"
|
||||
NAME = "cn"
|
||||
PHONE_NUMBER = "telephoneNumber"
|
||||
EMAIL = "mail"
|
||||
GIVEN_NAME = "givenName"
|
||||
FAMILY_NAME = "sn"
|
||||
PREFERRED_USERNAME = "displayName"
|
||||
LOCALE = "preferredLanguage"
|
||||
PICTURE = "jpegPhoto"
|
||||
ADDRESS = "postalAddress"
|
||||
# User objectClass.
|
||||
# {attribute} will be replaced by the user ldap attribute value.
|
||||
# Default values fits inetOrgPerson.
|
||||
SUB = "{uid}"
|
||||
NAME = "{cn}"
|
||||
PHONE_NUMBER = "{telephoneNumber}"
|
||||
EMAIL = "{mail}"
|
||||
GIVEN_NAME = "{givenName}"
|
||||
FAMILY_NAME = "{sn}"
|
||||
PREFERRED_USERNAME = "{displayName}"
|
||||
LOCALE = "{preferredLanguage}"
|
||||
ADDRESS = "{postalAddress}"
|
||||
|
||||
# The SMTP server options. If not set, mail related features such as
|
||||
# user invitations, and password reset emails, will be disabled.
|
||||
|
|
|
@ -17,6 +17,7 @@ from authlib.oidc.core.grants import (
|
|||
OpenIDHybridGrant as _OpenIDHybridGrant,
|
||||
)
|
||||
from authlib.oidc.core import UserInfo
|
||||
from collections import defaultdict
|
||||
from flask import current_app
|
||||
from .models import Client, AuthorizationCode, Token, User
|
||||
|
||||
|
@ -38,9 +39,9 @@ def get_jwt_config(grant):
|
|||
|
||||
def generate_user_info(user, scope):
|
||||
user = User.get(dn=user)
|
||||
fields = ["sub"]
|
||||
claims = ["sub"]
|
||||
if "profile" in scope:
|
||||
fields += [
|
||||
claims += [
|
||||
"name",
|
||||
"family_name",
|
||||
"given_name",
|
||||
|
@ -56,26 +57,40 @@ def generate_user_info(user, scope):
|
|||
"updated_at",
|
||||
]
|
||||
if "email" in scope:
|
||||
fields += ["email", "email_verified"]
|
||||
claims += ["email", "email_verified"]
|
||||
if "address" in scope:
|
||||
fields += ["address"]
|
||||
claims += ["address"]
|
||||
if "phone" in scope:
|
||||
fields += ["phone_number", "phone_number_verified"]
|
||||
claims += ["phone_number", "phone_number_verified"]
|
||||
if "groups" in scope:
|
||||
fields += ["groups"]
|
||||
claims += ["groups"]
|
||||
|
||||
data = generate_user_claims(user, claims)
|
||||
return UserInfo(**data)
|
||||
|
||||
|
||||
def generate_user_claims(user, claims, jwt_mapping_config=None):
|
||||
jwt_mapping_config = jwt_mapping_config or current_app.config["JWT"]["MAPPING"]
|
||||
|
||||
user_ldap_attributes = defaultdict(str)
|
||||
for attr in user.attrs:
|
||||
user_ldap_attributes[attr] = user.__getattr__(attr)
|
||||
if isinstance(user_ldap_attributes[attr], list):
|
||||
user_ldap_attributes[attr] = user_ldap_attributes[attr][0]
|
||||
|
||||
data = {}
|
||||
for field in fields:
|
||||
ldap_field_match = current_app.config["JWT"]["MAPPING"].get(field.upper())
|
||||
if ldap_field_match and ldap_field_match in user.attrs:
|
||||
data[field] = user.__getattr__(ldap_field_match)
|
||||
if isinstance(data[field], list):
|
||||
data[field] = data[field][0]
|
||||
if field == "groups":
|
||||
for claim in claims:
|
||||
format_claim = jwt_mapping_config.get(claim.upper())
|
||||
if format_claim:
|
||||
formatted_claim = format_claim.format_map(user_ldap_attributes)
|
||||
if formatted_claim:
|
||||
# According to https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
||||
# it's better to not insert a null or empty string value
|
||||
data[claim] = formatted_claim
|
||||
if claim == "groups":
|
||||
group_name_attr = current_app.config["LDAP"]["GROUP_NAME_ATTRIBUTE"]
|
||||
data[field] = [getattr(g, group_name_attr)[0] for g in user.groups]
|
||||
|
||||
return UserInfo(**data)
|
||||
data[claim] = [getattr(g, group_name_attr)[0] for g in user.groups]
|
||||
return data
|
||||
|
||||
|
||||
def save_authorization_code(code, request):
|
||||
|
|
|
@ -145,17 +145,18 @@ EXP = 3600
|
|||
|
||||
[JWT.MAPPING]
|
||||
# Mapping between JWT fields and LDAP attributes from your
|
||||
# User objectClass. Default values fits inetOrgPerson.
|
||||
SUB = "uid"
|
||||
NAME = "cn"
|
||||
PHONE_NUMBER = "telephoneNumber"
|
||||
EMAIL = "mail"
|
||||
GIVEN_NAME = "givenName"
|
||||
FAMILY_NAME = "sn"
|
||||
PREFERRED_USERNAME = "displayName"
|
||||
LOCALE = "preferredLanguage"
|
||||
PICTURE = "jpegPhoto"
|
||||
ADDRESS = "postalAddress"
|
||||
# User objectClass.
|
||||
# {attribute} will be replaced by the user ldap attribute value.
|
||||
# Default values fits inetOrgPerson.
|
||||
SUB = "{uid}"
|
||||
NAME = "{cn}"
|
||||
PHONE_NUMBER = "{telephoneNumber}"
|
||||
EMAIL = "{mail}"
|
||||
GIVEN_NAME = "{givenName}"
|
||||
FAMILY_NAME = "{sn}"
|
||||
PREFERRED_USERNAME = "{displayName}"
|
||||
LOCALE = "{preferredLanguage}"
|
||||
ADDRESS = "{postalAddress}"
|
||||
|
||||
# The SMTP server options. If not set, mail related features such as
|
||||
# user invitations, and password reset emails, will be disabled.
|
||||
|
|
|
@ -186,15 +186,14 @@ def configuration(slapd_server, smtpd, keypair_path):
|
|||
"KTY": "RSA",
|
||||
"EXP": 3600,
|
||||
"MAPPING": {
|
||||
"SUB": "uid",
|
||||
"NAME": "cn",
|
||||
"PHONE_NUMBER": "telephoneNumber",
|
||||
"EMAIL": "mail",
|
||||
"GIVEN_NAME": "givenName",
|
||||
"FAMILY_NAME": "sn",
|
||||
"PREFERRED_USERNAME": "displayName",
|
||||
"LOCALE": "preferredLanguage",
|
||||
"PICTURE": "jpegPhoto",
|
||||
"SUB": "{uid}",
|
||||
"NAME": "{cn}",
|
||||
"PHONE_NUMBER": "{telephoneNumber}",
|
||||
"EMAIL": "{mail}",
|
||||
"GIVEN_NAME": "{givenName}",
|
||||
"FAMILY_NAME": "{sn}",
|
||||
"PREFERRED_USERNAME": "{displayName}",
|
||||
"LOCALE": "{preferredLanguage}",
|
||||
},
|
||||
},
|
||||
"SMTP": {
|
||||
|
|
82
tests/test_oauth2utils.py
Normal file
82
tests/test_oauth2utils.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
from canaille.models import User
|
||||
from canaille.oauth2utils import generate_user_claims
|
||||
|
||||
STANDARD_CLAIMS = [
|
||||
"sub",
|
||||
"name",
|
||||
"given_name",
|
||||
"family_name",
|
||||
"middle_name",
|
||||
"nickname",
|
||||
"preferred_username",
|
||||
"profile",
|
||||
"picture",
|
||||
"website",
|
||||
"email",
|
||||
"email_verified",
|
||||
"gender",
|
||||
"birthdate",
|
||||
"zoneinfo",
|
||||
"locale",
|
||||
"phone_number",
|
||||
"phone_number_verified",
|
||||
"address",
|
||||
"updated_at",
|
||||
]
|
||||
DEFAULT_JWT_MAPPING_CONFIG = {
|
||||
"SUB": "{uid}",
|
||||
"NAME": "{cn}",
|
||||
"PHONE_NUMBER": "{telephoneNumber}",
|
||||
"EMAIL": "{mail}",
|
||||
"GIVEN_NAME": "{givenName}",
|
||||
"FAMILY_NAME": "{sn}",
|
||||
"PREFERRED_USERNAME": "{displayName}",
|
||||
"LOCALE": "{preferredLanguage}",
|
||||
}
|
||||
|
||||
|
||||
def test_generate_user_standard_claims_with_default_config(slapd_connection, user):
|
||||
User.ldap_object_classes(slapd_connection)
|
||||
|
||||
data = generate_user_claims(user, STANDARD_CLAIMS, DEFAULT_JWT_MAPPING_CONFIG)
|
||||
|
||||
assert data == {
|
||||
"name": "John (johnny) Doe",
|
||||
"family_name": "Doe",
|
||||
"email": "john@doe.com",
|
||||
"sub": "user",
|
||||
}
|
||||
|
||||
|
||||
def test_custom_config_format_claim_is_well_formated(slapd_connection, user):
|
||||
User.ldap_object_classes(slapd_connection)
|
||||
jwt_mapping_config = DEFAULT_JWT_MAPPING_CONFIG.copy()
|
||||
jwt_mapping_config["EMAIL"] = "{uid}@mydomain.tld"
|
||||
|
||||
data = generate_user_claims(user, STANDARD_CLAIMS, jwt_mapping_config)
|
||||
|
||||
assert data["email"] == "user@mydomain.tld"
|
||||
|
||||
|
||||
def test_claim_is_omitted_if_empty(slapd_connection, user):
|
||||
# According to https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
||||
# it's better to not insert a null or empty string value
|
||||
User.ldap_object_classes(slapd_connection)
|
||||
user.mail = ""
|
||||
user.save(slapd_connection)
|
||||
|
||||
data = generate_user_claims(user, STANDARD_CLAIMS, DEFAULT_JWT_MAPPING_CONFIG)
|
||||
|
||||
assert "email" not in data
|
||||
|
||||
|
||||
def test_custom_format_claim_is_formatted_with_empty_value_and_not_omitted(slapd_connection, user):
|
||||
# If the jwt mapping config is customized, it's not canaille's responsability to verify value consistency when one user attribute is not set or null.
|
||||
# Attribute field is left empty in the formatted string.
|
||||
User.ldap_object_classes(slapd_connection)
|
||||
jwt_mapping_config = DEFAULT_JWT_MAPPING_CONFIG.copy()
|
||||
jwt_mapping_config["EMAIL"] = "{givenName}@mydomain.tld"
|
||||
|
||||
data = generate_user_claims(user, STANDARD_CLAIMS, jwt_mapping_config)
|
||||
|
||||
assert data["email"] == "@mydomain.tld"
|
Loading…
Reference in a new issue