providers/oauth2: create access tokens as JWT

This commit is contained in:
Jens Langhammer 2020-12-27 19:08:02 +01:00
parent 378fe38b12
commit e216efb6ec
6 changed files with 100 additions and 96 deletions

View File

@ -27,10 +27,6 @@ class Migration(migrations.Migration):
field=models.TextField( field=models.TextField(
choices=[ choices=[
("code", "code (Authorization Code Flow)"), ("code", "code (Authorization Code Flow)"),
(
"code_adfs",
"code (ADFS Compatibility Mode, sends id_token as access_token)",
),
("id_token", "id_token (Implicit Flow)"), ("id_token", "id_token (Implicit Flow)"),
("id_token token", "id_token token (Implicit Flow)"), ("id_token token", "id_token token (Implicit Flow)"),
("code token", "code token (Hybrid Flow)"), ("code token", "code token (Hybrid Flow)"),

View File

@ -19,10 +19,6 @@ class Migration(migrations.Migration):
field=models.TextField( field=models.TextField(
choices=[ choices=[
("code", "code (Authorization Code Flow)"), ("code", "code (Authorization Code Flow)"),
(
"code#adfs",
"code (ADFS Compatibility Mode, sends id_token as access_token)",
),
("id_token", "id_token (Implicit Flow)"), ("id_token", "id_token (Implicit Flow)"),
("id_token token", "id_token token (Implicit Flow)"), ("id_token token", "id_token token (Implicit Flow)"),
("code token", "code token (Hybrid Flow)"), ("code token", "code token (Hybrid Flow)"),

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2020-12-27 18:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_oauth2", "0009_remove_oauth2provider_response_type"),
]
operations = [
migrations.AlterField(
model_name="refreshtoken",
name="access_token",
field=models.TextField(verbose_name="Access Token"),
),
]

View File

@ -84,10 +84,6 @@ class ResponseTypes(models.TextChoices):
"""Response Type required by the client.""" """Response Type required by the client."""
CODE = "code", _("code (Authorization Code Flow)") CODE = "code", _("code (Authorization Code Flow)")
CODE_ADFS = (
"code#adfs",
_("code (ADFS Compatibility Mode, sends id_token as access_token)"),
)
ID_TOKEN = "id_token", _("id_token (Implicit Flow)") ID_TOKEN = "id_token", _("id_token (Implicit Flow)")
ID_TOKEN_TOKEN = "id_token token", _("id_token token (Implicit Flow)") ID_TOKEN_TOKEN = "id_token token", _("id_token token (Implicit Flow)")
CODE_TOKEN = "code token", _("code token (Hybrid Flow)") CODE_TOKEN = "code token", _("code token (Hybrid Flow)")
@ -218,19 +214,17 @@ class OAuth2Provider(Provider):
) )
def create_refresh_token( def create_refresh_token(
self, user: User, scope: List[str], id_token: Optional["IDToken"] = None self, user: User, scope: List[str], request: HttpRequest
) -> "RefreshToken": ) -> "RefreshToken":
"""Create and populate a RefreshToken object.""" """Create and populate a RefreshToken object."""
token = RefreshToken( token = RefreshToken(
user=user, user=user,
provider=self, provider=self,
access_token=uuid4().hex,
refresh_token=uuid4().hex, refresh_token=uuid4().hex,
expires=timezone.now() + timedelta_from_string(self.token_validity), expires=timezone.now() + timedelta_from_string(self.token_validity),
scope=scope, scope=scope,
) )
if id_token: token.access_token = token.create_access_token(user, request)
token.id_token = id_token
return token return token
def get_jwt_keys(self) -> List[Key]: def get_jwt_keys(self) -> List[Key]:
@ -444,9 +438,7 @@ class IDToken:
class RefreshToken(ExpiringModel, BaseGrantModel): class RefreshToken(ExpiringModel, BaseGrantModel):
"""OAuth2 Refresh Token""" """OAuth2 Refresh Token"""
access_token = models.CharField( access_token = models.TextField(verbose_name=_("Access Token"))
max_length=255, unique=True, verbose_name=_("Access Token")
)
refresh_token = models.CharField( refresh_token = models.CharField(
max_length=255, unique=True, verbose_name=_("Refresh Token") max_length=255, unique=True, verbose_name=_("Refresh Token")
) )
@ -485,6 +477,13 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
.decode("ascii") .decode("ascii")
) )
def create_access_token(self, user: User, request: HttpRequest) -> str:
"""Create access token with a similar format as Okta, Keycloak, ADFS"""
token = self.create_id_token(user, request).to_dict()
token["cid"] = self.provider.client_id
token["uid"] = uuid4().hex
return self.provider.encode(token)
def create_id_token(self, user: User, request: HttpRequest) -> IDToken: def create_id_token(self, user: User, request: HttpRequest) -> IDToken:
"""Creates the id_token. """Creates the id_token.
See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken""" See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken"""

View File

@ -101,7 +101,7 @@ class OAuthAuthorizationParams:
response_type = query_dict.get("response_type", "") response_type = query_dict.get("response_type", "")
grant_type = None grant_type = None
# Determine which flow to use. # Determine which flow to use.
if response_type in [ResponseTypes.CODE, ResponseTypes.CODE_ADFS]: if response_type in [ResponseTypes.CODE]:
grant_type = GrantTypes.AUTHORIZATION_CODE grant_type = GrantTypes.AUTHORIZATION_CODE
elif response_type in [ elif response_type in [
ResponseTypes.ID_TOKEN, ResponseTypes.ID_TOKEN,
@ -273,7 +273,6 @@ class OAuthFulfillmentStage(StageView):
"""Create a final Response URI the user is redirected to.""" """Create a final Response URI the user is redirected to."""
uri = urlsplit(self.params.redirect_uri) uri = urlsplit(self.params.redirect_uri)
query_params = parse_qs(uri.query) query_params = parse_qs(uri.query)
query_fragment = {}
try: try:
code = None code = None
@ -290,64 +289,17 @@ class OAuthFulfillmentStage(StageView):
query_params["state"] = [ query_params["state"] = [
str(self.params.state) if self.params.state else "" str(self.params.state) if self.params.state else ""
] ]
elif self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]:
token = self.provider.create_refresh_token( uri = uri._replace(query=urlencode(query_params, doseq=True))
user=self.request.user, return urlunsplit(uri)
scope=self.params.scope, if self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]:
query_fragment = self.create_implicit_response(code)
uri = uri._replace(
fragment=uri.fragment + urlencode(query_fragment, doseq=True),
) )
return urlunsplit(uri)
# Check if response_type must include access_token in the response. raise OAuth2Error()
if self.params.response_type in [
ResponseTypes.ID_TOKEN_TOKEN,
ResponseTypes.CODE_ID_TOKEN_TOKEN,
ResponseTypes.ID_TOKEN,
ResponseTypes.CODE_TOKEN,
]:
query_fragment["access_token"] = token.access_token
# We don't need id_token if it's an OAuth2 request.
if SCOPE_OPENID in self.params.scope:
id_token = token.create_id_token(
user=self.request.user,
request=self.request,
)
id_token.nonce = self.params.nonce
# Include at_hash when access_token is being returned.
if "access_token" in query_fragment:
id_token.at_hash = token.at_hash
if self.params.response_type in [
ResponseTypes.CODE_ID_TOKEN,
ResponseTypes.CODE_ID_TOKEN_TOKEN,
]:
id_token.c_hash = code.c_hash
# Check if response_type must include id_token in the response.
if self.params.response_type in [
ResponseTypes.ID_TOKEN,
ResponseTypes.ID_TOKEN_TOKEN,
ResponseTypes.CODE_ID_TOKEN,
ResponseTypes.CODE_ID_TOKEN_TOKEN,
]:
query_fragment["id_token"] = self.provider.encode(
id_token.to_dict()
)
token.id_token = id_token
# Store the token.
token.save()
# Code parameter must be present if it's Hybrid Flow.
if self.params.grant_type == GrantTypes.HYBRID:
query_fragment["code"] = code.code
query_fragment["token_type"] = "bearer"
query_fragment["expires_in"] = timedelta_from_string(
self.provider.token_validity
).seconds
query_fragment["state"] = self.params.state if self.params.state else ""
except OAuth2Error as error: except OAuth2Error as error:
LOGGER.exception("Error when trying to create response uri", error=error) LOGGER.exception("Error when trying to create response uri", error=error)
raise AuthorizeError( raise AuthorizeError(
@ -357,19 +309,67 @@ class OAuthFulfillmentStage(StageView):
self.params.state, self.params.state,
) )
replace_kwargs = {} def create_implicit_response(self, code: Optional[AuthorizationCode]) -> dict:
if self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]: """Create implicit response's URL Fragment dictionary"""
replace_kwargs = { query_fragment = {}
"fragment": uri.fragment + urlencode(query_fragment, doseq=True),
}
else:
replace_kwargs = {
"query": urlencode(query_params, doseq=True),
}
uri = uri._replace(**replace_kwargs) token = self.provider.create_refresh_token(
user=self.request.user,
scope=self.params.scope,
request=self.request,
)
return urlunsplit(uri) # Check if response_type must include access_token in the response.
if self.params.response_type in [
ResponseTypes.ID_TOKEN_TOKEN,
ResponseTypes.CODE_ID_TOKEN_TOKEN,
ResponseTypes.ID_TOKEN,
ResponseTypes.CODE_TOKEN,
]:
query_fragment["access_token"] = token.access_token
# We don't need id_token if it's an OAuth2 request.
if SCOPE_OPENID in self.params.scope:
id_token = token.create_id_token(
user=self.request.user,
request=self.request,
)
id_token.nonce = self.params.nonce
# Include at_hash when access_token is being returned.
if "access_token" in query_fragment:
id_token.at_hash = token.at_hash
if self.params.response_type in [
ResponseTypes.CODE_ID_TOKEN,
ResponseTypes.CODE_ID_TOKEN_TOKEN,
]:
id_token.c_hash = code.c_hash
# Check if response_type must include id_token in the response.
if self.params.response_type in [
ResponseTypes.ID_TOKEN,
ResponseTypes.ID_TOKEN_TOKEN,
ResponseTypes.CODE_ID_TOKEN,
ResponseTypes.CODE_ID_TOKEN_TOKEN,
]:
query_fragment["id_token"] = self.provider.encode(id_token.to_dict())
token.id_token = id_token
# Store the token.
token.save()
# Code parameter must be present if it's Hybrid Flow.
if self.params.grant_type == GrantTypes.HYBRID:
query_fragment["code"] = code.code
query_fragment["token_type"] = "bearer"
query_fragment["expires_in"] = timedelta_from_string(
self.provider.token_validity
).seconds
query_fragment["state"] = self.params.state if self.params.state else ""
return query_fragment
class AuthorizationFlowInitView(PolicyAccessView): class AuthorizationFlowInitView(PolicyAccessView):

View File

@ -177,6 +177,7 @@ class TokenView(View):
refresh_token = self.params.authorization_code.provider.create_refresh_token( refresh_token = self.params.authorization_code.provider.create_refresh_token(
user=self.params.authorization_code.user, user=self.params.authorization_code.user,
scope=self.params.authorization_code.scope, scope=self.params.authorization_code.scope,
request=self.request,
) )
if self.params.authorization_code.is_open_id: if self.params.authorization_code.is_open_id:
@ -204,13 +205,6 @@ class TokenView(View):
"id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()), "id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()),
} }
# if self.params.provider.response_type == ResponseTypes.CODE_ADFS:
# # This seems to be expected by some OIDC Clients
# # namely VMware vCenter. This is not documented in any OpenID or OAuth2 Standard.
# # Maybe this should be a setting
# # in the future?
# response_dict["access_token"] = response_dict["id_token"]
return response_dict return response_dict
def create_refresh_response_dic(self) -> Dict[str, Any]: def create_refresh_response_dic(self) -> Dict[str, Any]:
@ -227,6 +221,7 @@ class TokenView(View):
refresh_token: RefreshToken = provider.create_refresh_token( refresh_token: RefreshToken = provider.create_refresh_token(
user=self.params.refresh_token.user, user=self.params.refresh_token.user,
scope=self.params.scope, scope=self.params.scope,
request=self.request,
) )
# If the Token has an id_token it's an Authentication request. # If the Token has an id_token it's an Authentication request.