FAQ | This is a LIVE service | Changelog

Skip to content
Snippets Groups Projects
Commit d4bdf5d1 authored by Dr Rich Wareham's avatar Dr Rich Wareham
Browse files

provide an initial implementation

Provides a MVP implementation of a login and consent webapp. The
emphasis here is on the "M". In particular we hard-code the list of
supported scopes pending some API Gateway-side work (#3).

The actual functionality of the app is in login/views.py and
login/gateway.py. The remainder of the app is various bits of Django
ceremony to wire those things together.

I've tried to minimise the required settings. In particular we make use
of the OpenID discovery endpoint to configure a lot of the
OAuth2-related settings.

A required setting includes the expected audience for the session id JWT
from the Gateway. It is tempting to form this dynamically from the
incoming request but that puts something which is used for token
verification under the control of the incoming request's headers which
risks confused-deputy style attacks[1].

Despite being functional and deployable as is, this app needs some
further work.

We implement the webapp using the traditional Project Light templates
for speed of implementation. The UI needs a designer's eye (#4).

If the requested scopes include "profile" then we fetch very basic user
claims from Lookup. This is essentially just the display name at the
moment. We can add given names, surnames, profile pictures, etc at a
later date (#1).

Most importantly, I think this app is an opportunity to require 2SV from
the get-go for applications using the API Gateway. I've opened (#2) to
track this since it's not part of the MVP but I think it'd be a good
thing to implement ASAP.

Part of https://gitlab.developers.cam.ac.uk/uis/devops/api/gateway-ops/-/issues/66

[1] https://en.wikipedia.org/wiki/Confused_deputy_problem
parent 9bcfef2f
No related branches found
No related tags found
No related merge requests found
Pipeline #114608 passed with warnings
Showing
with 577 additions and 72 deletions
......@@ -39,6 +39,10 @@ RUN \
EXTERNAL_SETTING_DATABASES="{'default': {}}" \
EXTERNAL_SETTING_SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=placeholder \
EXTERNAL_SETTING_SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=placeholder \
EXTERNAL_SETTING_GATEWAY_OPENID_CONFIGURATION_URL=placeholder \
EXTERNAL_SETTING_GATEWAY_CLIENT_ID=placeholder \
EXTERNAL_SETTING_GATEWAY_CLIENT_SECRET=placeholder \
EXTERNAL_SETTING_GATEWAY_JWT_AUDIENCE=https://placeholder.example.invalid/ \
./manage.py collectstatic
ENTRYPOINT ["./docker-entrypoint.sh"]
LICENSE 0 → 100644
MIT License
Copyright (c) 2021 University of Cambridge Information Services
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Django Boilerplate Login and Consent
**THIS README NEEDS COMPLETING**
This repository contains [...] which does [...] in order to [...].
## Documentation
The project has detailed documentation for developers, including a "getting
started" guide. See below for information on building the documentation.
## Guidebook - Quickstart and Testing
To know how to get started with this project visit our [Guidebook](https://guidebook.devops.uis.cam.ac.uk/en/latest/notes/webapp-dev-environment/).
To view notes on how to get the project working locally visit the [getting started section](https://guidebook.devops.uis.cam.ac.uk/en/latest/notes/webapp-dev-environment/#getting-started-with-a-project) of the Guidebook.
To get started with Testing visit our [testing section](https://guidebook.devops.uis.cam.ac.uk/en/latest/notes/webapp-dev-environment/#running-tests) of the Guidebook.
## Loading secrets at runtime
If the `EXTRA_SETTINGS_URLS` environment variable is non-empty it is interpreted
as a comma-separated set of URLs from which to fetch settings. Settings are
fetched and applied in the order they are listed.
The settings should be in the form of a YAML document which is fetched, parsed
and interpolated into the Django settings when the server starts.
`EXTRA_SETTINGS_URLS` currently understands the following URL schemes:
* file://... URLs are loaded from the local file system. If the URL
lacks any scheme, it is assumed to be a file URL.
* https://... URLs are fetched using HTTP over TLS.
* gs://BUCKET/LOCATION formatted URLs specify a Google Cloud Storage
bucket and a location within that bucket of an object to load settings
from.
* sm://PROJECT/SECRET#VERSION formatted URLs specify a Google Secret
Manager secret to load settings from. If the version is omitted, the
latest version is used.
For Google Cloud Storage and Secret Manager URLs, application default
credentials are used to authenticate to Google.
Settings which can be loaded from external YAML documents can also be specified
in environment variables. A variable of the form EXTERNAL_SETTING_[NAME] is
imported as the setting "NAME" and the value of the variable is interpreted as a
YAML formatted value for the setting.
## Notes on debugging
The Full-screen console debugger `pudb` has been included to allow you to run a debug in the
docker-compose environment. To use, simply set the breakpoint using `import pdb; pdb.set_trace()`
and attach to the container using:
```bash
docker attach login_and_consent_development_app_1
```
For a fuller description of how to debug follow the
[guide to debugging with pdb and Docker](https://blog.lucasferreira.org/howto/2017/06/03/running-pdb-with-docker-and-gunicorn.html)
(it works just as well for `pudb`).
## CI configuration
The project is configured with Gitlab AutoDevOps via Gitlab CI using the .gitlab-ci.yml file.
# Login and consent application for the API Gateway
This application implements the login and consent UI for the API Gateway.
End-user login for the Gateway is de-coupled from the Apigee configuration
itself by means of the [login and consent
API](https://gitlab.developers.cam.ac.uk/uis/devops/api/gateway-ops/-/blob/master/doc/oauth2-login-api/openapi.yaml).
In brief, when the Gateway requires an end-user login, it redirects to this
application passing a signed JWT containing an opaque login session id. This
application authenticates the current user, retrieves information on the
in-flight login session via the login and consent API and gets user consent.
Depending on the user's response the login session is allowed to proceed or is
terminated.
## Developer Quickstart and Testing
To know how to get started with this project visit our
[Guidebook](https://guidebook.devops.uis.cam.ac.uk/en/latest/notes/webapp-dev-environment/).
To view notes on how to get the project working locally visit the [getting
started
section](https://guidebook.devops.uis.cam.ac.uk/en/latest/notes/webapp-dev-environment/#getting-started-with-a-project)
of the Guidebook. Note that some extra secrets are required. See the relevant
section below.
To get started with running unit tests visit our [testing
section](https://guidebook.devops.uis.cam.ac.uk/en/latest/notes/webapp-dev-environment/#running-tests)
of the Guidebook.
### Testing the login flow
The API Gateway operations project contains instructions for [manually testing
the login
flow](https://gitlab.developers.cam.ac.uk/uis/devops/api/gateway-ops/-/blob/master/doc/oauth2-design-and-implementation.md#user-content-manually-testing-the-flows).
Initiating the authorisation code flow via the `/auth` endpoint should redirect
to the login and consent app with a URL of the form
`https://login.example.com/?token=...`. Rewriting this URL to be
`https://localhost:8000/?token=...` lets you test the login flow using the local
application.
## Required settings
See the [login module docstring](login/__init__.py) for a list of required
settings for the login and consent module. In addition you will need to
[configure OAuth2
credentials](https://guidebook.devops.uis.cam.ac.uk/en/latest/notes/webapp-dev-environment/#google-oauth2-credentials)
for the application along with Lookup credentials.
The [secrets.env.in](secrets.env.in) file contains a template configuration. For
developers within UIS, a [secrets.env file suitable for use in local
development](https://start.1password.com/open/i?a=D3ATZUD36RDHLDKSVJUQZWGORQ&v=rzdugks5meinz5oc772yudf3ra&i=5kwf5inds3zvae3csnlf7c4lne&h=uis-devops.1password.eu)
is kept in 1password.
## Copyright License
......
......@@ -3,5 +3,9 @@ TOXINI_ARTEFACT_DIR=/tmp/tox-data/artefacts
TOXINI_SITEPACKAGES=True
COVERAGE_FILE=/tmp/tox-data/coverage
EXTERNAL_SETTING_SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET='fake-secret'
EXTERNAL_SETTING_SOCIAL_AUTH_GOOGLE_OAUTH2_KEY='fake-key'
EXTERNAL_SETTING_SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=fake-secret
EXTERNAL_SETTING_SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=fake-key
EXTERNAL_SETTING_GATEWAY_JWT_AUDIENCE=https://login.example.invalid/
EXTERNAL_SETTING_GATEWAY_OPENID_CONFIGURATION_URL=https://gateway.example.invalid/oauth2/v1/.well-known/openid-configuration
EXTERNAL_SETTING_GATEWAY_CLIENT_ID=my-exciting-client
EXTERNAL_SETTING_GATEWAY_CLIENT_SECRET=my-exciting-client-secret
......@@ -31,7 +31,7 @@ django.setup()
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.githubpages']
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.githubpages', 'sphinx.ext.napoleon']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
......
Login and Consent
===============================================================================
Login and Consent Application
=============================
.. automodule:: login
:members:
Installation
````````````
......@@ -19,6 +22,19 @@ Default URL routing
.. automodule:: login.urls
:members:
Integration with the API Gateway
````````````````````````````````
.. automodule:: login.gateway
:members:
Scopes
``````
.. automodule:: login.scopes
:members:
Application configuration
`````````````````````````
......
"""
Login and Consent Application
This application implements the login and consent UI for the API Gateway.
End-user login for the Gateway is de-coupled from the Apigee configuration
itself by means of the `login and consent
API
<https://gitlab.developers.cam.ac.uk/uis/devops/api/gateway-ops/-/blob/master/doc/oauth2-login-api/openapi.yaml>`_.
Required settings
-----------------
The following settings are *required*:
* **GATEWAY_CLIENT_ID** Client id of login and consent application as registered with the API
Gateway.
* **GATEWAY_CLIENT_SECRET** Client secret of login and consent application as registered with the
API Gateway.
* **GATEWAY_JWT_AUDIENCE** Expected audience claim for signed session JWT from the API Gateway.
* **GATEWAY_OPENID_CONFIGURATION_URL** URL pointing to the OpenID discovery document for the API
Gateway.
Optional settings
-----------------
The following settings are *optional*:
* **GATEWAY_LOGIN_SCOPE** Scope to request when authenticating to the API Gayeway in order to
process logins. Defaults to `https://api.apps.cam.ac.uk/oauth2/logins`.
"""
default_app_config = 'login.apps.Config'
......@@ -6,3 +6,4 @@ Default settings values for the :py:mod:`login` application.
# used as default settings for the assets application. See .apps.Config how this is achieved. This
# is a bit mucky but, at the moment, Django does not have a standard way to specify default values
# for settings. See: https://stackoverflow.com/questions/8428556/
GATEWAY_LOGIN_SCOPE = 'https://api.apps.cam.ac.uk/oauth2/logins'
"""
API-gateway related operations.
"""
import json
import logging
import urllib.parse
import jwcrypto.common
import jwcrypto.jwk
import jwcrypto.jwt
import pydantic
import requests
import requests_auth
import requests_cache
from django.conf import settings
LOG = logging.getLogger(__name__)
def decode_session_token(unverified_token, expected_audience):
"""
Decode and verify a login session token.
Args:
unverified_token (str): unverified JWT
Returns:
str: The verified session id.
Raises:
jwcrypto.common.JWException: token is invalid
requests.HTTPError: error fetching public keys
"""
oidc_config = _get_oidc_configuration()
jwt = jwcrypto.jwt.JWT(
algs=['RS256'],
check_claims={
'iss': oidc_config['issuer'],
'aud': expected_audience,
'sub': None, # == "must be present"
'exp': None, # == "must be present"
}
)
jwt.deserialize(unverified_token, key=_get_jwks())
return json.loads(jwt.claims)['sub']
class SessionInfo(pydantic.BaseModel):
"""
A subset of the information returned by the /login/{sessionid} endpoint.
"""
#: Human-friendly name for application requesting login.
app_name: str
#: Redirect URI for application.
redirect_uri: str
#: Space-separated list of requested scopes.
req_scope: str
class APIError(RuntimeError):
"""
Exception raised when there is a problem with a response from the API Gateway.
"""
def get_session(session_id) -> SessionInfo:
"""
Retrieve an in-flight login session.
Args:
session_id (str): Session id as passed from API Gateway.
Returns:
SessionInfo: Information about the in-flight session.
"""
resp = _get_api_gateway_session().get(_oauth2_endpoint(f'login/{session_id}'))
resp.raise_for_status()
return SessionInfo(**(resp.json()))
def delete_session(session_id):
"""
Delete (invalidate) in-flight login session.
Args:
session_id (str): Session id as passed from API Gateway.
"""
resp = _get_api_gateway_session().delete(_oauth2_endpoint(f'login/{session_id}'))
resp.raise_for_status()
def proceed_with_session(session_id, scope, user_claims):
"""
Approve an in-flight login session to proceed.
Args:
session_id (str): Session id as passed from API Gateway.
scope (str): Space separated list of *approved* scopes.
user_claims (dict): User claims to associated with id token.
Raises:
APIError: the response from the gateway was not a redirect.
Returns:
str: the URL to redirect back to the application.
"""
query = urllib.parse.urlencode({'scope': scope})
resp = _get_api_gateway_session().post(
_oauth2_endpoint(f'login/{session_id}/proceed?{query}'),
json=user_claims, allow_redirects=False)
resp.raise_for_status()
if not resp.is_redirect:
LOG.error('Expected redirect from gateway. Got: %r', resp.status_code)
raise APIError('Expected redirect')
return resp.headers['location']
def _oauth2_endpoint(path):
"""
Return full URL to OAuth2 API endpoint given path.
"""
# For the moment we assume that the OAuth2 API is located where the token endpoint is.
oidc_config = _get_oidc_configuration()
return urllib.parse.urljoin(oidc_config['token_endpoint'], path)
def _get_jwks():
"""
Return a jwcrypto.jwk.JWKSet corresponding to the public gateway keys.
"""
# Fetch the JWKS as specified in the discovery document and parse.
r = _get_cached_requests_session().get(_get_oidc_configuration()['jwks_uri'])
r.raise_for_status()
return jwcrypto.jwk.JWKSet.from_json(r.content)
def _get_oidc_configuration():
"""
Return a parsed OpenID discovery document.
"""
r = _get_cached_requests_session().get(settings.GATEWAY_OPENID_CONFIGURATION_URL)
r.raise_for_status()
return r.json()
def _get_api_gateway_session():
"""
Returns a Requests session authenticated against the API gateway. Auth token is cached so
it is safe to call this multiple times.
"""
oidc_configuration = _get_oidc_configuration()
session = requests.Session()
# NB: requests_auth performs transparent token cacheing.
session.auth = requests_auth.OAuth2ClientCredentials(
token_url=oidc_configuration['token_endpoint'],
client_id=settings.GATEWAY_CLIENT_ID,
client_secret=settings.GATEWAY_CLIENT_SECRET,
scope=settings.GATEWAY_LOGIN_SCOPE
)
return session
def _get_cached_requests_session():
"""
Return a requests.Session object which includes cacheing suitable for use with JWKS and OpenID
discovery endpoints.
"""
# Default is to expire after 300s == 5m. We're unlikely to rotate keys faster than that(!)
return requests_cache.CachedSession(
backend='memory', namespace='login-jwks', expire_after=300)
"""
List of supported scopes.
In the fullness of time we need a better way of handling scopes. Be that adding a model to this
application so they can be added in the database or adding an endpoint to the Gateway allowing
scope information to be retrieved.
For the moment, hard-code the list of scopes until we decide which is the best option moving
forward.
"""
#: Known scopes. Descriptions should finish the sentence "the application would like to use this
#: service to..." but *not* include the ending ".".
SCOPES = {
"profile": {
"service": "lookup",
"description": (
"know basic personal information such as your display name and University user id"
),
},
"email": {
"service": "lookup",
"description": "know your @cam.ac.uk email address",
},
}
#: Descriptions of services. Descriptions should be full English sentences along with the ending
#: ".".
SERVICES = {
"lookup": {
"name": "Lookup",
"description": (
"The University Lookup directory is a simple database containing information "
"about every person and every University institution known to University Information "
"Services, together with a small number of University-related institutions."
),
"help_url": "https://help.uis.cam.ac.uk/service/collaboration/lookup",
},
}
......@@ -14,7 +14,10 @@ from django.core.checks import register, Error
REQUIRED_SETTINGS = [
# ... TODO: add any required settings here
'GATEWAY_OPENID_CONFIGURATION_URL',
'GATEWAY_CLIENT_ID',
'GATEWAY_CLIENT_SECRET',
'GATEWAY_JWT_AUDIENCE',
]
......
{% extends 'login/_layout.html' %}
{% block content %}
<h1>Error 400: Bad Request</h1>
<p>This application was sent a request it did not understand.</p>
{% endblock %}
{% extends 'login/_layout.html' %}
{% block content %}
<h1>Error 403: Forbidden</h1>
<p>Your account is not permitted to use this service.</p>
{% endblock %}
{% extends 'login/_layout.html' %}
{% block content %}
<h1>Error 404: Not Found</h1>
<p>The requested resource could not be found.</p>
{% endblock %}
{% extends 'login/_layout.html' %}
{% block content %}
<h1>Error 500: Internal Server Error</h1>
<p>An error occurred handling the request. Administrators have been informed.</p>
{% endblock %}
{% extends 'ucamprojectlight.html' %}
{% block head_title %}Application Gateway{% endblock %}
{% block site_name %}Application Gateway{% endblock %}
{% block search_bar %}{% endblock %}
{% block campl_tabs_header %}{% endblock %}
{% block local_footer %}{% endblock %}
{% block static_breadcrumbs %}
<li><a href="http://www.cam.ac.uk/">University of Cambridge</a></li>
<li><a href="http://developer.api.apps.cam.ac.uk/">Application Gateway</a></li>
{% endblock %}
{% block page_content %}
<div class="campl-column12 campl-main-content">
<div class="campl-content-container">
{% block content %}{% endblock %}
</div>
</div>
{% endblock %}
{% extends 'login/_layout.html' %}
{% block content %}
<h1>Hello, {{ person.displayName }}</h1>
<p>
You are signed in as <strong>{{ person.displayName }} ({{ crsid }})</strong>.
If this isn't you, please
<a href="/accounts/logout?next={{ request.get_full_path | urlencode }}">sign
in with a different account</a>.
</p>
<p>
<strong>{{ login_session.app_name }}</strong>, hosted at
<strong>{{ app_url }}</strong>, is requesting access to the following
University services on your behalf.
Only allow this request to proceed if you trust the application.
</p>
{% for consent in consents %}
<h2>{{ consent.info.name }}</h2>
<p>
{{ consent.info.description }}
{% if consent.info.help_url %}
<em><a href="{{ consent.info.help_url }}">More information.</a></em>
{% endif %}
</p>
<p>The application would like to use this service to:</p>
<ul>
{% for scope in consent.scopes %}
<li>
{% if forloop.revcounter0 == 0 %}
<strong>{{ scope.description }}</strong>.
{% elif forloop.revcounter0 == 1 %}
<strong>{{ scope.description }}</strong>, and
{% else %}
<strong>{{ scope.description }}</strong>,
{% endif %}
</li>
{% endfor %}
</ul>
{% endfor %}
<form method="post" action="{% url 'login-consent' %}">
{% csrf_token %}
<fieldset>
<div class="campl-form-actions">
<input type="submit" name="allow" value="Allow access to these services" class="campl-btn" />
<input type="submit" name="deny" autofocus value="Deny this time" class="campl-btn" />
</div>
</fieldset>
</form>
{% endblock %}
{% extends 'login/_layout.html' %}
{% block content %}
<h1>You have denied consent this time</h1>
<p>
If the application requires your consent again, it will ask.
</p>
{% endblock %}
import json
import unittest.mock as mock
import faker
import jwcrypto.jwk
import jwcrypto.jwt
import requests
from django.conf import settings
class MockOpenIDConfigurationMixin:
"""
Mixin which mocks the OIDC discovery process used by login.gateway.
"""
def set_up_oidc_config_mock(self):
"""
Install mocks and register cleanup handlers.
Initialises:
self.faker (if not present)
self.key - JWK signing key
self.keyset - JWKS for issuer
self.oauth2_base_url - Base URL for OAuth API
self.oidc_configuration - OIDC discovery document returned by OIDC configuration
endpoint
self.cached_session_mock - magic mock representing the cacheing requests.Session
used to fetch OIDC configuration and JWKS keys
The get() method of self.cached_session_mock is mocked to return appropriate mock responses
for the OIDC URLs.
"""
if not hasattr(self, 'faker'):
self.faker = faker.Faker()
# Construct a key id, key and keyset
self.key = jwcrypto.jwk.JWK.generate(
kty='RSA', size=2048, kid=self.faker.password(special_chars=False))
self.keyset = jwcrypto.jwk.JWKSet()
self.keyset['keys'].add(self.key)
# Construct a mock discovery document.
self.oauth2_base_url = 'https://gateway.example.invalid/oauth2/v1/'
self.oidc_configuration = {
'issuer': self.oauth2_base_url.rstrip('/'),
'authorization_endpoint': f'{self.oauth2_base_url}auth',
'token_endpoint': f'{self.oauth2_base_url}token',
'userinfo_endpoint': f'{self.oauth2_base_url}userinfo',
'jwks_uri': f'{self.oauth2_base_url}.well-known/jwks.json',
'response_types_supported': ['code'],
'response_modes_supported': ['query', 'fragment'],
'grant_types_supported': ['client_credentials', 'authorization_code'],
'id_token_signing_alg_values_supported': ['RS256'],
'token_endpoint_auth_methods_supported': ['client_secret_post', 'client_secret_basic'],
'code_challenge_methods_supported': ['S256']
}
# Mock login.gateway._get_cached_requests_session
get_cached_session_patcher = mock.patch('requests_cache.CachedSession')
self.addCleanup(get_cached_session_patcher.stop)
get_cached_session_mock = get_cached_session_patcher.start()
self.cached_session_mock = get_cached_session_mock.return_value
# Mock side effect for requests.get() for the various URLs fetched as part of the token
# verification dance.
get_method_return_values = {url: mock.MagicMock() for url in [
self.oidc_configuration['jwks_uri'],
settings.GATEWAY_OPENID_CONFIGURATION_URL,
]}
not_found_mock = mock.MagicMock()
not_found_mock.raise_for_status.side_effect = requests.HTTPError
rv = get_method_return_values[self.oidc_configuration['jwks_uri']]
rv.content = self.keyset.export(private_keys=False)
rv.json.return_value = json.loads(rv.content)
rv = get_method_return_values[settings.GATEWAY_OPENID_CONFIGURATION_URL]
rv.content = json.dumps(self.oidc_configuration)
rv.json.return_value = self.oidc_configuration
self.cached_session_mock.get.side_effect = (
lambda url, *args, **kwags: get_method_return_values.get(url, not_found_mock))
import unittest.mock as mock
from django.conf import settings
from django.test import TestCase, override_settings
from .. import gateway
from . import MockOpenIDConfigurationMixin
@override_settings(
GATEWAY_CLIENT_ID='placeholder-id',
GATEWAY_CLIENT_SECRET='placeholder-secret',
)
class TestAPIGatewaySession(MockOpenIDConfigurationMixin, TestCase):
def setUp(self):
self.set_up_oidc_config_mock()
def test_get_api_gateway_session(self):
"API gateway session is appropriately authenticated."
with mock.patch('requests_auth.OAuth2ClientCredentials') as mock_auth:
session = gateway._get_api_gateway_session()
self.assertIs(session.auth, mock_auth.return_value)
mock_auth.assert_called_with(
token_url=self.oidc_configuration['token_endpoint'],
client_id=settings.GATEWAY_CLIENT_ID,
client_secret=settings.GATEWAY_CLIENT_SECRET,
scope=settings.GATEWAY_LOGIN_SCOPE
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment