In case you don't have access to the Columbia University OAuth2 AS, or want to quickly experiment without
waiting for your requested client registration to be implemented, you can optionally configure
the built-in Django OAuth Toolkit (DOT) AS.
It's fairly easy to use DOT as your AS, logging in Django "staff" users with the Authorization Code flow. To
keep it easy to switch between DOT and our external AS we'll
configure our DOT AS using the same scopes as used for the Columbia AS — they just won't mean much. (For example,
the auth-columbiascope selector will not lead to a Shibboleth login when using DOT.)
DOT >= 1.5.0 supports OpenID Connect (OIDC) 1.0, so we'll
configure that too. This means
we'll also get an ID Token along with the Access Token (and possible Refresh Token).
In settings.py we use OAUTH2_SERVER as the base URL for our OAuth2 AS. We'll overload the meaning of this variable
with a value of "self" meaning to use the built-in DOT AS. Also, we'll configure the scopes our AS offers — matching
what our external AS currently supports. And, we'll add the RSA private key that we generated per the
OIDC instructions.
+###+# OAuth2/OIDC Server Configuration+# Set env['OAUTH2_SERVER'] to 'self' to use the built-in django-oauth-toolkit server.+# Otherwise et it to a baseURL of an external PingFederate AS.+###
OAUTH2_SERVER = os.environ.get('OAUTH2_SERVER','https://oauth-test.cc.columbia.edu')
+# Workaround inability of PyCharm to handle multi-line environment variables by reading+# the OIDC RSA private key from a file. Otherwise just take it from the env.+oidc_key_file = os.environ.get('OIDC_RSA_PRIVATE_KEY_FILE', None)+if oidc_key_file:+ oidc_key = open(oidc_key_file, 'rb').read().decode()+else:+ oidc_key = os.environ.get('OIDC_RSA_PRIVATE_KEY', None)++
OAUTH2_PROVIDER = {
# here's where we add the external introspection endpoint:
- 'RESOURCE_SERVER_INTROSPECTION_URL': OAUTH2_SERVER + '/as/introspect.oauth2',+ 'RESOURCE_SERVER_INTROSPECTION_URL': None if OAUTH2_SERVER == 'self' else OAUTH2_SERVER + '/as/introspect.oauth2',
'RESOURCE_SERVER_INTROSPECTION_CREDENTIALS': (
os.environ.get('RESOURCE_SERVER_ID','demo-django-jsonapi-training_validator'),
os.environ.get('RESOURCE_SERVER_SECRET','SaulGoodman')
),
+ 'SCOPES': {+ "address": "Share my address",+ "read": "Read my resource(s)",+ "openid": "Share my UNI",+ "profile": "Share my name",+ "email": "Share my email address",+ "update": "Update my resource(s)",+ "demo-djt-sla-bronze": "May access the django-jsonapi-training API",+ "auth-columbia": "Columbia University UNI login",+ "delete": "Delete my resources(s)",+ "auth-none": "no login required",+ "https://api.columbia.edu/scope/group": "Share my group memberships",+ "create": "Create my resource(s)",+ "introspection": "Introspect token scope",+ },+ 'OIDC_ENABLED': True if oidc_key else False,+ 'OIDC_RSA_PRIVATE_KEY': oidc_key,
}
(The "introspection" scope has special meaning for DOT only: It's how a client can get authorized to use the
token introspection endpoint.)
When using the Columbia AS, users are members of the user community and show up with a Django
request.user of _UNI_@columbia.edu. When using DOT,
you need to create users in the
Django Authentication System.
Since we've configured user admin in our app, you can go to http://localhost:8000/admin/ and login as the superuser
that we created with manage.py createsuperuser (see building).
To save manual clicking around, you can load a few users (including the superuser) from a fixture:
This creates (or updates) the admin user as well as users user1, user2 and user3. It also creates three groups:
team-a, team-b and team-c, assigns some privileges to them and some user memberships.
See http://localhost:8000/admin/auth/ for these details:
DOT calls clients "applications". Let's add one. Go to http://localhost:8000/admin/oauth2_provider/application/
and select Add Application. DOT offers up a random string for the client ID and secret, but let's overwrite them
with our demo values:
Besides the "end user" client, your backend service may need an introspection client in order to introspect
the Access Token. We'll create one, copying the example client name and secret from settings.py
(When using client credentials for the introspection client, it appears there's no need for introspection scope
mentioned earlier; that appears to be a holdover from prior behavior which required a granted Bearer Access Token
rather than the more common approach of using Basic Auth to authenticate introspection.)
For Oauth 2.0 token introspection, you'll use the introspection client
credentials described above. Enter these as basic auth and then, for the request body, send it x-url-form-encoded
with the value being the granted token. The result should look something like this for a valid token:
OIDC adds the Userinfo endpoint which is very similar to introspection but uses the granted Access Token for
authentication. Set the postman authorization type to Bearer Token, paste in the token and the response will
look like this:
You'll note that the default response simply provides the sub (subject) numeric ID.
Next, we'll explore extending DOT's userinfo and introspection responses to provide more details.
Resolving Missing Claims: Extending ID Token and Userinfo¶
OIDC defines a set of standard claims
that are supposed to be returned by the Userinfo endpoint or in the ID Token. As delivered, DOT returns only
a minimal set but documents how to
customize
it to add additional claims.
Of the above, notably several are the result of two OIDC
standard scopes
that were requested: profile -- which requests several claims and email.
Let's extend the ID Token
to include those claims as well as our custom groups claim only if the appropriate scopes are granted.
Per the instructions, let's add class CustomOauth2Validator in myapp/oauth2_validator.py and define
get_additional_claims to replace the numeric sub ID with the username and add additional claims
if the required scopes are present:
1 2 3 4 5 6 7 8 910111213141516171819202122232425
fromoauth2_provider.oauth2_validatorsimportOAuth2ValidatorclassCustomOAuth2Validator(OAuth2Validator):defget_additional_claims(self,request):""" Return additional ID Token Claims based on the OIDC scope claims. Args: request: Returns: dict of additional claims """claims={"sub":request.user.username}if'profile'inrequest.scopes:claims["given_name"]=request.user.first_nameclaims["family_name"]=request.user.last_nameclaims["name"]=' '.join([request.user.first_name,request.user.last_name])if'email'inrequest.scopes:claims["email"]=request.user.emailif'https://api.columbia.edu/scope/group'inrequest.scopes:claims['https://api.columbia.edu/claim/group']=' '.join([g.nameforginrequest.user.groups.all()])returnclaims
It should be pretty easy to extend DOT's Userinfo response to look similar. In fact, we should be able to reuse
the ID Token code we just wrote -- but it appears there's a
bug
in DOT 1.5.0 that the request.scopes is missing everything but openid:
1 2 3 4 5 6 7 8 910111213141516171819202122232425
diff --git a/myapp/oauth2_validator.py b/myapp/oauth2_validator.pyindex 169550f..88dbffd 100644--- a/myapp/oauth2_validator.py+++ b/myapp/oauth2_validator.py@@ -24,3 +24,20 @@ class CustomOAuth2Validator(OAuth2Validator):
if 'https://api.columbia.edu/scope/group' in request.scopes:
claims['https://api.columbia.edu/claim/group'] = ' '.join([g.name for g in request.user.groups.all()])
return claims
++ def get_userinfo_claims(self, request):+ """+ Return additional Userinfo claims+ Args:+ request:++ Returns: userinfo dict+ """+ claims = super().get_userinfo_claims(request)+ # This version of request seems to only have the 'openid' scope. That's probably a bug.+ # additional_claims = self.get_additional_claims(request)+ # for now kludge it to provide all the stuff while we investigate if this is a bug.+ kludged_request = request+ kludged_request.scopes += ['profile', 'email', 'https://api.columbia.edu/scope/group']+ additional_claims = self.get_additional_claims(kludged_request)+ return {**claims, **additional_claims}