diff --git a/assets/authentication.py b/assets/authentication.py index 03ceb48..16b2c1c 100644 --- a/assets/authentication.py +++ b/assets/authentication.py @@ -4,14 +4,12 @@ """ import datetime import logging -import requests from django.conf import settings from django.contrib.auth import get_user_model -from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist from rest_framework.authentication import BaseAuthentication -import requests.exceptions +from .models import UserLookup from .oauth2client import AuthenticatedSession @@ -21,9 +19,6 @@ INTROSPECT_SESSION = AuthenticatedSession(scopes=settings.ASSETS_OAUTH2_INTROSPECT_SCOPES) -_request.__session = None - - def _utc_now(): """Return a UNIX-style timestamp representing "now" in UTC.""" return (datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1)).total_seconds() @@ -59,38 +54,7 @@ def authenticate(self, request): subject = token.get('sub', '') if subject != '': - # Our subjects are of the form ':'. Form a valid Django username - # from these values. - scheme, identifier = subject.split(':') - username = '{}+{}'.format(scheme, identifier) - - # This is not quite the same as the default get_or_create() behaviour because we make - # use of the create_user() helper here. This ensures the user is created and that - # set_unusable_password() is also called on it. - try: - user = get_user_model().objects.get(username=username) - except ObjectDoesNotExist: - user = get_user_model().objects.create_user(username=username) - - if cache.get("{user.username}:lookup".format(user=user)) is None: - # Adding 10 extra seconds to the expiry so that if the API requests takes long - # the cache doesn't get expired between the authentication and the response - lookup_response = requests.get( - settings.LOOKUP_SELF + "?fetch=all_insts,all_groups", - headers={"Authorization": "Bearer %s" % bearer}) - - try: - # Ensure the response succeeded - lookup_response.raise_for_status() - - # Cache the response body as parsed JSON - cache.set("{user.username}:lookup".format(user=user), lookup_response.json(), - datetime.timedelta(token['exp'] - _utc_now()).seconds+10) - except requests.exceptions.HTTPError as error: - LOG.error( - ('HTTP Error {error} retrieving institutions for user "{user.username}" ' - 'with subject {subject}').format(error=error, user=user, subject=subject)) - LOG.error('Payload was: {}'.format(lookup_response.content)) + user = user_from_subject(subject) else: user = None @@ -124,14 +88,6 @@ def validate_token(self, token): token['exp'], now) return None - # HACK: lookup:anonymous is required for the moment since we make use of the token/self - # lookupproxy endpoint *and* we do so using the bearer token provided to the backend by the - # user. TODO: refactor this to use the lookupproxy as the backend client. - if 'lookup:anonymous' not in token.get('scope', '').split(' '): - LOG.warning( - 'Presented bearer token with no lookup:anonymous scope. Permissions checking ' - 'will be broken.') - return token def authenticate_header(self, request): @@ -140,3 +96,29 @@ def authenticate_header(self, request): """ return 'Bearer' + + +def user_from_subject(subject): + """ + Return a Django user object given a token subject. + + """ + # Our subjects are of the form ':'. Form a valid Django username + # from these values. + scheme, identifier = subject.split(':') + username = '{}+{}'.format(scheme, identifier) + + # This is not quite the same as the default get_or_create() behaviour because we make + # use of the create_user() helper here. This ensures the user is created and that + # set_unusable_password() is also called on it. + try: + user = get_user_model().objects.get(username=username) + except ObjectDoesNotExist: + user = get_user_model().objects.create_user(username=username) + + # Record this association of user, subject and lookup identity in the DB. Since the user is + # marked as the primary key field, this will throw a database error if there is an existing + # record with differing scheme and identifier. + UserLookup.objects.get_or_create(user=user, scheme=scheme, identifier=identifier) + + return user diff --git a/assets/defaultsettings.py b/assets/defaultsettings.py index 9cc8abd..05c8f30 100644 --- a/assets/defaultsettings.py +++ b/assets/defaultsettings.py @@ -42,9 +42,22 @@ """ +ASSETS_OAUTH2_LOOKUP_SCOPES = ['lookup:anonymous'] +""" +List of OAuth2 scopes the API server will request for the token it will use with lookup. + +""" + +LOOKUP_ROOT = None +""" +URL of the lookup proxy's API root. + +""" -LOOKUP_SELF = None """ -URL of the LookupProxy endpoint that the authentication uses to get data about the user logged in. +Responses to the people endpoint of lookupproxy are cached to increase performance. We assume that +lookup details on people change rarely. This setting specifies the lifetime of a single cached +lookup resource for a person in seconds. """ +LOOKUP_PEOPLE_CACHE_LIFETIME = 1800 diff --git a/assets/lookup.py b/assets/lookup.py new file mode 100644 index 0000000..61b68c9 --- /dev/null +++ b/assets/lookup.py @@ -0,0 +1,75 @@ +""" +Module providing lookup API-related functionality. + +""" +from urllib.parse import urljoin +import logging +from django.conf import settings +from django.core.cache import cache + +from .models import UserLookup +from .oauth2client import AuthenticatedSession + + +LOG = logging.getLogger(__name__) + + +#: An authenticated session which can access the lookup API +LOOKUP_SESSION = AuthenticatedSession(scopes=settings.ASSETS_OAUTH2_LOOKUP_SCOPES) + + +class LookupError(RuntimeError): + """ + Error raised if :py:func:`~.get_person_for_user` encounters a problem. + """ + pass + + +def get_person_for_user(user): + """ + Return the resource from Lookup associated with the specified user. A requests package + :py:class:`HTTPError` is raised if the request fails. + + The result of this function call is cached based on the username so it is safe to call this + multiple times. + + If user is the anonymous user (user.is_anonymous is True), :py:class:`~.UserIsAnonymousError` + is raised. + + """ + # check that the user is not anonymous + if user.is_anonymous: + raise LookupError('User is anonymous') + + # check the user has an associated lookup identity + if not UserLookup.objects.filter(user=user).exists(): + raise LookupError('User has no lookup identity') + + # return a cached response if we have it + cached_resource = cache.get("{user.username}:lookup".format(user=user)) + if cached_resource is not None: + return cached_resource + + # Extract the scheme and identifier for the token + scheme = user.lookup.scheme + identifier = user.lookup.identifier + + # Ask lookup about this person + lookup_response = LOOKUP_SESSION.request( + method='GET', url=urljoin( + settings.LOOKUP_ROOT, + 'people/{scheme}/{identifier}?fetch=all_insts,all_groups'.format( + scheme=scheme, identifier=identifier + ) + ) + ) + + # Raise if there was an error + lookup_response.raise_for_status() + + # save cached value + cache.set("{user.username}:lookup".format(user=user), lookup_response.json(), + settings.LOOKUP_PEOPLE_CACHE_LIFETIME) + + # recurse, which should now retrieve the value from the cache + return get_person_for_user(user) diff --git a/assets/migrations/0009_userlookup.py b/assets/migrations/0009_userlookup.py new file mode 100644 index 0000000..287ce39 --- /dev/null +++ b/assets/migrations/0009_userlookup.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.2 on 2018-03-06 12:31 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0009_alter_user_last_name_max_length'), + ('assets', '0008_auto_20180227_1035'), + ] + + operations = [ + migrations.CreateModel( + name='UserLookup', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='lookup', serialize=False, to=settings.AUTH_USER_MODEL)), + ('scheme', models.CharField(max_length=255)), + ('identifier', models.CharField(max_length=255)), + ], + ), + ] diff --git a/assets/models.py b/assets/models.py index 7a78699..17c89be 100644 --- a/assets/models.py +++ b/assets/models.py @@ -1,6 +1,7 @@ import uuid from automationcommon.models import ModelChangeMixin +from django.conf import settings from django.db import models from django.db.models import Case, When, Q, BooleanField, Value from multiselectfield import MultiSelectField @@ -184,3 +185,20 @@ class Asset(ModelChangeMixin, models.Model): created_at = models.DateTimeField(auto_now_add=True, db_index=True) updated_at = models.DateTimeField(auto_now=True, db_index=True) deleted_at = models.DateTimeField(default=None, blank=True, null=True) + + +class UserLookup(models.Model): + """ + A mapping from Django users to lookup schemes and identifiers. + + """ + #: The corresponding user. Since each use only has one token identity, this is a OneToOneField. + user = models.OneToOneField( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True, + related_name='lookup') + + #: The lookup identifier scheme property for the user + scheme = models.CharField(max_length=255) + + #: The lookup identifier identifier property for the user + identifier = models.CharField(max_length=255) diff --git a/assets/permissions.py b/assets/permissions.py index 848c5af..7fc5c2a 100644 --- a/assets/permissions.py +++ b/assets/permissions.py @@ -4,10 +4,11 @@ """ import logging -from django.core.cache import cache from rest_framework import permissions from rest_framework.exceptions import ValidationError +from .lookup import get_person_for_user + LOG = logging.getLogger(__name__) @@ -75,11 +76,11 @@ def _validate_asset_user_institution(user, department): """Validates that the user is member of the department that the asset belongs to (asset_department).""" - lookup_response = cache.get("{user.username}:lookup".format(user=user)) - if lookup_response is None: - LOG.error('No cached lookup response for user %s', user.username) + if user is None or user.is_anonymous or department is None: return False + lookup_response = get_person_for_user(user) + institutions = lookup_response.get('institutions') if institutions is None: LOG.error('No institutions in cached lookup response for user %s', user.username) diff --git a/assets/systemchecks.py b/assets/systemchecks.py index 2d304a5..cbea4d0 100644 --- a/assets/systemchecks.py +++ b/assets/systemchecks.py @@ -30,7 +30,7 @@ def api_credentials_check(app_configs, **kwargs): 'ASSETS_OAUTH2_CLIENT_ID', 'ASSETS_OAUTH2_CLIENT_SECRET', 'ASSETS_OAUTH2_INTROSPECT_SCOPES', - 'LOOKUP_SELF', + 'LOOKUP_ROOT', ] for idx, name in enumerate(required_settings): value = getattr(settings, name, None) diff --git a/assets/tests/__init__.py b/assets/tests/__init__.py index e69de29..c61c09d 100644 --- a/assets/tests/__init__.py +++ b/assets/tests/__init__.py @@ -0,0 +1,13 @@ +from django.core.cache import cache + + +def clear_cached_person_for_user(user): + """Explicitly clear the cached lookup response for a user.""" + if not user.is_anonymous: + cache.delete("{user.username}:lookup".format(user=user)) + + +def set_cached_person_for_user(user, person): + """Explicitly set the cached lookup response for a user.""" + if not user.is_anonymous: + cache.set("{user.username}:lookup".format(user=user), person) diff --git a/assets/tests/test_admin.py b/assets/tests/test_admin.py index 51a2127..10f2112 100644 --- a/assets/tests/test_admin.py +++ b/assets/tests/test_admin.py @@ -2,7 +2,7 @@ from django.test import TestCase from django.urls import reverse -from assets.models import Asset +from assets.models import Asset, UserLookup class AdminViewsTests(TestCase): @@ -10,7 +10,9 @@ def setUp(self): super().setUp() self.asset = Asset.objects.create(name='foo') self.superuser = get_user_model().objects.create_user( - username='testing', is_staff=True, is_superuser=True) + username='test0001', is_staff=True, is_superuser=True) + self.superuser_lookup = UserLookup.objects.create( + user=self.superuser, scheme='mock', identifier=self.superuser.username) def test_index(self): """Viewing the assets index in admin should be possible.""" diff --git a/assets/tests/test_checks.py b/assets/tests/test_checks.py index 5a79d6a..e0ee9f9 100644 --- a/assets/tests/test_checks.py +++ b/assets/tests/test_checks.py @@ -19,7 +19,7 @@ class RequiredSettings(TestCase): 'ASSETS_OAUTH2_CLIENT_ID', 'ASSETS_OAUTH2_CLIENT_SECRET', 'ASSETS_OAUTH2_INTROSPECT_SCOPES', - 'LOOKUP_SELF' + 'LOOKUP_ROOT' ] def test_checks_pass(self): diff --git a/assets/tests/test_lookup.py b/assets/tests/test_lookup.py new file mode 100644 index 0000000..3dfe38b --- /dev/null +++ b/assets/tests/test_lookup.py @@ -0,0 +1,72 @@ +import unittest.mock as mock + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser +from django.test import TestCase + +from assets import lookup, models + +from . import clear_cached_person_for_user + + +class LookupTests(TestCase): + def setUp(self): + self.user = get_user_model().objects.create_user(username='test0001') + self.user_lookup = models.UserLookup.objects.create( + user=self.user, scheme='mock', identifier=self.user.username) + self.anonymous_user = AnonymousUser() + + # Ensure the Django cache is "clean" between calls. + clear_cached_person_for_user(self.user) + + def test_anonymous_user(self): + """Calling get_person_for_user with the anonymous user fails.""" + with self.assertRaises(lookup.LookupError): + lookup.get_person_for_user(self.anonymous_user) + + def test_no_lookup_user(self): + """Calling get_person_for_user with a Django user with no corresponding lookup user + fails. + + """ + self.user_lookup.delete() + with self.assertRaises(lookup.LookupError): + lookup.get_person_for_user(self.user) + + def test_simple_call(self): + """A simple call to get_person_for_user succeeds.""" + mock_response = { + 'url': 'http://lookupproxy.invalid/people/xxx', + 'institutions': [{'instid': 'INSTA'}, {'instid': 'INSTB'}], + } + + with self.mocked_session() as LOOKUP_SESSION: + LOOKUP_SESSION.request.return_value.json.return_value = mock_response + response = lookup.get_person_for_user(self.user) + + self.assertEqual(response, mock_response) + LOOKUP_SESSION.request.assert_called_once_with( + url='http://lookupproxy.invalid/people/mock/test0001?fetch=all_insts,all_groups', + method='GET' + ) + + def test_results_are_cached(self): + """Two calls to get_person_for_user succeeds only results in one lookup API call.""" + mock_response = { + 'url': 'http://lookupproxy.invalid/people/xxx', + 'institutions': [{'instid': 'INSTA'}, {'instid': 'INSTB'}], + } + + with self.mocked_session() as LOOKUP_SESSION: + LOOKUP_SESSION.request.return_value.json.return_value = mock_response + lookup.get_person_for_user(self.user) + lookup.get_person_for_user(self.user) + + LOOKUP_SESSION.request.assert_called_once_with( + url='http://lookupproxy.invalid/people/mock/test0001?fetch=all_insts,all_groups', + method='GET' + ) + + def mocked_session(self): + """Return a patch for the assets.lookup.LOOKUP_SESSION object.""" + return mock.patch('assets.lookup.LOOKUP_SESSION') diff --git a/assets/tests/test_model.py b/assets/tests/test_model.py index f69da0a..2969e45 100644 --- a/assets/tests/test_model.py +++ b/assets/tests/test_model.py @@ -2,12 +2,14 @@ from django.contrib.auth import get_user_model from django.test import TestCase -from assets.models import Asset +from assets.models import Asset, UserLookup class AuditTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user(username='test0001') + self.user_lookup = UserLookup.objects.create( + user=self.user, scheme='mock', identifier=self.user.username) set_local_user(self.user) self.asset = Asset(name='test-asset') self.asset.save() diff --git a/assets/tests/test_permissions.py b/assets/tests/test_permissions.py index d154f0a..4bdeeb5 100644 --- a/assets/tests/test_permissions.py +++ b/assets/tests/test_permissions.py @@ -5,13 +5,14 @@ from django.contrib.auth import get_user_model from django.http import HttpRequest from django.test import TestCase -from django.core.cache import cache from rest_framework.exceptions import ValidationError from rest_framework.permissions import BasePermission from rest_framework.request import Request from assets import permissions -from assets.models import Asset +from assets.models import Asset, UserLookup + +from . import clear_cached_person_for_user, set_cached_person_for_user class HasScopesPermissionTests(TestCase): @@ -66,12 +67,15 @@ def setUp(self): # By default, authentication succeeds self.user = get_user_model().objects.create_user(username="test0001") + self.user_lookup = UserLookup.objects.create( + user=self.user, scheme='mock', identifier=self.user.username) self.request.user = self.user - cache.set("%s:lookup" % self.user.username, {'institutions': [{'instid': 'UIS'}]}, 120) + # Explicitly set the default user's lookup response + set_cached_person_for_user(self.user, {'institutions': [{'instid': 'UIS'}]}) def tearDown(self): - cache.delete("%s:lookup" % self.user.username) + clear_cached_person_for_user(self.user) def test_view_perms_true_for_all_except_POST(self): """check that the view permission returns true for all request methods expect POST""" @@ -86,16 +90,9 @@ def test_view_perms_POST_department_not_set(self): self.request.method = 'POST' self.assertRaises(ValidationError, self.has_permission) - def test_view_perms_POST_no_cached_lookup(self): - """check the view permission is false when there is not cached lookup for the user""" - cache.delete("%s:lookup" % self.user.username) - self.request.method = 'POST' - self.request.data['department'] = 'UIS' - self.assertFalse(self.has_permission()) - def test_view_perms_POST_no_institution_in_cached_lookup(self): """check the view permission is false when the user's cached lookup has no institutions""" - cache.set("%s:lookup" % self.user.username, {}, 120) + set_cached_person_for_user(self.user, {}) self.request.method = 'POST' self.request.data['department'] = 'UIS' self.assertFalse(self.has_permission()) @@ -103,7 +100,7 @@ def test_view_perms_POST_no_institution_in_cached_lookup(self): def test_view_perms_POST_user_not_in_TESTDEPT(self): """check the view permission is false when the user's isn't associated with the asset's department""" - cache.set("%s:lookup" % self.user.username, {'institutions': [{'instid': 'OTHER'}]}, 120) + set_cached_person_for_user(self.user, {'institutions': [{'instid': 'OTHER'}]}) self.request.method = 'POST' self.request.data['department'] = 'UIS' self.assertFalse(self.has_permission()) diff --git a/assets/tests/test_views.py b/assets/tests/test_views.py index b196480..4e19307 100644 --- a/assets/tests/test_views.py +++ b/assets/tests/test_views.py @@ -7,7 +7,7 @@ from django.test import TestCase from django.urls import reverse from rest_framework.test import APIClient -from assets.models import Asset +from assets.models import Asset, UserLookup from assets.tests.test_models import COMPLETE_ASSET from assets.views import REQUIRED_SCOPES from automationcommon.models import set_local_user @@ -22,6 +22,8 @@ def setUp(self): # By default, authentication succeeds self.user = get_user_model().objects.create_user(username="test0001") + self.user_lookup = UserLookup.objects.create( + user=self.user, scheme='mock', identifier=self.user.username) self.refresh_user() cache.set("%s:lookup" % self.user.username, @@ -432,6 +434,9 @@ def setUp(self): # By default, authentication succeeds self.user = get_user_model().objects.create_user(username="test0001") + self.user_lookup = UserLookup.objects.create( + user=self.user, scheme='mock', identifier=self.user.username) + self.mock_authenticate.return_value = (self.user, {'scope': ' '.join(REQUIRED_SCOPES)}) self.client = APIClient() diff --git a/doc/assets.rst b/doc/assets.rst index 9e5a3f0..4aa8f4e 100644 --- a/doc/assets.rst +++ b/doc/assets.rst @@ -37,6 +37,12 @@ Authentication and permissions .. automodule:: assets.permissions :members: +Interaction with lookup +``````````````````````` + +.. automodule:: assets.lookup + :members: + Extensions to drf-yasg `````````````````````` diff --git a/iarbackend/settings/base.py b/iarbackend/settings/base.py index 10b7da4..7b2eb2b 100644 --- a/iarbackend/settings/base.py +++ b/iarbackend/settings/base.py @@ -185,7 +185,6 @@ 'flow': 'implicit', 'authorizationUrl': 'http://oauth2.example.com/oauth2/auth', 'scopes': { - 'lookup:anonymous': 'Anonymous Lookup Access', 'assetregister': 'Read/write access to the asset register', }, }, diff --git a/iarbackend/settings/developer.py b/iarbackend/settings/developer.py index ed62d8c..e78adf9 100644 --- a/iarbackend/settings/developer.py +++ b/iarbackend/settings/developer.py @@ -56,7 +56,7 @@ ASSETS_OAUTH2_CLIENT_ID = os.environ.get('IAR_CLIENT_ID') ASSETS_OAUTH2_CLIENT_SECRET = os.environ.get('IAR_CLIENT_SECRET') ASSETS_OAUTH2_INTROSPECT_SCOPES = ['hydra.introspect'] - LOOKUP_SELF = 'https://lookupproxy.automation.uis.cam.ac.uk/people/token/self' + LOOKUP_ROOT = 'https://lookupproxy.automation.uis.cam.ac.uk/' # Set the OAuth2 authorisation endpoint SWAGGER_SETTINGS['SECURITY_DEFINITIONS']['oauth2']['authorizationUrl'] = ( # noqa: F405 @@ -66,10 +66,10 @@ # docker-compose. ASSETS_OAUTH2_TOKEN_URL = 'http://hydra:4444/oauth2/token' ASSETS_OAUTH2_INTROSPECT_URL = 'http://hydra:4444/oauth2/introspect' - ASSETS_OAUTH2_CLIENT_ID = 'hydraroot' - ASSETS_OAUTH2_CLIENT_SECRET = 'secret' + ASSETS_OAUTH2_CLIENT_ID = 'iarbackend' + ASSETS_OAUTH2_CLIENT_SECRET = 'backendsecret' ASSETS_OAUTH2_INTROSPECT_SCOPES = ['hydra.introspect'] - LOOKUP_SELF = 'http://lookupproxy:8080/people/token/self' + LOOKUP_ROOT = 'http://lookupproxy:8080/' # Set the OAuth2 authorisation endpoint SWAGGER_SETTINGS['SECURITY_DEFINITIONS']['oauth2']['authorizationUrl'] = ( # noqa: F405 diff --git a/iarbackend/settings/tox.py b/iarbackend/settings/tox.py index 807594e..9eaf423 100644 --- a/iarbackend/settings/tox.py +++ b/iarbackend/settings/tox.py @@ -35,4 +35,4 @@ ASSETS_OAUTH2_CLIENT_ID = 'api-client-id' ASSETS_OAUTH2_CLIENT_SECRET = 'api-client-secret' ASSETS_OAUTH2_INTROSPECT_SCOPES = ['introspect'] -LOOKUP_SELF = 'http://lookupproxy/people/token/self' +LOOKUP_ROOT = 'http://lookupproxy.invalid/' diff --git a/scripts/create-client.sh b/scripts/create-client.sh index 62b6efa..7e3808b 100755 --- a/scripts/create-client.sh +++ b/scripts/create-client.sh @@ -1,13 +1,51 @@ #!/usr/bin/env bash # -# Use Hydra command line client to create a test client application with id -# "testclient" capable of requesting the scope required to access the lookup -# API. +# Use Hydra command line client to create a test client application. +# +# For UI testing, create "testclient" capable of requesting the scope required +# to access the IAR API. +# +# For the backend itself, create a client, "iarbackend", capable of requesting +# hydra.introspect and lookup:anonymous scopes. # set -xe -docker-compose exec hydra hydra clients create \ - --id testclient --secret secret \ - --grant-types implicit,authorization_code \ + +# A convenient alias for calling hydra +function hydra() { + docker-compose exec hydra hydra $@ +} + +# Delete any existing clients. It is OK for these calls to fail if the +# corresponding clients did not exist +hydra clients delete testclient || echo "-- testclient not deleted" +hydra clients delete iarbackend || echo "-- iarbackend not deleted" + +# Create testclient client which can use implicit flow to log into web-based +# UIs. It is a "public" client (i.e. one with no secret) and as such can *only* +# use the implicit flow +hydra clients create \ + --id testclient --is-public \ + --grant-types implicit \ --response-types token,code,refresh_token \ --callbacks http://localhost:4445/callback,http://localhost:8000/static/iarbackend/oauth2-redirect.html,http://localhost:8080/static/lookupproxy/oauth2-redirect.html,http://localhost:3000/oauth2-callback \ --allowed-scopes lookup:anonymous,assetregister + +# Create iarbackend client which can request scopes to access the lookup proxy +# and to introspect tokens from hydra. +hydra clients create \ + --id iarbackend --secret backendsecret \ + --grant-types client_credentials \ + --response-types token \ + --allowed-scopes lookup:anonymous,hydra.introspect + +# We need to create a Hydra policy allowing the backend to introspect tokens. +# Delete a policy if it is already in place and re-create it +hydra policies delete introspect-policy \ + || echo "-- introspect-policy not deleted" + +hydra policies create --actions introspect \ + --description "Allow all clients with hydra.introspect to instrospect" \ + --allow \ + --id introspect-policy \ + --resources "rn:hydra:oauth2:tokens" \ + --subjects "<.*>"