Created
February 10, 2023 16:47
-
-
Save dopry/36e8cb676b08697ad26612521993b57d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class OauthEndSessionView(views.APIView): | |
"""Impements an endpoint to end the session | |
https://openid.net/specs/openid-connect-rpinitiated-1_0.html | |
* Works on both GET and POST | |
OpenID Providers MUST support the use of the HTTP GET and POST methods defined in RFC 7231 [RFC7231] at the Logout | |
Endpoint. RPs MAY use the HTTP GET or POST methods to send the logout request to the OP. If using the HTTP GET | |
method, the request parameters are serialized using URI Query String Serialization. If using the HTTP POST method, | |
the request parameters are serialized using Form Serialization. | |
* Runs the normal django logout | |
* The spec doesn't state what http response codes to use, so we'll use 204 no content | |
""" | |
def _get_token(self, id_token_hint, request): | |
""" | |
When an id_token_hint parameter is present, the OP MUST validate that it was the issuer of the ID Token. | |
The OP SHOULD accept ID Tokens when the RP identified by the ID Token's aud claim and/or sid claim has a | |
current session or had a recent session at the OP, even when the exp time has passed. If the ID Token's sid | |
claim does not correspond to the RP's current session or a recent session at the OP, the OP SHOULD treat | |
the logout request as suspect, and MAY decline to act upon it. | |
""" | |
IDToken = get_id_token_model() | |
# decode the id_token_hint | |
private_key = JWK.from_pem(str.encode(oauth2_settings.OIDC_RSA_PRIVATE_KEY)) | |
public_key = private_key.export_public(as_dict=True) | |
key = JWK(**public_key) | |
try: | |
deserialized_id_token_hint = JWT(jwt=id_token_hint, key=JWK(**public_key)) | |
except ValueError as e: | |
logger.error("Unable to deserialize JWT") | |
return None | |
claims = json.loads(deserialized_id_token_hint.claims) | |
# verify we issued the token | |
# Oauth toolkit oidc_issuer does not accept a DRF Request (even though it's compatible and would work) | |
# because DRF doesn't inherit from HttpRequest but extends the class using composition. | |
# Thankfully DRF request hold an instance of a Django Request object that we *can* use | |
# TODO: Submit a patch to oauth toolkit to allow DRF Requests | |
# See: https://www.django-rest-framework.org/api-guide/requests/#standard-httprequest-attributes | |
# See: https://github.com/jazzband/django-oauth-toolkit/blob/492a867499b50f348c28db4ef3e429e8f46dc412/oauth2_provider/settings.py#L290 | |
if claims["iss"] != oauth2_settings.oidc_issuer(request._request): | |
logger.error( | |
"The id_token_hint is not issued by the OP.", | |
extra={"iss": claims["iss"]}, | |
) | |
return None | |
# Verify user id matches | |
if int(claims["sub"]) != request.user.id: | |
logger.error( | |
"The id_token_hint is not for the user", extra={"sub": claims["sub"]} | |
) | |
try: | |
token = IDToken.objects.get(user=request.user, jti=claims["jti"]) | |
except IDToken.DoesNotExist: | |
logger.error( | |
"The token was not found in the database", | |
extra={"jti": claims["jti"], "user_id": request.user_id}, | |
) | |
return None | |
return token | |
def _get_client_id_app(self, client_id, token=None): | |
""" | |
Looks up the client Application by client it and returns it. | |
If the client Application can't be found then return None | |
If a token is passed in verify that the token's Application matches the client Application | |
""" | |
try: | |
client_id_app = Application.objects.get(client_id=client_id) | |
except Application.DoesNotExist: | |
logger.error("Invalid client_id", extra={"client_id": client_id}) | |
return None | |
if token is not None: | |
if token.application != client_id_app: | |
logger.error( | |
"The client_id does not match the application of the submitted id_token_hint", | |
extra={ | |
"client_id": client_id, | |
"token_application": token.application, | |
}, | |
) | |
return None | |
return client_id_app | |
def _get_redirect_uri( | |
self, post_logout_redirect_uri, token=None, client_id_app=None | |
): | |
""" | |
In some cases, the RP will request that the End-User's User Agent to be redirected back to the RP after a | |
logout has been performed. Post-logout redirection is only done when the logout is RP-initiated, in which | |
case the redirection target is the post_logout_redirect_uri parameter value sent by the initiating RP. An | |
id_token_hint carring an ID Token for the RP is also RECOMMENDED when requesting post-logout redirection; | |
if it is not supplied with post_logout_redirect_uri, the OP MUST NOT perform post-logout redirection unless | |
the OP has other means of confirming the legitimacy of the post-logout redirection target. The OP also MUST | |
NOT perform post-logout redirection if the post_logout_redirect_uri value supplied does not exactly match | |
one of the previously registered post_logout_redirect_uris values. The post-logout redirection is performed | |
after the OP has finished notifying the RPs that logged in with the OP for that End-User that they are to | |
log out the End-User. | |
""" | |
if token is not None and token.application is not None: | |
# look up the app associated with the id_token_hint | |
if token.application.post_logout_redirect_uri_allowed( | |
post_logout_redirect_uri | |
): | |
return post_logout_redirect_uri | |
else: | |
logger.error( | |
"post_logout_redirect_uri not in token.application valid urls", | |
extra={"post_logout_redirect_uri": post_logout_redirect_uri}, | |
) | |
elif client_id_app is not None: | |
# look up app that app associated with the client_id | |
if client_id_app.post_logout_redirect_uri_allowed(post_logout_redirect_uri): | |
return post_logout_redirect_uri | |
else: | |
logger.error( | |
"post_logout_redirect_uri not in client_id_app valid urls", | |
extra={"post_logout_redirect_uri": post_logout_redirect_uri}, | |
) | |
else: | |
logger.error( | |
"post_logout_redirect_uri requires id_token_hint or client_id to be set" | |
) | |
def get(self, request, *args, **kwards): | |
return self.handle_logout_request(request, request.query_params) | |
def post(self, request, *args, **kwargs): | |
return self.handle_logout_request(request, request.data) | |
def handle_logout_request(self, request, params): | |
""" | |
id_token_hint | |
RECOMMENDED. ID Token previously issued by the OP to the RP passed to the Logout Endpoint as a hint about the | |
End-User's current authenticated session with the Client. This is used as an indication of the identity of the | |
End-User that the RP is requesting be logged out by the OP. | |
""" | |
id_token_hint = params.get("id_token_hint") | |
token = None | |
if id_token_hint is not None: | |
token = self._get_token(id_token_hint, request) | |
""" | |
logout_hint | |
OPTIONAL. Hint to the Authorization Server about the End-User that is logging out. The value and meaning of | |
this parameter is left up to the OP's discretion. For instance, the value might contain an email address, | |
phone number, username, or session identifier pertaining to the RP's session with the OP for the End-User. | |
(This parameter is intended to be analogous to the login_hint parameter defined in Section 3.1.2.1 of OpenID | |
Connect Core 1.0 [OpenID.Core] that is used in Authentication Requests; whereas, logout_hint is used in | |
RP-Initiated Logout Requests.) | |
""" | |
logout_hint = params.get("logout_hint") | |
if logout_hint is not None: | |
# logout hint support is not implemented. | |
pass | |
""" | |
client_id | |
OPTIONAL. OAuth 2.0 Client Identifier valid at the Authorization Server. When both client_id and id_token_hint | |
are present, the OP MUST verify that the Client Identifier matches the one used when issuing the ID Token. The | |
most common use case for this parameter is to specify the Client Identifier when post_logout_redirect_uri is | |
used but id_token_hint is not. Another use is for symmetrically encrypted ID Tokens used as id_token_hint | |
values that require the Client Identifier to be specified by other means, so that the ID Tokens can be | |
decrypted by the OP. | |
""" | |
client_id = params.get("client_id") | |
client_id_app = None | |
if client_id is not None: | |
client_id_app = self._get_client_id_app(client_id, token) | |
""" | |
post_logout_redirect_uri | |
OPTIONAL. URI to which the RP is requesting that the End-User's User Agent be redirected after a logout has | |
been performed. This URI SHOULD use the https scheme and MAY contain port, path, and query parameter | |
components; however, it MAY use the http scheme, provided that the Client Type is confidential, as defined | |
in Section 2.1 of OAuth 2.0 [RFC6749], and provided the OP allows the use of http RP URIs. The URI MAY use an | |
alternate scheme, such as one that is intended to identify a callback into a native application. The value MUST | |
have been previously registered with the OP, either using the post_logout_redirect_uris Registration parameter | |
or via another mechanism. An id_token_hint is also RECOMMENDED when this parameter is included. | |
""" | |
post_logout_redirect_uri = params.get("post_logout_redirect_uri") | |
redirect_uri = None | |
if post_logout_redirect_uri is not None: | |
redirect_uri = self._get_redirect_uri( | |
post_logout_redirect_uri, token, client_id_app | |
) | |
# If both a token and a client id were passed they must BOTH be valid for redirect to work | |
if id_token_hint is not None and client_id is not None: | |
if token is None or client_id_app is None: | |
redirect_uri = None | |
""" | |
state | |
OPTIONAL. Opaque value used by the RP to maintain state between the logout request and the callback to the | |
endpoint specified by the post_logout_redirect_uri parameter. If included in the logout request, the OP passes | |
this value back to the RP using the state parameter when redirecting the User Agent back to the RP. | |
""" | |
state = params.get("state") | |
querystring = "" | |
if state is not None: | |
querystring = "?state=" + state | |
""" | |
ui_locales | |
OPTIONAL. End-User's preferred languages and scripts for the user interface, represented as a space-separated | |
list of BCP47 [RFC5646] language tag values, ordered by preference. For instance, the value "fr-CA fr en" | |
represents a preference for French as spoken in Canada, then French (without a region designation), followed by | |
English (without a region designation). An error SHOULD NOT result if some or all of the requested locales are | |
not supported by the OpenID Provider. | |
""" | |
ui_locales = params.get("ui_locales") | |
if ui_locales is not None: | |
# ui_locales support is not implemented. | |
pass | |
if request.user.is_authenticated: | |
""" | |
TODO: At the Logout Endpoint, the OP SHOULD ask the End-User whether to log out of the OP as well. | |
Furthermore, the OP MUST ask the End-User this question if an id_token_hint was not provided or if the | |
supplied ID Token does not belong to the current OP session with the RP and/or currently logged in | |
End-User. If the End-User says "yes", then the OP MUST log out the End-User. | |
""" | |
# Oauth2AccessToken is different then the "Token" model that DRF uses | |
Oauth2AccessToken = get_access_token_model() | |
# default to OP Logout until we implement the prompt for the user. | |
user_requested_op_logout = True | |
if user_requested_op_logout: | |
# delete all DRF tokens for the user | |
Token.objects.filter(user=request.user).delete() | |
# delete all oauth tokens for the user. | |
Oauth2AccessToken.objects.filter(user=request.user).delete() | |
# OP Logout, end the django session. | |
logout(request) | |
else: | |
# application specific logout | |
if token is not None: | |
# delete all tokens for the User and app. | |
Oauth2AccessToken.objects.filter( | |
user=request.user.id, application=token.application | |
).delete() | |
if client_id_app: | |
# delete all tokens for the app. | |
Oauth2AccessToken.objects.filter( | |
user=request.user.id, application=client_id_app | |
).delete() | |
""" | |
TODO: As part of the OP logging out the End-User, the OP uses the logout mechanism(s) registered by the RPs to | |
notify any RPs logged in as that End-User that they are to likewise log out the End-User. RPs can use any of | |
OpenID Connect Session Management 1.0 [OpenID.Session], OpenID Connect Front-Channel Logout 1.0 | |
[OpenID.FrontChannel], and/or OpenID Connect Back-Channel Logout 1.0 [OpenID.BackChannel] to receive logout | |
notifications from the OP, depending upon which of these mechanisms the OP and RPs mutually support. The RP | |
initiating the logout is to be included in these notifications before the post-logout redirection defined in | |
Section 3 is performed. | |
""" | |
if redirect_uri is not None: | |
# redirect_uri will not be set unless we were able to validate the id_token_hint or client_id, and verified | |
# the post_logout_redirect_uri is valid for the app, per section 3 of the spec. | |
return HttpResponseRedirect(redirect_uri + querystring) | |
else: | |
# Until redirect is implemented we're always going to return success, even if the user is already logged out | |
return Response({}, status=status.HTTP_204_NO_CONTENT) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment