Technical Articles
How to add custom properties to JWT token
This blog is dedicated to Vandana
When I first heard about this feature, it was when Vandana asked me about it, because
a backend service required additional custom property, to be contained in the JWT token
This blog provides info and hands-on examples
Quicklinks:
Quick Guide
Sample Code
Intro
You know what?
…and even the pronunciation as
JOTT
made it so unsociable…
Look at it:
Doesn’t it look like that scary Matrix movie…
Where some metal monsters
are already waiting
to jott you…?
Now, after discovering that we can modify it (slightly), it feels much more sympathetic
I don’t know why…
I’ve searched the internet and found:
This phenomenon is called “The Vandana effect”
Yes, with nice color…
Interesting…
Anyways, I’d like to share this experience with you
Intro
Another attempt of an intro…
What we do:
We request a JWT token and do nothing with it
BUT, we request the JWT token in a way that adds some data to it, as desired by us
Afterwards, to verify, we look into the token
First, we’ll check 3 examples which can be run from our local laptop
Finally, we deploy a little node.js application to the Cloud Platform
Note:
The example is written in node.js but it works similarly in java
Why custom properties?
There are use cases, which require that some special piece of data is contained in a JWT token.
Ask Vandana if you don’t believe ?
This special piece of data can be a special requirement by the backend service which is protected with OAuth
I could imagine for instance, a scenario where an ERP backend needs additional information in order to properly map the Cloud Business User to the ABAP User
Such additional information could be the good old abap name (with upper case, of course) or some special abap permission or abap variant, or abap filter whatever you can imagine in the abap world
And the abap world is a real upper case world….
OK, forget this chapter and go ahead
Overview
Another attempt of a chapter…
0) Preparation: XSUAA
1) Manual request with Postman
2) Programmatic REST call
3) How to use the client library xssec
4) Example for Cloud Foundry
Prerequisite
To follow this blog, no prerequisites are required
Only:
- First of all, please everybody say “hello” to Vandana
- Second, we need an instance of XSUAA service
- Third, nothing else is required, optionally, can use node.js to create hands-on sample code
- Fourth, no need to go through any blogs…
… But if you want, you may read (and like) my OAuth Intro Blog Post
… Or this one about oauth scopes and grants
… Or oo know more about what is a JWT token (here)
… Or why not simply read all my Blog Posts?
Sorry, I don’t know why all chapters are so silly – this is NOT a Vandana effect…
0) Preparation
- We need an instance of XSUAA, which issues a JWT token
We can use any existing instance, no special configuration is neededOr create a new instance of xsuaa, without custom params and with any name of your choice
The command:
cf cs xsuaa application yourXsuaaName - To run the local examples, we need a service key
Command to create service key:
cf csk yourXsuaaName newServiceKeyName - View service key:
cf service-key yourXsuaaName newServiceKeyName
I suggest to keep the service key open, we need to use it later
1) Manual request with Postman
Now let’s try to do it:
Fetch a JWT token which contains a property as defined by us
First attempt is the manual request using a REST client
Everybody knows how to send a request to fetch a JWT token…
In case I forgot to mention it in the prerequisites section, see here
Overview:
We send a request to the authentication URL (from service key)
-> and as as response we get the JWT token
Compose standard request
Let’s first try a normal request to get a JWT token
URL:
We need to take the url property from the service key
https://<subaccount>.authentication…hana.ondemand.com
We have to append the endpoint which issues tokens
/oauth/token
And we have to append the following parameters to the URL
?grant_type=client_credentials&response_type=token
Finally, we’ve composed a URL which asks the authorization server to generate a token:
https://<acc>.authentication...ondemand.com/oauth/token?grant_type=client_credentials&response_type=token
Authorization
The request doesn’t work without credentials
We find the credentials in the service key, the values of the properties clientid and clientsecret
In postman, we specify authorization as Basic Authentication,
where user is <clientid> and password is <clientsecret>
With the information given above, we can fire a GET request with postman and in the response we can see the access_token
Compose request with custom prop
OK, once verified that it works, we can come to the special Vandana-requirement
To add a custom property, we have to add another parameter to the URL
Parameter name:
authorities
Parameter value:
a JSON object with az_attr as root and the custom property as sub node.
Furthermore, it needs to be encoded
Example value:
We want to add a custom property with name as abap_name and value as AVANDANA
As such, the JSON object looks like this:
{
"az_attr":{
"abap_name": "AVANDANA"
}
}
Now we want it as URL param:
&authorities={“az_attr”:{“abap_name”: “AVANDANA”}}
And furthermore we need to encode the URL parameter. Luckily, we only need to encode the brackets:
{ | %7B |
} | %7D |
Now the (encoded) URL param looks like this:
&authorities=%7B”az_attr”:%7B”abap_name”:”AVANDANA”%7D%7D
That’s it , we can now add this param to the URL of the request which we fired before:
https://<acc>.authentication..../oauth/token?grant_type=client_credentials&response_type=token&authorities=%7B"az_attr":%7B"abap_name":"AVANDANA"%7D%7D
Basic Auth as before
Example:
Result:
Response is a JSON object which contains the token
But it also contains our custom property on root level, not encoded:
However, we can decode the JWT token (https://jwt.io/ -> Debugger), then we can see the custom properties are also contained in the payload of the JWT token itself
That’s it.
We’ve just fired a little GET request with REST client.
But it took time to figure out…
Thanks to Ba
2) Programmatic REST call
After doing the manual request with REST client, we might want to see how it works programmatically.
So I’ve created a little node.js script which uses the native node.js capabilities
It is just a silly little script that calls the oauth endpoint with the custom property,
then decodes the token from the response
and finally prints the desired custom property to the console
The script runs locally.
However, the script needs to connect to the instance of XSUAA, so we need to copy&paste the relevant properties of the XSUAA service key (see above) into the code
const CLIENTID = 'sb-xsappname!t12345'
const SECRET = 'ab12AB12xx33ab12AB12xx33xxyy10'
const TOKEN_URL = 'https://subaccount.authentication.eu10.hana.ondemand.com'
Below snippet shows a standard REST call
And we see how easily the JSON property can be encoded
const props = encodeURIComponent(JSON.stringify(CUSTOM_PROP))
const options = {
host: TOKEN_URL.replace('https://', ''),
path: `/oauth/token?grant_type=client_credentials&response_type=token&authorities=${props}`,
headers: {
Authorization: "Basic " + Buffer.from(`${CLIENTID}:${SECRET}` ).toString("base64")
}
}
https.get(options, res => {
Afterwards, after getting the response, we can manually decode the token:
const theJwtToken = response.access_token
...
console.log(`Custom prop ABAP NAME=${jwtDecodedJson.az_attr.abap_name}`)
Please forgive, the script is minimalistic, but you can improve it
See appendix for whole code
3) How to use the client library xssec
Next example is again a script which can be run locally. Again, it requires the credentials of xsuaa, but this time it needs some more properties of the service key, to be copied into the code. Please refer to the appendix to see which properties are required
In this example, we’re using the convenience library @sap/xssec
It makes it easier to handle JWT tokens
Since it is convenient, we take the opportunity to define 2 custom properties
Usually we use the xssec library to protect an endpoint of an application, to ensure correct validation of incoming JWT tokens (See here for an example)
In the next example, we use the library for fetching a token
The advantage:
The library offers a convenient method which allows setting custom properties to the token-request
We only need to understand which parameters we have to pass to the convenience function
xssec.requests.requestClientCredentialsToken(null, credentials, prop, function)
The first parameter is the subdomain.
We can leave it empty, because our example is simple, and the subdomain doesn’t differ from the authentication subdomain
Otherwise, we can find it in the respective service key (or VCAP) , usually it is the same as the subaccount name
The second param: credentials
It mandatory and it corresponds to the credentials section of the VACP_SERVICES of xsuaa
If you have a service key, then you can just pass the full service key as credentials
The normal way would be to read the credentials-subnode from the binding of the XSUAA instance
(We’ll do it in chapter 4)
The third param: custom properties
Yes, it is the custom property, the Vandana-property, which we’re interested in, is in fact just a JSON object containing the required property or properties.
We have to know:
We don’t include the top-level attribute az_attr, because this is added by the library
The last parameter: function
This is the callback function, which gets invoked after the token request.
We have to implement it, so we get the response of the request and can use it
The callback function itself has 2 parameters:
The first param: error
It is filled if the token-request failed
To make our example short, we skip it, otherwise we would have to check first, if error is empty
The second parameter: token
This is the JWT token, which we requested
It is the raw encoded token and we can be happy if we get it.
Again:
xssec.requests.requestClientCredentialsToken(null, CREDENTIALS, customProp, (error, token)=>{
This line of code is already all what I wanted to share with you.
Next line which I want to share with you:
What do we do with the token?
Once we have the JWT token, we can decode it manually (like we did in this example)
Then traverse the JSON object to access the properties (claims)
However, the convenient way is to use the helper functions of the library, to access the properties by name.
And that’s the second line of code which I wanted to share with you:
xssec.createSecurityContext(token, credentials, function)
Basically, we give the token and we get a wrapper object.
With other words:
We need to create a SecurityContext, then use the helper methods
To create the SecurityContext, we need the received token and again the same credentials as above.
After successful creation, we get the result in the callback function and there we can access the created SecurityContext
Again, the callback function has an error param which we should check first (but we skip that today)
The second param is the SecurityContext, which offers convenience functions
And the third param, tokenInfo, allows low-level access to the token, which is needed as well
Again:
xssec.createSecurityContext(token, CREDENTIALS, (error, securityContext, tokenInfo) => {
Example for useful helper method:
securityContext.getEmail()
In our tutorial, the useful helper method which we need is the one which reads the custom property
securityContext.getAdditionalAuthAttribute('abap_name')
The tokenInfo provides native access to the token, either as JSON object, or the encoded string
In below example, we can see the manual way of getting the same property value:
tokenInfo.getPayload().az_attr.abap_name
The appendix contains the whole sample code of the script
4) Example for Cloud Foundry
Usually, in blog posts, it is not recommended to use third-party libraries (due to e.g. maintenance reasons)
However, in this example, since we’re anyways using a convenience library (xssec) we should also use the other convenience library: @sap/xsenv
This is a small lib which makes it easier to access the environment of our application, when deployed to SAP Cloud Platform. It supports Cloud Foundry and Kubernetes
Environment?
Yes, after deployment to Cloud Foundry, an application receives the information about bound services. This info is needed when an app wants to access the bound services. The info can be accessed in the environment variables
The classic way:
const vcap_raw = process.env.VCAP_SERVICES
const VCAP_SERVICES = JSON.parse(vcap_raw)
const CREDENTIALS = VCAP_SERVICES.jobscheduler[0].credentials
const UAA = CREDENTIALS.uaa
const OA_CLIENTID = UAA.clientid;
With xsenv, one of the handy convenience functions is the following one:
const CREDENTIALS = xsenv.serviceCredentials({ tag: 'xsuaa' });
It finds the binding to XSUAA instance without hardcoded name (works only if the app is bound to only one xsuaa)
And it returns only the credentials section, not the whole VCAP_SERVICES variable
In our example, we only need the credentials
BTW, both libraries are recommended and make our code sample very short
Yes… I know… the code is so short because once more I removed all error handling….
All other (few) lines are copied from previous example
Please go to the appendix to view all the whole project files for a deployable sample
Summary
In this blog post we’ve learned how to add a custom property to a JWT token
Such custom property might be needed by the receiving (backend) application
To add such custom property, an additional parameter is added to the URL while requesting the JWT token from XSUAA
The convenience library @sap/xssec supports adding custom properties
Links
- URL encoding: https://www.w3schools.com/tags/ref_urlencode.ASP
- Decode JWT token: https://jwt.io/
- Understanding JWT token, audiences
- xssec: https://www.npmjs.com/package/@sap/xssec
- xsenv: https://www.npmjs.com/package/@sap/xsenv
- UAA docu: https://github.com/cloudfoundry/uaa/blob/v74.29.0/docs/UAA-APIs.rst
Quick Guide
To get additional custom properties into a JWT token, we have to append a new parameter to the URL
The URL is what we use to fetch a JWT token from xsuaa token endpoint
The custon property is a JSON object, but brackets need to be encoded
Example URL:
https://<acc>.authentication…./oauth/token?grant_type=client_credentials&response_type=token&authorities=%7B”az_attr”:%7B”abap_name”:”AVANDANA”%7D%7D
The easy programmatic way:
The lib @sap/xssec offers helper method to fetch JWT token and specify additional properties
xssec.requests.requestClientCredentialsToken(
null,
CREDENTIALS,
customProp,
(error, token)=>{
...
Appendix: All Sample Project Files
1. The manual REST call
Create instance of XSUAA and create service key.
View service key and take a note of properties: url, clientid, clientsecret
Compose request:
URL:
https://<acc>.authentication..../oauth/token?grant_type=client_credentials&response_type=token&authorities=%7B"az_attr":%7B"abap_name":"AVANDANA"%7D%7D
acc = “url” property from service key of XSUAA instance
Authorization: Basic Auth
username = “clientid” from service key
password = “clientsecret” from service key
2. The node.js script with REST call
app.js
const https = require('https');
// replace these values with those from your service key
const CLIENTID = 'sb-xsappname!t12345'
const SECRET = 'ab12cd34hh55ab12cd34hh55123='
const TOKEN_URL = 'https://subaccount.authentication.sap.hana.ondemand.com'
const CUSTOM_PROP = {
"az_attr": {
"abap_name": "AVANDANA"
}
}
// helper function
const fetchJwtToken = function() {
return new Promise ((resolve, reject) => {
const props = encodeURIComponent(JSON.stringify(CUSTOM_PROP))
const options = {
host: TOKEN_URL.replace('https://', ''),
path: `/oauth/token?grant_type=client_credentials&response_type=token&authorities=${props}`,
headers: {
Authorization: "Basic " + Buffer.from(`${CLIENTID}:${SECRET}` ).toString("base64")
}
}
https.get(options, res => {
res.setEncoding('utf8')
let response = ''
res.on('data', chunk => {
response += chunk
})
res.on('end', () => {
resolve(JSON.parse(response))
})
})
.on("error", (error) => {
return reject({error: error})
});
})
}
// script
console.log('=> Start script: call XSUAA to get token with custom property...')
fetchJwtToken().then((response)=>{
console.log(`=> Custom property at root level of response : ${response.az_attr.abap_name}`)
const theJwtToken = response.access_token
const jwtBase64Encoded = theJwtToken.split('.')[1];
const jwtDecodedJson = JSON.parse(Buffer.from(jwtBase64Encoded, 'base64').toString('ascii'));
console.log(`=> Custom property in JWT payload: ABAP NAME=${jwtDecodedJson.az_attr.abap_name}`)
})
3. The script using @sap/xssec
app.js
const xssec = require('@sap/xssec')
const CREDENTIALS = {
"clientid": "sb-xsappname!t12345",
"xsappname": "xsappname!t12345",
"clientsecret": "ab12cd34de56ab12cd34de56123=",
"url": "https://subaccount.authentication.sap.hana.ondemand.com",
"verificationkey": "-----BEGIN PUBLIC KEY-----abcde12345...ABCDE12345§$%&/-----END PUBLIC KEY-----",
}
const CUSTOM_PROP = {
"abap_name": "AVANDANA",
"abap_role": "ALL"
}
// script
console.log('=> Start script: use xssec library to get token with custom property...')
xssec.requests.requestClientCredentialsToken(null, CREDENTIALS, CUSTOM_PROP, (error, token)=>{
printCustomProp(token)
})
// helper
const printCustomProp = function(token){
// use library convenience function to access token content
xssec.createSecurityContext(token, CREDENTIALS, (error, securityContext, tokenInfo) => {
// the convenient way
const propName = securityContext.getAdditionalAuthAttribute('abap_name')
const propRole = securityContext.getAdditionalAuthAttribute('abap_role')
console.log(`=> Custom property values from SecurityContext: name: '${propName}' and role: '${propRole}'`)
// the manual way, access the token content (still convenient)
const payload = tokenInfo.getPayload()
const allProps = payload.az_attr
console.log(`=> Custom Properties from JWT payload: ${JSON.stringify(allProps)}`)
});
}
package.json
{
"dependencies": {
"@sap/xssec": "latest"
}
}
4. The Cloud Foundry App
server.js
const express = require('express')
const app = express()
const xssec = require('@sap/xssec')
const xsenv = require('@sap/xsenv')
const CREDENTIALS = xsenv.serviceCredentials({ tag: 'xsuaa' });
const CUSTOM_PROP = {"abap_name": "AVANDANA"}
// endpoint
app.get('/jwt', function(req, res){
xssec.requests.requestClientCredentialsToken(null, CREDENTIALS, CUSTOM_PROP, (error, token)=>{
xssec.createSecurityContext(token, CREDENTIALS, (error, securityContext, tokenInfo) => {
const prop = securityContext.getAdditionalAuthAttribute('abap_name')
console.log(`===> Custom Property from SecurityContext: ${prop}`)
const payload = tokenInfo.getPayload()
const prop2 = payload.az_attr.abap_name
console.log(`===> Custom Property from JWT payload: ${prop2}`)
res.status(202).send(`Received JWT token. The token contains custom property 'abap_name' with value: ${prop}`);
});
})
});
// Start server
app.listen(process.env.PORT || 8080, ()=>{})
package.json
{
"dependencies": {
"express": "^4.16.3",
"@sap/xssec": "latest",
"@sap/xsenv": "latest"
}
}
manifest.yml
---
applications:
- name: jwtapp
memory: 128M
buildpacks:
- nodejs_buildpack
services:
- xsuaa_custom_prop
This is exactly what I was looking for right now. I just wonder: Is the “authorities” parameter documented anywhere? In the standard UAA documentation I can’t find it.
I think what you use here is the Client Credentials Grant with Authorization:
Token – UAA API Reference (cloudfoundry.org)
Do you know where I can find this API documented?
Edit: I think I found it, in the Github UAA project: uaa/UAA-APIs.rst at v74.29.0 · cloudfoundry/uaa · GitHub
Still I wonder why this doesn't match the CloudFoundry UAA documentation.
Hi Christian Schaefer , thanks very much for the comment and for sharing the link!
I've added it to the blog post
Kind Regards,
Carlos
Hello Carlos Roggan ,
Thanks for the blog post!
Is there any troubleshooting I can do for step 1? It's not working for me and I'm wandering if it's something in the IDP or something to be put in the xs-security.json when creating the XSUAA service? Perhaps a setting in Trust Configurations? I tried also with tenant-mode dedicated and in my trial account but no luck.
Kind regards,
Stefania
Hello Stefania Santimbrean ,
Hum, I didn't use any specific setting for xsuaa instance, as described above.
If you don't get an error message, but just not get the property in the token, then it seems that your URL parameter is ignored. Reason could be that it is not properly encoded?
Have you tried using library? In that case, the request is composed by the lib and you should get the desired result.
Good luck and kind regards,
Carlos
Hello Carlos Roggan ,
It was something around the encoding! Thank you for your suggestion!
I leave here my issue in case anybody else encounters it:
I copy-pasted from the blogpost this part:
&authorities=%7B”az_attr”:%7B”abap_name”:”AVANDANA”%7D%7D
At first glance it seems to be okay, but actually the double quotes are different than the usual ones ("").
So what worked was: &authorities=%7B"az_attr":%7B"abap_name":"AVANDANA"%7D%7D
Many, many thanks for the blogpost and your response!
Kind regards,
Stefania
Thanks Stefania Santimbrean for sharing your solution with the community !
Cheers,
Carlos
Hi Carlos Roggan ,
very cool post and thanks for this. Quick question: is there a way to dynamically add a custom attribute to the JWT after successful login into the application? After the user logs in, I basically want to fetch a parameter for an external database and include that in the JWT so that all microservices can fetch this parameter from the JWT.
I know we can include attributes from the Identity Provider but that's a bit too early on in the process for me and the attribute is different per end-user and needs to be picked from an external DB.
Bram
Hi Bram Purnot ,
thanks for your feedback, and about modifying an existing JWT token, I would strongly assume that this would break all security aspects of it 😉 however there's a mechanism for exchanging an existing token for a new one. You have destination type tokenExchange, also there's a USER_TOKEN type of flow.
The background of this mechanism: you have a user-centric app which requires a scope and user sends the jwt token with this scope. Now your app wants to call a microservice which itself requires different scope which for several reasons is not expected to be in the user's role collection. As such, your app does token-exchange and sends the existing valid token and gets a new one with the scopes required (and granted) by the reuse-microservice.
I assume you can take advantage of this mechanism and ask for new token where you add the desired param.
Hope this assumption becomes true 😉
Kind Regards,
Carlos
That makes sense, thanks!
Hi Carlos,
I have a question about the fetch token API, we must use this kind url for 'az_attr' property?
https://mobile-tenant1.authentication.sap.hana.ondemand.com/oauth/token?grant_type=client_credentials&authorities=%7B"az_attr":%7B"ms_readonly":"true"%7D%7D
our mobile service is using post request to fetch token, but after I add 'authorities' parameter, the generated token not include 'az_attr' property.
We must use authorization_code as grant type because the generated token include user info as follow:
{
"jti": "635db3efc007422abc5a7a7981c7b91e",
"ext_attr": {
"enhancer": "XSUAA",
"subaccountid": "mobile-tenant1",
"zdn": "mobile-tenant1"
},
"xs.system.attributes": {
"xs.rolecollections": [
"auditlog_viewer"
]
},
"given_name": "Xu-dong",
"xs.user.attributes": {},
"family_name": "Liu",
"sub": "94b58c5c-0075-4a7c-9197-7c71beab6a1b",
"scope": [
"openid",
"user_attributes",
"uaa.user"
],
"client_id": "sb-xapp3-xudong!t57",
"cid": "sb-xapp3-xudong!t57",
"azp": "sb-xapp3-xudong!t57",
"grant_type": "authorization_code",
"user_id": "94b58c5c-0075-4a7c-9197-7c71beab6a1b",
"origin": "ldap",
"user_name": "xu-dong.liu@sap.com",
"email": "xu-dong.liu@sap.com",
"auth_time": 1633749165,
"rev_sig": "cfe837b5",
"iat": 1633751129,
"exp": 1635479129,
"iss": "http://mobile-tenant1.localhost:8080/uaa/oauth/token",
"zid": "mobile-tenant1",
"aud": [
"uaa",
"sb-xapp3-xudong!t57",
"openid"
]
}
But from get API https://mobile-tenant1.authentication.sap.hana.ondemand.com/oauth/token?grant_type=client_credentials&authorities=%7B"az_attr":%7B"ms_readonly":"true"%7D%7D
we found the generated token has no user info:
{
"jti": "83cdb7c3d2b84ae9913b1c8cabbae5a2",
"az_attr": {
"ms_readonly": "true"
},
"ext_attr": {
"enhancer": "XSUAA",
"subaccountid": "mobile-tenant1",
"zdn": "mobile-tenant1"
},
"sub": "sb-xapp3-xudong!t57",
"authorities": [
"uaa.resource"
],
"scope": [
"uaa.resource"
],
"client_id": "sb-xapp3-xudong!t57",
"cid": "sb-xapp3-xudong!t57",
"azp": "sb-xapp3-xudong!t57",
"grant_type": "client_credentials",
"rev_sig": "e198f009",
"iat": 1633750791,
"exp": 1635478791,
"iss": "http://mobile-tenant1.localhost:8080/uaa/oauth/token",
"zid": "mobile-tenant1",
"aud": [
"uaa",
"sb-xapp3-xudong!t57"
]
}
Hello Xu-dong Liu
The client-credentials flow has been designed for non-user-centric scenarios, so there's no user info.
The docu states that those additional properties can be requested with OAuth flow "authorization code" as well.
And in your sample token, we can see that there's the az_attr section.
You need to find out why your request for adding your custom prop is not considere.
KInd Regards,
Carlos
Hi Carlos,
Thanks for the reply. Yes. it's my mistake, I used a wrong authorization code from our app router request url, After I changed to use xsuaa to fetch authorization code, it works now.
Thanks much
Xu-dong
Hi Xu-dong Liu ,
Thanks for the info - it is good to know that it is finally working!
Cheers,
Carlos