providers/oauth2: make sub configurable based on hash, username, email and upn
This commit is contained in:
parent
c4de808c4e
commit
5c622cd4d2
|
@ -23,6 +23,7 @@ class OAuth2ProviderSerializer(ModelSerializer):
|
|||
"rsa_key",
|
||||
"redirect_uris",
|
||||
"post_logout_redirect_uris",
|
||||
"sub_mode",
|
||||
"property_mappings",
|
||||
]
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ class OAuth2ProviderForm(forms.ModelForm):
|
|||
"rsa_key",
|
||||
"redirect_uris",
|
||||
"post_logout_redirect_uris",
|
||||
"sub_mode",
|
||||
"property_mappings",
|
||||
]
|
||||
widgets = {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 3.1.1 on 2020-09-15 18:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_providers_oauth2", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="oauth2provider",
|
||||
name="sub_mode",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("hashed_user_id", "Based on the Hashed User ID"),
|
||||
("user_username", "Based on the username"),
|
||||
(
|
||||
"user_email",
|
||||
"Based on the User's Email. This is recommended over the UPN method.",
|
||||
),
|
||||
(
|
||||
"user_upn",
|
||||
"Based on the User's UPN, only works if user has a 'upn' attribute set. Use this method only if you have different UPN and Mail domains.",
|
||||
),
|
||||
],
|
||||
default="hashed_user_id",
|
||||
help_text="Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -46,6 +46,26 @@ class GrantTypes(models.TextChoices):
|
|||
HYBRID = "hybrid"
|
||||
|
||||
|
||||
class SubModes(models.TextChoices):
|
||||
"""Mode after which 'sub' attribute is generateed, for compatibility reasons"""
|
||||
|
||||
HASHED_USER_ID = "hashed_user_id", _("Based on the Hashed User ID")
|
||||
USER_USERNAME = "user_username", _("Based on the username")
|
||||
USER_EMAIL = (
|
||||
"user_email",
|
||||
_("Based on the User's Email. This is recommended over the UPN method."),
|
||||
)
|
||||
USER_UPN = (
|
||||
"user_upn",
|
||||
_(
|
||||
(
|
||||
"Based on the User's UPN, only works if user has a 'upn' attribute set. "
|
||||
"Use this method only if you have different UPN and Mail domains."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ResponseTypes(models.TextChoices):
|
||||
"""Response Type required by the client."""
|
||||
|
||||
|
@ -84,7 +104,7 @@ class ScopeMapping(PropertyMapping):
|
|||
return ScopeMappingForm
|
||||
|
||||
def __str__(self):
|
||||
return f"Scope Mapping '{self.scope_name}'"
|
||||
return f"Scope Mapping {self.name} ({self.scope_name})"
|
||||
|
||||
class Meta:
|
||||
|
||||
|
@ -162,6 +182,17 @@ class OAuth2Provider(Provider):
|
|||
),
|
||||
)
|
||||
|
||||
sub_mode = models.TextField(
|
||||
choices=SubModes.choices,
|
||||
default=SubModes.HASHED_USER_ID,
|
||||
help_text=_(
|
||||
(
|
||||
"Configure what data should be used as unique User Identifier. For most cases, "
|
||||
"the default should be fine."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
rsa_key = models.ForeignKey(
|
||||
CertificateKeyPair,
|
||||
verbose_name=_("RSA Key"),
|
||||
|
@ -249,6 +280,14 @@ class OAuth2Provider(Provider):
|
|||
def __str__(self):
|
||||
return f"OAuth2 Provider {self.name}"
|
||||
|
||||
def encode(self, payload: Dict[str, Any]) -> str:
|
||||
"""Represent the ID Token as a JSON Web Token (JWT)."""
|
||||
keys = self.get_jwt_keys()
|
||||
# If the provider does not have an RSA Key assigned, it was switched to Symmetric
|
||||
self.refresh_from_db()
|
||||
jws = JWS(payload, alg=self.jwt_alg)
|
||||
return jws.sign_compact(keys)
|
||||
|
||||
def html_setup_urls(self, request: HttpRequest) -> Optional[str]:
|
||||
"""return template and context modal with URLs for authorize, token, openid-config, etc"""
|
||||
try:
|
||||
|
@ -368,14 +407,6 @@ class IDToken:
|
|||
dic.update(self.claims)
|
||||
return dic
|
||||
|
||||
def encode(self, provider: OAuth2Provider) -> str:
|
||||
"""Represent the ID Token as a JSON Web Token (JWT)."""
|
||||
keys = provider.get_jwt_keys()
|
||||
# If the provider does not have an RSA Key assigned, it was switched to Symmetric
|
||||
provider.refresh_from_db()
|
||||
jws = JWS(self.to_dict(), alg=provider.jwt_alg)
|
||||
return jws.sign_compact(keys)
|
||||
|
||||
|
||||
class RefreshToken(ExpiringModel, BaseGrantModel):
|
||||
"""OAuth2 Refresh Token"""
|
||||
|
@ -424,7 +455,22 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
|
|||
def create_id_token(self, user: User, request: HttpRequest) -> IDToken:
|
||||
"""Creates the id_token.
|
||||
See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken"""
|
||||
sub = ""
|
||||
if self.provider.sub_mode == SubModes.HASHED_USER_ID:
|
||||
sub = sha256(f"{user.id}-{settings.SECRET_KEY}".encode("ascii")).hexdigest()
|
||||
elif self.provider.sub_mode == SubModes.USER_EMAIL:
|
||||
sub = user.email
|
||||
elif self.provider.sub_mode == SubModes.USER_USERNAME:
|
||||
sub = user.username
|
||||
elif self.provider.sub_mode == SubModes.USER_UPN:
|
||||
sub = user.attributes["upn"]
|
||||
else:
|
||||
raise ValueError(
|
||||
(
|
||||
f"Provider {self.provider} has invalid sub_mode "
|
||||
f"selected: {self.provider.sub_mode}"
|
||||
)
|
||||
)
|
||||
|
||||
# Convert datetimes into timestamps.
|
||||
now = int(time.time())
|
||||
|
|
|
@ -281,7 +281,9 @@ class OAuthFulfillmentStage(StageView):
|
|||
ResponseTypes.CODE_ID_TOKEN,
|
||||
ResponseTypes.CODE_ID_TOKEN_TOKEN,
|
||||
]:
|
||||
query_fragment["id_token"] = id_token.encode(self.provider)
|
||||
query_fragment["id_token"] = self.provider.encode(
|
||||
id_token.to_dict()
|
||||
)
|
||||
token.id_token = id_token
|
||||
|
||||
# Store the token.
|
||||
|
|
|
@ -193,11 +193,11 @@ class TokenView(View):
|
|||
dic = {
|
||||
"access_token": refresh_token.access_token,
|
||||
"refresh_token": refresh_token.refresh_token,
|
||||
"token_type": "bearer",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": timedelta_from_string(
|
||||
self.params.provider.token_validity
|
||||
).seconds,
|
||||
"id_token": refresh_token.id_token.encode(refresh_token.provider),
|
||||
"id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()),
|
||||
}
|
||||
|
||||
return dic
|
||||
|
@ -237,8 +237,8 @@ class TokenView(View):
|
|||
"expires_in": timedelta_from_string(
|
||||
refresh_token.provider.token_validity
|
||||
).seconds,
|
||||
"id_token": refresh_token.id_token.encode(
|
||||
self.params.refresh_token.provider
|
||||
"id_token": self.params.provider.encode(
|
||||
self.params.refresh_token.id_token.to_dict()
|
||||
),
|
||||
}
|
||||
|
||||
|
|
10
swagger.yaml
10
swagger.yaml
|
@ -6646,6 +6646,16 @@ definitions:
|
|||
title: Post Logout Redirect URIs
|
||||
description: Enter each URI on a new line.
|
||||
type: string
|
||||
sub_mode:
|
||||
title: Sub mode
|
||||
description: Configure what data should be used as unique User Identifier.
|
||||
For most cases, the default should be fine.
|
||||
type: string
|
||||
enum:
|
||||
- hashed_user_id
|
||||
- user_username
|
||||
- user_email
|
||||
- user_upn
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
|
|
Reference in New Issue