Skip to content

Instantly share code, notes, and snippets.

@dopry
Created February 10, 2023 16:47
Show Gist options
  • Save dopry/36e8cb676b08697ad26612521993b57d to your computer and use it in GitHub Desktop.
Save dopry/36e8cb676b08697ad26612521993b57d to your computer and use it in GitHub Desktop.
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