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(
|
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)"),
|
||||||
|
|
|
@ -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)"),
|
||||||
|
|
|
@ -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."""
|
"""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"""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Reference in New Issue