providers/oauth2: create access tokens as JWT
This commit is contained in:
parent
378fe38b12
commit
e216efb6ec
|
@ -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)"),
|
||||
|
|
|
@ -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)"),
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
]
|
|
@ -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"""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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.
|
||||
|
|
Reference in a new issue