Support for scraping email addresses from OIDC providers (#9245)

This commit is contained in:
Richard van der Hoff 2021-01-27 21:28:59 +00:00 committed by GitHub
parent fbd9de6d1f
commit 869667760f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 53 additions and 30 deletions

1
changelog.d/9245.feature Normal file
View File

@ -0,0 +1 @@
Add support to the OpenID Connect integration for adding the user's email address.

View File

@ -1791,9 +1791,9 @@ saml2_config:
# #
# For the default provider, the following settings are available: # For the default provider, the following settings are available:
# #
# sub: name of the claim containing a unique identifier for the # subject_claim: name of the claim containing a unique identifier
# user. Defaults to 'sub', which OpenID Connect compliant # for the user. Defaults to 'sub', which OpenID Connect
# providers should provide. # compliant providers should provide.
# #
# localpart_template: Jinja2 template for the localpart of the MXID. # localpart_template: Jinja2 template for the localpart of the MXID.
# If this is not set, the user will be prompted to choose their # If this is not set, the user will be prompted to choose their
@ -1802,6 +1802,9 @@ saml2_config:
# display_name_template: Jinja2 template for the display name to set # display_name_template: Jinja2 template for the display name to set
# on first login. If unset, no displayname will be set. # on first login. If unset, no displayname will be set.
# #
# email_template: Jinja2 template for the email address of the user.
# If unset, no email address will be added to the account.
#
# extra_attributes: a map of Jinja2 templates for extra attributes # extra_attributes: a map of Jinja2 templates for extra attributes
# to send back to the client during login. # to send back to the client during login.
# Note that these are non-standard and clients will ignore them # Note that these are non-standard and clients will ignore them
@ -1837,6 +1840,12 @@ oidc_providers:
# userinfo_endpoint: "https://accounts.example.com/userinfo" # userinfo_endpoint: "https://accounts.example.com/userinfo"
# jwks_uri: "https://accounts.example.com/.well-known/jwks.json" # jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
# skip_verification: true # skip_verification: true
# user_mapping_provider:
# config:
# subject_claim: "id"
# localpart_template: "{ user.login }"
# display_name_template: "{ user.name }"
# email_template: "{ user.email }"
# For use with Keycloak # For use with Keycloak
# #

View File

@ -143,9 +143,9 @@ class OIDCConfig(Config):
# #
# For the default provider, the following settings are available: # For the default provider, the following settings are available:
# #
# sub: name of the claim containing a unique identifier for the # subject_claim: name of the claim containing a unique identifier
# user. Defaults to 'sub', which OpenID Connect compliant # for the user. Defaults to 'sub', which OpenID Connect
# providers should provide. # compliant providers should provide.
# #
# localpart_template: Jinja2 template for the localpart of the MXID. # localpart_template: Jinja2 template for the localpart of the MXID.
# If this is not set, the user will be prompted to choose their # If this is not set, the user will be prompted to choose their
@ -154,6 +154,9 @@ class OIDCConfig(Config):
# display_name_template: Jinja2 template for the display name to set # display_name_template: Jinja2 template for the display name to set
# on first login. If unset, no displayname will be set. # on first login. If unset, no displayname will be set.
# #
# email_template: Jinja2 template for the email address of the user.
# If unset, no email address will be added to the account.
#
# extra_attributes: a map of Jinja2 templates for extra attributes # extra_attributes: a map of Jinja2 templates for extra attributes
# to send back to the client during login. # to send back to the client during login.
# Note that these are non-standard and clients will ignore them # Note that these are non-standard and clients will ignore them
@ -189,6 +192,12 @@ class OIDCConfig(Config):
# userinfo_endpoint: "https://accounts.example.com/userinfo" # userinfo_endpoint: "https://accounts.example.com/userinfo"
# jwks_uri: "https://accounts.example.com/.well-known/jwks.json" # jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
# skip_verification: true # skip_verification: true
# user_mapping_provider:
# config:
# subject_claim: "id"
# localpart_template: "{{ user.login }}"
# display_name_template: "{{ user.name }}"
# email_template: "{{ user.email }}"
# For use with Keycloak # For use with Keycloak
# #

View File

@ -1056,7 +1056,8 @@ class OidcSessionData:
UserAttributeDict = TypedDict( UserAttributeDict = TypedDict(
"UserAttributeDict", {"localpart": Optional[str], "display_name": Optional[str]} "UserAttributeDict",
{"localpart": Optional[str], "display_name": Optional[str], "emails": List[str]},
) )
C = TypeVar("C") C = TypeVar("C")
@ -1135,11 +1136,12 @@ def jinja_finalize(thing):
env = Environment(finalize=jinja_finalize) env = Environment(finalize=jinja_finalize)
@attr.s @attr.s(slots=True, frozen=True)
class JinjaOidcMappingConfig: class JinjaOidcMappingConfig:
subject_claim = attr.ib(type=str) subject_claim = attr.ib(type=str)
localpart_template = attr.ib(type=Optional[Template]) localpart_template = attr.ib(type=Optional[Template])
display_name_template = attr.ib(type=Optional[Template]) display_name_template = attr.ib(type=Optional[Template])
email_template = attr.ib(type=Optional[Template])
extra_attributes = attr.ib(type=Dict[str, Template]) extra_attributes = attr.ib(type=Dict[str, Template])
@ -1156,23 +1158,17 @@ class JinjaOidcMappingProvider(OidcMappingProvider[JinjaOidcMappingConfig]):
def parse_config(config: dict) -> JinjaOidcMappingConfig: def parse_config(config: dict) -> JinjaOidcMappingConfig:
subject_claim = config.get("subject_claim", "sub") subject_claim = config.get("subject_claim", "sub")
localpart_template = None # type: Optional[Template] def parse_template_config(option_name: str) -> Optional[Template]:
if "localpart_template" in config: if option_name not in config:
return None
try: try:
localpart_template = env.from_string(config["localpart_template"]) return env.from_string(config[option_name])
except Exception as e: except Exception as e:
raise ConfigError( raise ConfigError("invalid jinja template", path=[option_name]) from e
"invalid jinja template", path=["localpart_template"]
) from e
display_name_template = None # type: Optional[Template] localpart_template = parse_template_config("localpart_template")
if "display_name_template" in config: display_name_template = parse_template_config("display_name_template")
try: email_template = parse_template_config("email_template")
display_name_template = env.from_string(config["display_name_template"])
except Exception as e:
raise ConfigError(
"invalid jinja template", path=["display_name_template"]
) from e
extra_attributes = {} # type Dict[str, Template] extra_attributes = {} # type Dict[str, Template]
if "extra_attributes" in config: if "extra_attributes" in config:
@ -1192,6 +1188,7 @@ class JinjaOidcMappingProvider(OidcMappingProvider[JinjaOidcMappingConfig]):
subject_claim=subject_claim, subject_claim=subject_claim,
localpart_template=localpart_template, localpart_template=localpart_template,
display_name_template=display_name_template, display_name_template=display_name_template,
email_template=email_template,
extra_attributes=extra_attributes, extra_attributes=extra_attributes,
) )
@ -1213,16 +1210,23 @@ class JinjaOidcMappingProvider(OidcMappingProvider[JinjaOidcMappingConfig]):
# a usable mxid. # a usable mxid.
localpart += str(failures) if failures else "" localpart += str(failures) if failures else ""
display_name = None # type: Optional[str] def render_template_field(template: Optional[Template]) -> Optional[str]:
if self._config.display_name_template is not None: if template is None:
display_name = self._config.display_name_template.render( return None
user=userinfo return template.render(user=userinfo).strip()
).strip()
if display_name == "": display_name = render_template_field(self._config.display_name_template)
display_name = None if display_name == "":
display_name = None
return UserAttributeDict(localpart=localpart, display_name=display_name) emails = [] # type: List[str]
email = render_template_field(self._config.email_template)
if email:
emails.append(email)
return UserAttributeDict(
localpart=localpart, display_name=display_name, emails=emails
)
async def get_extra_attributes(self, userinfo: UserInfo, token: Token) -> JsonDict: async def get_extra_attributes(self, userinfo: UserInfo, token: Token) -> JsonDict:
extras = {} # type: Dict[str, str] extras = {} # type: Dict[str, str]