Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 8696677

Browse files
authored
Support for scraping email addresses from OIDC providers (#9245)
1 parent fbd9de6 commit 8696677

File tree

4 files changed

+53
-30
lines changed

4 files changed

+53
-30
lines changed

changelog.d/9245.feature

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

docs/sample_config.yaml

+12-3
Original file line numberDiff line numberDiff line change
@@ -1791,9 +1791,9 @@ saml2_config:
17911791
#
17921792
# For the default provider, the following settings are available:
17931793
#
1794-
# sub: name of the claim containing a unique identifier for the
1795-
# user. Defaults to 'sub', which OpenID Connect compliant
1796-
# providers should provide.
1794+
# subject_claim: name of the claim containing a unique identifier
1795+
# for the user. Defaults to 'sub', which OpenID Connect
1796+
# compliant providers should provide.
17971797
#
17981798
# localpart_template: Jinja2 template for the localpart of the MXID.
17991799
# If this is not set, the user will be prompted to choose their
@@ -1802,6 +1802,9 @@ saml2_config:
18021802
# display_name_template: Jinja2 template for the display name to set
18031803
# on first login. If unset, no displayname will be set.
18041804
#
1805+
# email_template: Jinja2 template for the email address of the user.
1806+
# If unset, no email address will be added to the account.
1807+
#
18051808
# extra_attributes: a map of Jinja2 templates for extra attributes
18061809
# to send back to the client during login.
18071810
# Note that these are non-standard and clients will ignore them
@@ -1837,6 +1840,12 @@ oidc_providers:
18371840
# userinfo_endpoint: "https://accounts.example.com/userinfo"
18381841
# jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
18391842
# skip_verification: true
1843+
# user_mapping_provider:
1844+
# config:
1845+
# subject_claim: "id"
1846+
# localpart_template: "{ user.login }"
1847+
# display_name_template: "{ user.name }"
1848+
# email_template: "{ user.email }"
18401849

18411850
# For use with Keycloak
18421851
#

synapse/config/oidc_config.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs):
143143
#
144144
# For the default provider, the following settings are available:
145145
#
146-
# sub: name of the claim containing a unique identifier for the
147-
# user. Defaults to 'sub', which OpenID Connect compliant
148-
# providers should provide.
146+
# subject_claim: name of the claim containing a unique identifier
147+
# for the user. Defaults to 'sub', which OpenID Connect
148+
# compliant providers should provide.
149149
#
150150
# localpart_template: Jinja2 template for the localpart of the MXID.
151151
# If this is not set, the user will be prompted to choose their
@@ -154,6 +154,9 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs):
154154
# display_name_template: Jinja2 template for the display name to set
155155
# on first login. If unset, no displayname will be set.
156156
#
157+
# email_template: Jinja2 template for the email address of the user.
158+
# If unset, no email address will be added to the account.
159+
#
157160
# extra_attributes: a map of Jinja2 templates for extra attributes
158161
# to send back to the client during login.
159162
# Note that these are non-standard and clients will ignore them
@@ -189,6 +192,12 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs):
189192
# userinfo_endpoint: "https://accounts.example.com/userinfo"
190193
# jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
191194
# skip_verification: true
195+
# user_mapping_provider:
196+
# config:
197+
# subject_claim: "id"
198+
# localpart_template: "{{ user.login }}"
199+
# display_name_template: "{{ user.name }}"
200+
# email_template: "{{ user.email }}"
192201
193202
# For use with Keycloak
194203
#

synapse/handlers/oidc_handler.py

+28-24
Original file line numberDiff line numberDiff line change
@@ -1056,7 +1056,8 @@ class OidcSessionData:
10561056

10571057

10581058
UserAttributeDict = TypedDict(
1059-
"UserAttributeDict", {"localpart": Optional[str], "display_name": Optional[str]}
1059+
"UserAttributeDict",
1060+
{"localpart": Optional[str], "display_name": Optional[str], "emails": List[str]},
10601061
)
10611062
C = TypeVar("C")
10621063

@@ -1135,11 +1136,12 @@ def jinja_finalize(thing):
11351136
env = Environment(finalize=jinja_finalize)
11361137

11371138

1138-
@attr.s
1139+
@attr.s(slots=True, frozen=True)
11391140
class JinjaOidcMappingConfig:
11401141
subject_claim = attr.ib(type=str)
11411142
localpart_template = attr.ib(type=Optional[Template])
11421143
display_name_template = attr.ib(type=Optional[Template])
1144+
email_template = attr.ib(type=Optional[Template])
11431145
extra_attributes = attr.ib(type=Dict[str, Template])
11441146

11451147

@@ -1156,23 +1158,17 @@ def __init__(self, config: JinjaOidcMappingConfig):
11561158
def parse_config(config: dict) -> JinjaOidcMappingConfig:
11571159
subject_claim = config.get("subject_claim", "sub")
11581160

1159-
localpart_template = None # type: Optional[Template]
1160-
if "localpart_template" in config:
1161+
def parse_template_config(option_name: str) -> Optional[Template]:
1162+
if option_name not in config:
1163+
return None
11611164
try:
1162-
localpart_template = env.from_string(config["localpart_template"])
1165+
return env.from_string(config[option_name])
11631166
except Exception as e:
1164-
raise ConfigError(
1165-
"invalid jinja template", path=["localpart_template"]
1166-
) from e
1167+
raise ConfigError("invalid jinja template", path=[option_name]) from e
11671168

1168-
display_name_template = None # type: Optional[Template]
1169-
if "display_name_template" in config:
1170-
try:
1171-
display_name_template = env.from_string(config["display_name_template"])
1172-
except Exception as e:
1173-
raise ConfigError(
1174-
"invalid jinja template", path=["display_name_template"]
1175-
) from e
1169+
localpart_template = parse_template_config("localpart_template")
1170+
display_name_template = parse_template_config("display_name_template")
1171+
email_template = parse_template_config("email_template")
11761172

11771173
extra_attributes = {} # type Dict[str, Template]
11781174
if "extra_attributes" in config:
@@ -1192,6 +1188,7 @@ def parse_config(config: dict) -> JinjaOidcMappingConfig:
11921188
subject_claim=subject_claim,
11931189
localpart_template=localpart_template,
11941190
display_name_template=display_name_template,
1191+
email_template=email_template,
11951192
extra_attributes=extra_attributes,
11961193
)
11971194

@@ -1213,16 +1210,23 @@ async def map_user_attributes(
12131210
# a usable mxid.
12141211
localpart += str(failures) if failures else ""
12151212

1216-
display_name = None # type: Optional[str]
1217-
if self._config.display_name_template is not None:
1218-
display_name = self._config.display_name_template.render(
1219-
user=userinfo
1220-
).strip()
1213+
def render_template_field(template: Optional[Template]) -> Optional[str]:
1214+
if template is None:
1215+
return None
1216+
return template.render(user=userinfo).strip()
1217+
1218+
display_name = render_template_field(self._config.display_name_template)
1219+
if display_name == "":
1220+
display_name = None
12211221

1222-
if display_name == "":
1223-
display_name = None
1222+
emails = [] # type: List[str]
1223+
email = render_template_field(self._config.email_template)
1224+
if email:
1225+
emails.append(email)
12241226

1225-
return UserAttributeDict(localpart=localpart, display_name=display_name)
1227+
return UserAttributeDict(
1228+
localpart=localpart, display_name=display_name, emails=emails
1229+
)
12261230

12271231
async def get_extra_attributes(self, userinfo: UserInfo, token: Token) -> JsonDict:
12281232
extras = {} # type: Dict[str, str]

0 commit comments

Comments
 (0)