Adding custom claims to in OAuth 2.0?
I have an OAuth 2.0 development environment where Caché is serving all three roles as the Authorization Server, Client and Resource Server based on a great 3-part series on OAuth 2.0 by @Daniel Kutac. I have a simple password grant type where an x-www-form-urlencoded body (as described in this post) is sent as a POST to the token endpoint at https://localhost:57773/oauth2/token and a response body with a HTTP Response 200 header is returned. The response body looks something like this.
{ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJqdGkiOiJodHRwczovL2NhY2NvbnMxLmhwc3Mubi1pLm5ocy51azo1Nzc3My9vYXV0aDIuazZKZlpSZnpBRU02NHpzRDJYUFZsSHRBOElnIiwiaXNzIjoiaHR0cHM6Ly9jYWNjb25zMS5ocHNzLm4taS5uaHMudWs6NTc3NzMvb2F1dGgyIiwic3ViIjoidGVzdDEiLCJleHAiOjE1NjU2ODc4OTcsImF1ZCI6IkhwSHlfTDA2MVJLVExsaW1OS3FnWjJHR2xkQnE3dWJTNWlZNE5UNFNfVFkifQ.", "token_type": "bearer", "expires_in": 180, "scope": "createModify openid profile publish" }
In this example, the token generation class is the %OAuth2.Server.JWT class.
The expires_in property is the 'Access Token Interval' set in seconds via System Management Portal OAuth options. I have a low interval for the purposes of testing token expiry responses.
Scopes are sorted alphabetically by default. I have added the custom scopes 'createModify' and 'publish' here but these don't really make sense as they are passed into the Request body before the user is authenticated via the token endpoint. You don't know what application roles a user has until they have been authenticated so I would like to return these application roles as claims to the response body. I think I should remove these scopes and replace them with 'MyCustomApplication'. This should be the same for all users. I can sometimes get confused between scopes and application roles! Any thoughts?
I want to customize this response body to retrieve a set of custom claims the user has when they successfully generate an access token. An example of this using a .NET Core demo I was playing with looks something like this.
{ "bearerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQU2hlcmlmZiIsImp0aSI6IjliMzJhNGJhLTdkOWEtNGQ5MS04NWMwLTA2NGM5MTlkNWNmZCIsIkNhbkFjY2Vzc1Byb2R1Y3RzIjoidHJ1ZSIsIkNhbkFkZFByb2R1Y3QiOiJ0cnVlIiwiQ2FuU2F2ZVByb2R1Y3QiOiJ0cnVlIiwiQ2FuQWNjZXNzQ2F0ZWdvcmllcyI6InRydWUiLCJDYW5BZGRDYXRlZ29yeSI6InRydWUiLCJuYmYiOjE1NjU2OTIyNDksImV4cCI6MTU2NTY5MjMwOSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjoiUFRDVXNlcnMifQ.wLXZ-b7Q-xu2WWYDCvnKVN_8vurEtkpftjFOAHHu8Fs", "expires": "13 August 2019 11:31:49", "claims": [ { "claimType": "CanAccessProducts", "claimValue": "true" }, { "claimType": "CanAddProduct", "claimValue": "true" }, { "claimType": "CanSaveProduct", "claimValue": "true" }, { "claimType": "CanAccessCategories", "claimValue": "true" }, { "claimType": "CanAddCategory", "claimValue": "true" } ] }
So what if I wanted to add these additional claims to the response body. Where can this be done?
I found an interesting comment in the classmethod ##class(%OAuth2.Server.Validate).ValidateUser() that read 'Use the Cache roles for the user to setup a custom property' and I can see the roles for my user being set via
Summary
- How do I customize what is returned in the response body of the token endpoint?
- Any thoughts on scope design principles vs. user-based application roles?
I can also see in the classmethod ##class(OAuth2.Server.Token).ReturnToken(client,token) where there is a section on adding customized response properties but where are these set?
My AccessToken.ResponseProperties array appears to be empty
Hi Stephen,
I think you're on the right track by using custom claims for access control. This is what I've done in the past. Scopes in OAuth are intended to be granted by the user, which is not quite what you want here.
As far as I know there's no way to customize the token response. Your best option is to add the custom claims to the userinfo response. This means adding logic to your ValidateUser() method that will set the claim values and also add them to the list of user info claims.
Set tClaim = ##class(%OAuth2.Server.Claim).%New() Do properties.UserinfoClaims.SetAt(tClaim,"MyCustomNamespace/MyCustomClaim") Do properties.SetClaimValue("MyCustomNamespace/MyCustomClaim","something based on the user")
Then when the "resource server" part of your app validates the access token, you can call the userinfo endpoint to get this claim and determine the user's permissions.
$$$ThrowOnError(##class(%SYS.OAuth2.AccessToken).GetUserinfo(appName,accessToken,,.pUserInfo)) set myCustomClaim = pUserInfo."MyCustomNamespace/MyCustomClaim"
If you'd rather not make a separate call to userinfo each time, your other option is to add them to the body of the JWT. That would look similar in the ValidateUser() method, but with
properties.JWTClaims
instead of UserinfoClaims. Then your resource server can validate the signature on the JWT and get the claim from the token body using the##class(%SYS.OAuth2.Validation).ValidateJWT()
method. This is a little more complicated because you have to enforce that the JWT does have signing enabled (unfortunately the ValidateJWT() method will accept a token with no signature.)Thanks @Barton Pravin for clarifying scope and providing the code snippets! If you want the claim to be part of the Token endpoint response message, you can use
Do properties.ResponseProperties.SetAt(roles,"roles")
Heres's a sample of ValidateUser customization
And a sample of the REST API application
And the Postman response for GET https://{{SERVER}}:{{SSLPORT}}/api/demobearertoken/getToken
You can see 'roles' has been added to JWT, Introspection and UserInfo claim types. In my real-world application it's probably sufficient to add it to the JWT.
Also @Eduard Lebedyuk suggested using the
AccessCheck
method to verify the token in the post From Cache how to Retrieve and Use/Reuse a Bearer Token to authenticate and send data to REST web service? AccessCheck is called before anything else so if the token is invalid or has expired a 401 Unauthorized HTTP Response is returned. A web application consuming this REST API can then process this unauthorized status and return them to the login screen.