mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-03 09:43:33 +02:00
Add Login.gov *\o/*
This commit incldues a wrapper application which handles the Private Key JWT method of authenticating via OpenID Connect using the underlying pyoidc library.
This commit is contained in:
parent
331d2e575a
commit
00f87168e5
21 changed files with 1296 additions and 59 deletions
201
src/djangooidc/LICENSE
Normal file
201
src/djangooidc/LICENSE
Normal file
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2014 Andrea Biancini <andrea.biancini@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
10
src/djangooidc/NOTICE
Normal file
10
src/djangooidc/NOTICE
Normal file
|
@ -0,0 +1,10 @@
|
|||
Python module to integrate pyoic into django
|
||||
Copyright 2014 Andrea Biancini
|
||||
|
||||
This product includes software developed by
|
||||
Andrea Biancini <andrea.biancini@gmail.com>
|
||||
|
||||
Portions of this software is derived from the pyoidc project
|
||||
avaialble from
|
||||
https://github.com/rohe/pyoidc
|
||||
|
30
src/djangooidc/README.rst
Normal file
30
src/djangooidc/README.rst
Normal file
|
@ -0,0 +1,30 @@
|
|||
Django OpenID Connect (OIDC) authentication provider
|
||||
====================================================
|
||||
|
||||
This module makes it easy to integrate OpenID Connect as an authentication source in a Django project.
|
||||
|
||||
Behind the scenes, it uses Roland Hedberg's great pyoidc library.
|
||||
|
||||
Modified by JHUAPL BOSS to support Python3
|
||||
|
||||
Modified by Thomas Frössman with fixes and additional modifications.
|
||||
|
||||
A note for anyone viewing this file from the .gov repository:
|
||||
|
||||
This code has been included from its upstream counterpart in order to minimize external dependencies. Here is an excerpt from setup.py::
|
||||
|
||||
name='django-oidc-tf',
|
||||
description="""A Django OpenID Connect (OIDC) authentication backend""",
|
||||
author='Thomas Frössman',
|
||||
author_email='thomasf@jossystem.se',
|
||||
url='https://github.com/py-pa/django-oidc',
|
||||
packages=[
|
||||
'djangooidc',
|
||||
],
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
'django>=1.10',
|
||||
'oic>=0.10.0',
|
||||
],
|
||||
|
||||
It was taken from https://github.com/koriaf/django-oidc at ae4a0ba5e6bfda1495f9447a507e6f54cc056980.
|
0
src/djangooidc/__init__.py
Normal file
0
src/djangooidc/__init__.py
Normal file
82
src/djangooidc/backends.py
Normal file
82
src/djangooidc/backends.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.utils import timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OpenIdConnectBackend(ModelBackend):
|
||||
"""
|
||||
This backend checks a previously performed OIDC authentication.
|
||||
If it is OK and the user already exists in the database, it is returned.
|
||||
If it is OK and user does not exist in the database, it is created and
|
||||
returned unless setting OIDC_CREATE_UNKNOWN_USER is False.
|
||||
In all other cases, None is returned.
|
||||
"""
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
logger.debug("kwargs %s" % kwargs)
|
||||
user = None
|
||||
if not kwargs or "sub" not in kwargs.keys():
|
||||
return user
|
||||
|
||||
UserModel = get_user_model()
|
||||
username = self.clean_username(kwargs["sub"])
|
||||
if "upn" in kwargs.keys():
|
||||
username = kwargs["upn"]
|
||||
|
||||
# Some OP may actually choose to withhold some information, so we must
|
||||
# test if it is present
|
||||
openid_data = {"last_login": timezone.now()}
|
||||
if "first_name" in kwargs.keys():
|
||||
openid_data["first_name"] = kwargs["first_name"]
|
||||
if "given_name" in kwargs.keys():
|
||||
openid_data["first_name"] = kwargs["given_name"]
|
||||
if "christian_name" in kwargs.keys():
|
||||
openid_data["first_name"] = kwargs["christian_name"]
|
||||
if "family_name" in kwargs.keys():
|
||||
openid_data["last_name"] = kwargs["family_name"]
|
||||
if "last_name" in kwargs.keys():
|
||||
openid_data["last_name"] = kwargs["last_name"]
|
||||
if "email" in kwargs.keys():
|
||||
openid_data["email"] = kwargs["email"]
|
||||
|
||||
# Note that this could be accomplished in one try-except clause, but
|
||||
# instead we use get_or_create when creating unknown users since it has
|
||||
# built-in safeguards for multiple threads.
|
||||
if getattr(settings, "OIDC_CREATE_UNKNOWN_USER", True):
|
||||
args = {
|
||||
UserModel.USERNAME_FIELD: username,
|
||||
"defaults": openid_data,
|
||||
}
|
||||
user, created = UserModel.objects.update_or_create(**args)
|
||||
if created:
|
||||
user = self.configure_user(user, **kwargs)
|
||||
else:
|
||||
try:
|
||||
user = UserModel.objects.get_by_natural_key(username)
|
||||
except UserModel.DoesNotExist:
|
||||
try:
|
||||
user = UserModel.objects.get(email=kwargs["email"])
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
return user
|
||||
|
||||
def clean_username(self, username):
|
||||
"""
|
||||
Performs any cleaning on the "username" prior to using it to get or
|
||||
create the user object. Returns the cleaned username.
|
||||
"""
|
||||
return username
|
||||
|
||||
def configure_user(self, user, **kwargs):
|
||||
"""
|
||||
Configures a user after creation and returns the updated user.
|
||||
"""
|
||||
user.set_unusable_password()
|
||||
return user
|
46
src/djangooidc/exceptions.py
Normal file
46
src/djangooidc/exceptions.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from oic import rndstr
|
||||
from http import HTTPStatus as status
|
||||
|
||||
|
||||
class OIDCException(Exception):
|
||||
"""
|
||||
Base class for django oidc exceptions.
|
||||
Subclasses should provide `.status` and `.friendly_message` properties.
|
||||
`.locator`, if used, should be a useful, unique identifier for
|
||||
locating related log messages.
|
||||
"""
|
||||
status = status.INTERNAL_SERVER_ERROR
|
||||
friendly_message = "A server error occurred."
|
||||
locator = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
friendly_message=None,
|
||||
status=None,
|
||||
locator=None
|
||||
):
|
||||
if friendly_message is not None:
|
||||
self.friendly_message = friendly_message
|
||||
if status is not None:
|
||||
self.status = status
|
||||
if locator is not None:
|
||||
self.locator = locator
|
||||
else:
|
||||
self.locator = rndstr(size=12)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.locator}] {self.friendly_message}"
|
||||
|
||||
|
||||
class AuthenticationFailed(OIDCException):
|
||||
status = status.UNAUTHORIZED
|
||||
friendly_message = "This login attempt didn't work."
|
||||
|
||||
|
||||
class InternalError(OIDCException):
|
||||
status = status.INTERNAL_SERVER_ERROR
|
||||
friendly_message = "The system broke while trying to log you in."
|
||||
|
||||
|
||||
class BannedUser(AuthenticationFailed):
|
||||
friendly_message = "Your user is not valid in this application."
|
277
src/djangooidc/oidc.py
Normal file
277
src/djangooidc/oidc.py
Normal file
|
@ -0,0 +1,277 @@
|
|||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import json
|
||||
|
||||
try:
|
||||
from builtins import unicode as str
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseRedirect
|
||||
from Cryptodome.PublicKey.RSA import importKey
|
||||
from jwkest.jwk import RSAKey
|
||||
from oic import oic, rndstr
|
||||
from oic.oauth2 import ErrorResponse
|
||||
from oic.oic import AuthorizationRequest, AuthorizationResponse, RegistrationResponse
|
||||
from oic.oic.message import AccessTokenResponse
|
||||
from oic.utils.authn.client import CLIENT_AUTHN_METHOD
|
||||
from oic.utils import keyio
|
||||
|
||||
from . import exceptions as o_e
|
||||
|
||||
__author__ = "roland"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Client(oic.Client):
|
||||
def __init__(self, op):
|
||||
"""Step 1: Configure the OpenID Connect client."""
|
||||
logger.debug("Initializing the OpenID Connect client...")
|
||||
try:
|
||||
provider = settings.OIDC_PROVIDERS[op]
|
||||
verify_ssl = getattr(settings, "OIDC_VERIFY_SSL", True)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error("Configuration missing for OpenID Connect client")
|
||||
raise o_e.InternalError()
|
||||
|
||||
try:
|
||||
# prepare private key for authentication method of private_key_jwt
|
||||
key_bundle = keyio.KeyBundle()
|
||||
rsa_key = importKey(provider["client_registration"]["sp_private_key"])
|
||||
key = RSAKey(key=rsa_key, use="sig")
|
||||
key_bundle.append(key)
|
||||
keyjar = keyio.KeyJar(verify_ssl=verify_ssl)
|
||||
keyjar.add_kb("", key_bundle)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error(
|
||||
"Key jar preparation failed for %s", provider["srv_discovery_url"],
|
||||
)
|
||||
raise o_e.InternalError()
|
||||
|
||||
try:
|
||||
# create the oic client instance
|
||||
super().__init__(
|
||||
client_id=None,
|
||||
client_authn_method=CLIENT_AUTHN_METHOD,
|
||||
keyjar=keyjar,
|
||||
verify_ssl=verify_ssl,
|
||||
config=None,
|
||||
)
|
||||
# must be set after client is initialized
|
||||
self.behaviour = provider["behaviour"]
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error(
|
||||
"Client creation failed for %s", provider["srv_discovery_url"],
|
||||
)
|
||||
raise o_e.InternalError()
|
||||
|
||||
try:
|
||||
# discover and store the provider (OP) urls, etc
|
||||
self.provider_config(provider["srv_discovery_url"])
|
||||
self.store_registration_info(
|
||||
RegistrationResponse(**provider["client_registration"])
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error(
|
||||
"Provider info discovery failed for %s", provider["srv_discovery_url"],
|
||||
)
|
||||
raise o_e.InternalError()
|
||||
|
||||
def create_authn_request(
|
||||
self,
|
||||
session,
|
||||
extra_args=None,
|
||||
):
|
||||
"""Step 2: Construct a login URL at OP's domain and send the user to it."""
|
||||
logger.debug("Creating the OpenID Connect authn request...")
|
||||
state = rndstr(size=32)
|
||||
try:
|
||||
session["state"] = state
|
||||
session["nonce"] = rndstr(size=32)
|
||||
scopes = list(self.behaviour.get("scope", []))
|
||||
scopes.append("openid")
|
||||
request_args = {
|
||||
"response_type": self.behaviour.get("response_type"),
|
||||
"scope": " ".join(set(scopes)),
|
||||
"state": session["state"],
|
||||
"nonce": session["nonce"],
|
||||
"redirect_uri": self.registration_response["redirect_uris"][0],
|
||||
"acr_values": self.behaviour.get("acr_value")
|
||||
}
|
||||
|
||||
if extra_args is not None:
|
||||
request_args.update(extra_args)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error("Failed to assemble request arguments for %s" % state)
|
||||
raise o_e.InternalError(locator=state)
|
||||
|
||||
logger.debug("request args: %s" % request_args)
|
||||
|
||||
try:
|
||||
# prepare the request for sending
|
||||
cis = self.construct_AuthorizationRequest(request_args=request_args)
|
||||
logger.debug("request: %s" % cis)
|
||||
|
||||
# obtain the url and headers from the prepared request
|
||||
url, body, headers, cis = self.uri_and_body(
|
||||
AuthorizationRequest,
|
||||
cis,
|
||||
method="GET",
|
||||
request_args=request_args,
|
||||
)
|
||||
logger.debug("body: %s" % body)
|
||||
logger.debug("URL: %s" % url)
|
||||
logger.debug("headers: %s" % headers)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error("Failed to prepare request for %s" % state)
|
||||
raise o_e.InternalError(locator=state)
|
||||
|
||||
try:
|
||||
# create the redirect object
|
||||
response = HttpResponseRedirect(str(url))
|
||||
# add headers to the object, if any
|
||||
if headers:
|
||||
for key, value in headers.items():
|
||||
response[key] = value
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error("Failed to create redirect object for %s" % state)
|
||||
raise o_e.InternalError(locator=state)
|
||||
|
||||
return response
|
||||
|
||||
def callback(self, unparsed_response, session):
|
||||
"""Step 3: Receive OP's response, request an access token, and user info."""
|
||||
logger.debug("Processing the OpenID Connect callback response...")
|
||||
state = session.get("state", "")
|
||||
try:
|
||||
# parse the response from OP
|
||||
authn_response = self.parse_response(
|
||||
AuthorizationResponse,
|
||||
unparsed_response,
|
||||
sformat="dict",
|
||||
keyjar=self.keyjar
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error("Unable to parse response for %s" % state)
|
||||
raise o_e.AuthenticationFailed(locator=state)
|
||||
|
||||
# ErrorResponse is not raised, it is passed back...
|
||||
if isinstance(authn_response, ErrorResponse):
|
||||
error = authn_response.get("error", "")
|
||||
if error == "login_required":
|
||||
logger.warning("User was not logged in (%s), trying again for %s" % (error, state))
|
||||
return self.create_authn_request(session)
|
||||
else:
|
||||
logger.error("Unable to process response %s for %s" % (error, state))
|
||||
raise o_e.AuthenticationFailed(locator=state)
|
||||
|
||||
logger.debug("authn_response %s" % authn_response)
|
||||
|
||||
if not authn_response.get("state", None):
|
||||
logger.error("State value not received from OP for %s" % state)
|
||||
raise o_e.AuthenticationFailed(locator=state)
|
||||
|
||||
if authn_response["state"] != session.get("state", None):
|
||||
# this most likely means the user's Django session vanished
|
||||
logger.error("Received state not the same as expected for %s" % state)
|
||||
raise o_e.AuthenticationFailed(locator=state)
|
||||
|
||||
if self.behaviour.get("response_type") == "code":
|
||||
try:
|
||||
# request a new token by which we may interact with OP
|
||||
# on behalf of the user
|
||||
token_response = self.do_access_token_request(
|
||||
scope="openid",
|
||||
state=authn_response["state"],
|
||||
request_args={
|
||||
"code": authn_response["code"],
|
||||
"redirect_uri": self.registration_response["redirect_uris"][0],
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret,
|
||||
},
|
||||
authn_method=self.registration_response[
|
||||
"token_endpoint_auth_method"
|
||||
],
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error("Unable to obtain access token for %s" % state)
|
||||
raise o_e.AuthenticationFailed(locator=state)
|
||||
|
||||
# ErrorResponse is not raised, it is passed back...
|
||||
if isinstance(token_response, ErrorResponse):
|
||||
logger.error("Unable to get token (%s) for %s" % (token_response.get("error",""), state))
|
||||
raise o_e.AuthenticationFailed(locator=state)
|
||||
|
||||
logger.debug("token response %s" % token_response)
|
||||
|
||||
try:
|
||||
# get the token and other bits of info
|
||||
id_token = token_response["id_token"]._dict
|
||||
|
||||
if id_token["nonce"] != session["nonce"]:
|
||||
logger.error("Received nonce not the same as expected for %s" % state)
|
||||
raise o_e.AuthenticationFailed(locator=state)
|
||||
|
||||
session["id_token"] = id_token
|
||||
session["id_token_raw"] = getattr(self, "id_token_raw", None)
|
||||
session["access_token"] = token_response["access_token"]
|
||||
session["refresh_token"] = token_response.get("refresh_token", "")
|
||||
session["expires_in"] = token_response.get("expires_in", "")
|
||||
self.id_token[authn_response["state"]] = getattr(self, "id_token_raw", None)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error("Unable to parse access token response for %s" % state)
|
||||
raise o_e.AuthenticationFailed(locator=state)
|
||||
|
||||
scopes = list(self.behaviour.get("user_info_request", []))
|
||||
scopes.append("openid")
|
||||
|
||||
try:
|
||||
# get info about the user from OP
|
||||
info_response = self.do_user_info_request(
|
||||
state=session["state"],
|
||||
method="GET",
|
||||
scope=" ".join(set(scopes)),
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error("Unable to request user info for %s" % state)
|
||||
raise o_e.AuthenticationFailed(locator=state)
|
||||
|
||||
# ErrorResponse is not raised, it is passed back...
|
||||
if isinstance(info_response, ErrorResponse):
|
||||
logger.error("Unable to get user info (%s) for %s" % (info_response.get("error",""), state))
|
||||
raise o_e.AuthenticationFailed(locator=state)
|
||||
|
||||
user_info = info_response.to_dict()
|
||||
logger.debug("user info: %s" % user_info)
|
||||
|
||||
return user_info
|
||||
|
||||
def store_response(self, resp, info):
|
||||
"""Make raw ID token available for internal use."""
|
||||
if isinstance(resp, AccessTokenResponse):
|
||||
info = json.loads(info)
|
||||
self.id_token_raw = info["id_token"]
|
||||
|
||||
super(Client, self).store_response(resp, info)
|
||||
|
||||
def __repr__(self):
|
||||
return "Client {} {} {}".format(
|
||||
self.client_id,
|
||||
self.client_prefs,
|
||||
self.behaviour,
|
||||
)
|
12
src/djangooidc/urls.py
Normal file
12
src/djangooidc/urls.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
# coding: utf-8
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("login/", views.openid, name="openid"),
|
||||
path("callback/login/", views.login_callback, name="openid_login_callback"),
|
||||
path("logout/", views.logout, name="logout"),
|
||||
path("callback/logout/", views.logout_callback, name="openid_logout_callback"),
|
||||
]
|
116
src/djangooidc/views.py
Normal file
116
src/djangooidc/views.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
# coding: utf-8
|
||||
|
||||
import logging
|
||||
|
||||
try:
|
||||
from urllib.parse import parse_qs, urlencode
|
||||
except ImportError:
|
||||
from urllib import urlencode
|
||||
from urlparse import parse_qs
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render
|
||||
from djangooidc.oidc import Client
|
||||
from djangooidc import exceptions as o_e
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialize provider using pyOICD
|
||||
OP = getattr(settings, "OIDC_ACTIVE_PROVIDER")
|
||||
CLIENT = Client(OP)
|
||||
logger.debug("client initialized %s" % CLIENT)
|
||||
|
||||
|
||||
def error_page(request, error):
|
||||
"""Display a sensible message and log the error."""
|
||||
logger.error(error)
|
||||
if isinstance(error, o_e.AuthenticationFailed):
|
||||
return render(
|
||||
request,
|
||||
"401.html",
|
||||
context={
|
||||
"friendly_message": error.friendly_message,
|
||||
"log_identifier": error.locator
|
||||
},
|
||||
status=401
|
||||
)
|
||||
if isinstance(error, o_e.InternalError):
|
||||
return render(
|
||||
request,
|
||||
"500.html",
|
||||
context={
|
||||
"friendly_message": error.friendly_message,
|
||||
"log_identifier": error.locator
|
||||
},
|
||||
status=500
|
||||
)
|
||||
if isinstance(error, Exception):
|
||||
return render(request, "500.html", status=500)
|
||||
|
||||
def openid(request):
|
||||
"""Redirect the user to an authentication provider (OP)."""
|
||||
request.session["next"] = request.GET.get("next", "/")
|
||||
|
||||
try:
|
||||
return CLIENT.create_authn_request(request.session)
|
||||
except Exception as err:
|
||||
return error_page(request, err)
|
||||
|
||||
def login_callback(request):
|
||||
"""Analyze the token returned by the authentication provider (OP)."""
|
||||
try:
|
||||
query = parse_qs(request.GET.urlencode())
|
||||
userinfo = CLIENT.callback(query, request.session)
|
||||
user = authenticate(request=request, **userinfo)
|
||||
if user:
|
||||
login(request, user)
|
||||
logger.info("Successfully logged in user %s" % user)
|
||||
return redirect(request.session.get("next", "/"))
|
||||
else:
|
||||
raise o_e.BannedUser()
|
||||
except Exception as err:
|
||||
return error_page(request, err)
|
||||
|
||||
|
||||
def logout(request, next_page=None):
|
||||
"""Redirect the user to the authentication provider (OP) logout page."""
|
||||
try:
|
||||
username = request.user.username
|
||||
request_args = {
|
||||
# it is perfectly fine to send the token, even if it is expired
|
||||
"id_token_hint": request.session["id_token_raw"],
|
||||
"state": request.session["state"],
|
||||
}
|
||||
if (
|
||||
"post_logout_redirect_uris" in CLIENT.registration_response.keys()
|
||||
and len(CLIENT.registration_response["post_logout_redirect_uris"]) > 0
|
||||
):
|
||||
request_args.update({
|
||||
"post_logout_redirect_uri":
|
||||
CLIENT.registration_response["post_logout_redirect_uris"][0]
|
||||
})
|
||||
|
||||
|
||||
url = CLIENT.provider_info["end_session_endpoint"]
|
||||
url += "?" + urlencode(request_args)
|
||||
return HttpResponseRedirect(url)
|
||||
except Exception as err:
|
||||
return error_page(request, err)
|
||||
finally:
|
||||
# Always remove Django session stuff - even if not logged out from OP.
|
||||
# Don't wait for the callback as it may never come.
|
||||
auth_logout(request)
|
||||
logger.info("Successfully logged out user %s" % username)
|
||||
next_page = getattr(settings, "LOGOUT_REDIRECT_URL", None)
|
||||
if next_page:
|
||||
request.session["next"] = next_page
|
||||
|
||||
def logout_callback(request):
|
||||
"""Simple redirection view: after logout, redirect to `next`."""
|
||||
next = request.session["next"] if "next" in request.session.keys() else "/"
|
||||
return redirect(next)
|
Loading…
Add table
Add a link
Reference in a new issue