From e216efb6ecf768ebd6854b6eb675566adcd3edba Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 27 Dec 2020 19:08:02 +0100 Subject: [PATCH] providers/oauth2: create access tokens as JWT --- .../migrations/0003_auto_20200916_2129.py | 4 - .../migrations/0005_auto_20200920_1240.py | 4 - .../migrations/0010_auto_20201227_1804.py | 18 +++ authentik/providers/oauth2/models.py | 21 ++- authentik/providers/oauth2/views/authorize.py | 140 +++++++++--------- authentik/providers/oauth2/views/token.py | 9 +- 6 files changed, 100 insertions(+), 96 deletions(-) create mode 100644 authentik/providers/oauth2/migrations/0010_auto_20201227_1804.py diff --git a/authentik/providers/oauth2/migrations/0003_auto_20200916_2129.py b/authentik/providers/oauth2/migrations/0003_auto_20200916_2129.py index bc14353c3..2481d6f88 100644 --- a/authentik/providers/oauth2/migrations/0003_auto_20200916_2129.py +++ b/authentik/providers/oauth2/migrations/0003_auto_20200916_2129.py @@ -27,10 +27,6 @@ class Migration(migrations.Migration): field=models.TextField( choices=[ ("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 token", "id_token token (Implicit Flow)"), ("code token", "code token (Hybrid Flow)"), diff --git a/authentik/providers/oauth2/migrations/0005_auto_20200920_1240.py b/authentik/providers/oauth2/migrations/0005_auto_20200920_1240.py index eb50bb39d..4a12ca312 100644 --- a/authentik/providers/oauth2/migrations/0005_auto_20200920_1240.py +++ b/authentik/providers/oauth2/migrations/0005_auto_20200920_1240.py @@ -19,10 +19,6 @@ class Migration(migrations.Migration): field=models.TextField( choices=[ ("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 token", "id_token token (Implicit Flow)"), ("code token", "code token (Hybrid Flow)"), diff --git a/authentik/providers/oauth2/migrations/0010_auto_20201227_1804.py b/authentik/providers/oauth2/migrations/0010_auto_20201227_1804.py new file mode 100644 index 000000000..96ba7e53b --- /dev/null +++ b/authentik/providers/oauth2/migrations/0010_auto_20201227_1804.py @@ -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"), + ), + ] diff --git a/authentik/providers/oauth2/models.py b/authentik/providers/oauth2/models.py index 0f3ac043b..6dd5ab3c9 100644 --- a/authentik/providers/oauth2/models.py +++ b/authentik/providers/oauth2/models.py @@ -84,10 +84,6 @@ class ResponseTypes(models.TextChoices): """Response Type required by the client.""" 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_TOKEN = "id_token token", _("id_token token (Implicit Flow)") CODE_TOKEN = "code token", _("code token (Hybrid Flow)") @@ -218,19 +214,17 @@ class OAuth2Provider(Provider): ) def create_refresh_token( - self, user: User, scope: List[str], id_token: Optional["IDToken"] = None + self, user: User, scope: List[str], request: HttpRequest ) -> "RefreshToken": """Create and populate a RefreshToken object.""" token = RefreshToken( user=user, provider=self, - access_token=uuid4().hex, refresh_token=uuid4().hex, expires=timezone.now() + timedelta_from_string(self.token_validity), scope=scope, ) - if id_token: - token.id_token = id_token + token.access_token = token.create_access_token(user, request) return token def get_jwt_keys(self) -> List[Key]: @@ -444,9 +438,7 @@ class IDToken: class RefreshToken(ExpiringModel, BaseGrantModel): """OAuth2 Refresh Token""" - access_token = models.CharField( - max_length=255, unique=True, verbose_name=_("Access Token") - ) + access_token = models.TextField(verbose_name=_("Access Token")) refresh_token = models.CharField( max_length=255, unique=True, verbose_name=_("Refresh Token") ) @@ -485,6 +477,13 @@ class RefreshToken(ExpiringModel, BaseGrantModel): .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: """Creates the id_token. See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken""" diff --git a/authentik/providers/oauth2/views/authorize.py b/authentik/providers/oauth2/views/authorize.py index 86deda680..23fe4024f 100644 --- a/authentik/providers/oauth2/views/authorize.py +++ b/authentik/providers/oauth2/views/authorize.py @@ -101,7 +101,7 @@ class OAuthAuthorizationParams: response_type = query_dict.get("response_type", "") grant_type = None # 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 elif response_type in [ ResponseTypes.ID_TOKEN, @@ -273,7 +273,6 @@ class OAuthFulfillmentStage(StageView): """Create a final Response URI the user is redirected to.""" uri = urlsplit(self.params.redirect_uri) query_params = parse_qs(uri.query) - query_fragment = {} try: code = None @@ -290,64 +289,17 @@ class OAuthFulfillmentStage(StageView): query_params["state"] = [ 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( - user=self.request.user, - scope=self.params.scope, + + uri = uri._replace(query=urlencode(query_params, doseq=True)) + return urlunsplit(uri) + 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), ) - - # 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 urlunsplit(uri) + raise OAuth2Error() except OAuth2Error as error: LOGGER.exception("Error when trying to create response uri", error=error) raise AuthorizeError( @@ -357,19 +309,67 @@ class OAuthFulfillmentStage(StageView): self.params.state, ) - replace_kwargs = {} - if self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]: - replace_kwargs = { - "fragment": uri.fragment + urlencode(query_fragment, doseq=True), - } - else: - replace_kwargs = { - "query": urlencode(query_params, doseq=True), - } + def create_implicit_response(self, code: Optional[AuthorizationCode]) -> dict: + """Create implicit response's URL Fragment dictionary""" + query_fragment = {} - 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): diff --git a/authentik/providers/oauth2/views/token.py b/authentik/providers/oauth2/views/token.py index 47a904210..78f4b41c8 100644 --- a/authentik/providers/oauth2/views/token.py +++ b/authentik/providers/oauth2/views/token.py @@ -177,6 +177,7 @@ class TokenView(View): refresh_token = self.params.authorization_code.provider.create_refresh_token( user=self.params.authorization_code.user, scope=self.params.authorization_code.scope, + request=self.request, ) 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()), } - # 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 def create_refresh_response_dic(self) -> Dict[str, Any]: @@ -227,6 +221,7 @@ class TokenView(View): refresh_token: RefreshToken = provider.create_refresh_token( user=self.params.refresh_token.user, scope=self.params.scope, + request=self.request, ) # If the Token has an id_token it's an Authentication request.