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
... JWT ...
You know what?
Until now, that JWT token always felt sooo cryptic,
so unfamiliar to me...
...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 #content:blogposts?
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=...
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:
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
@Sisn/xssec supports adding custom properties
Links
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
@Sisn/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 @Sisn/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