Application Development Discussions
Join the discussions or start your own on all things application development, including tools and APIs, programming models, and keeping your skills sharp.
cancel
Showing results for 
Search instead for 
Did you mean: 

SAP Developer Challenge - APIs - Task 11 - Examine the access token for scopes contained

qmacro
Developer Advocate
Developer Advocate
13,992

(Check out the SAP Developer Challenge - APIs blog post for everything you need to know about the challenge to which this task relates!)

You're almost ready to call the API endpoint to examine the details of the directory you created back in Task 7. But as we're going deliberately slowly and surely, let's take some time in this task to stare at the access token itself for a few minutes, to see what we can discover.

Background

In the previous task you obtained an access token, by completing the flow described by the OAuth Resource Owner Password Credentials grant type. The access token was made available to you in a JSON object which contained not only the access token itself, but other values. Here's that example from the previous task, with the expires_in property added back in:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "bearer...",
  "id_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "e72b61a9a9304dde963e...",
  "expires_in": 43199,
  "scope": "cis-central!b14.glob...",
  "jti": "579fea14a1cf47d7ab9e..."
}

One of the values also provided is scope, which contains a whitespace separated list of scopes. If you were to parse the value, you'd see the list. Here's one way to do that (truncating the list to the first 10 scope items), assuming the JSON object representing the token data is in a file called tokendata.json:

jq '.scope|split(" ")[:10]' tokendata.json

This would produce:

[
  "cis-central!b14.global-account.subaccount.update",
  "cis-central!b14.global-account.update",
  "user_attributes",
  "cis-central!b14.global-account.subaccount.delete",
  "cis-central!b14.global-account.subaccount.read",
  "cis-central!b14.job.read",
  "cis-central!b14.catalog.product.update",
  "cis-central!b14.catalog.product.delete",
  "cis-central!b14.global-account.account-directory.create",
  "cis-central!b14.directory.entitlement.update"
]

This metadata, data about the access token, essentially, is useful to us to have. But what's more interesting is how this scope information is conveyed in the actual call to the API endpoint.

It's specifically the value of the access_token property from the JSON object that is sent in the Authorization header of the HTTP request made, as you learned about in Task 9. The other values in the JSON object (the id_token, refresh_token, expires_in values, and so on) don't go anywhere, they're just for us, the consumer, to use in managing our use of that access token (including knowing when it will expire and requesting a refresh).

So the scope information for this access token appears to be conveyed in a property (scope) that doesn't get sent to the resource server. How does the server then know whether to respond with the requested information or not?

To answer this question, we're going to go on a bit of a digression in this task.

So.

Have you ever wondered about the value of the access token itself? It's a very large, opaque string. In fact, how long is it?

jq -r '"\(.access_token|length) bytes"' tokendata.json

Pretty long!

3912 bytes

Surely there must be a reason for something so large?

The anatomy of the access token

Yes. It's actually a JSON Web Token, or JWT (often pronounced "jot"). A JWT contains structured data, which is fascinating to peek at. And that's what you're going to do in this task.

First of all, it's worth knowing that the content of a JWT is organized into different sections, including:

  • Header
  • Payload
  • Signature

The Header contains a small amount of data about the JWT itself, and consists of values for a series of well-defined (in RFC7515) parameters, the names of which are all three characters in length (to keep things short). Examples are "alg" which identifies the algorithm used to secure the data, "jku", the value of which is a URL that points to a JSON Web Key Set used in the digital signature, and "typ" which conveys the type of content it is.

The Payload section of the JWT is where the data that's most interesting to us lives, or rather most interesting to the server that will handle our requests. It's where the scopes (that we saw earlier) are stored, amongst many other details.

The Signature is essentially a signed checksum of the entire contents.

So the answer to the question above is that the server knows how to respond to requests because there's enough information passed inside the access token (being in the form of a JWT), including a list of scopes that the token conveys for the consumer, for it to decide.

Your task

Your task is to examine the contents of the access token, by treating it for what it is, i.e. a JWT. You should take information from the Header, and information from the Payload, and combine it into a value that you should send to the hash service, and then put the resulting hash into a reply to this thread, as always, and as described in Task 0.

What specifically is that information?

  • from the Header, you should take the value of the "alg" and "typ" parameters
  • from the Payload, you should count the number of scopes conveyed

You should then combine those three pieces of information like this, using colons as separators:

<value-of-alg>:<value-of-typ>:<number-of-scopes>

Let's look at the partial contents of an imaginary (but typical) JWT in this context, to illustrate. This illustration assumes that the access token JSON data (such as you retrieved in the previous task, Task 10) is in a file called tokendata.json. This illustration is also based on using the jwt-cli package, and the command line tool it provides, as described in the "Hints and tips" section below.

So, to pick out the value of the access token (from the access_token property in the JSON object in tokendata.json), and then to treat that access token value for what it is, i.e. a JWT, and ask for the JWT to be expanded into its component parts, you'd do something like this (note the --output=json option to produce nicely machine-parseable output!):

jq -r .access_token tokendata.json | jwt --output=json

What is emitted is something like this (heavily redacted, for brevity, and with some values elided while others are replaced, for the illustration):

{
  "header": {
    "alg": "ABCDE",
    "jku": "https://c2d7b642trial-ga.authentication.eu10.hana.ondemand.com/token_keys",
    "kid": "default-jwt-key-1281344942",
    "typ": "XYZ",
    "jid": "iaVmTleRBCIVnVE7veQ9opMtlHnk+3DvKWWsjpsm542="
  },
  "payload": {
    "jti": "579fea14a1cf47d7ab9e5bf4c9d15d42",
    "ext_attr": {
      "enhancer": "XSUAA",
      "globalaccountid": "7da58aab-6c60-4492-a95b-b1ed3139e242",
      "zdn": "c2d7b642-ga",
      "serviceinstanceid": "f118abbb-b387-41b1-970f-bf4f0309c142"
    },
    "xs.system.attributes": {
      "xs.rolecollections": [
        "Global Account Administrator"
      ]
    },
    "given_name": "DJ",
    "xs.user.attributes": {},
    "family_name": "Adams",
    "sub": "965a393a-dc96-422f-87ac-9f3d8bb25142",
    "scope": [
      "cis-central!b14.global-account.subaccount.update",
      "cis-central!b14.global-account.update",
      "...another 39 scopes...",
      "cis-central!b14.global-account.subaccount.create"
    ],
    "client_id": "sb-ut-f86082c9-7fbf-4e1e-8310-f5d018dab542-clone!b254742|cis-central!b14",
    "cid": "sb-ut-f86082c9-7fbf-4e1e-8310-f5d018dab542-clone!b254742|cis-central!b14",
    "azp": "sb-ut-f86082c9-7fbf-4e1e-8310-f5d018dab542-clone!b254742|cis-central!b14",
    "grant_type": "password",
    "user_id": "965a393a-dc96-422f-87ac-9f3d8bb25142",
    "origin": "sap.default",
    "iat": 1692693022,
    "exp": 1692736222,
    "...": "..."
  },
  "signature": "ZVe_aqyLAyXwToCvG...",
  "input": "eyJhbGciOiJSUzI1NiIsI..."
}

So the three values in the result you should construct, thus:

<value-of-alg>:<value-of-typ>:<number-of-scopes>

should be, in order:

  • <value-of-alg>: the value of the .header.alg property
  • <value-of-typ>: the value of the .header.typ property
  • <number-of-scopes>: the length of the array that is the value of the .payload.scope property

For example:

ABCDE:XYZ:42

Hints and tips

There are many tools and libraries with which JWT tokens can be parsed, even online facilities ... though you should think twice before sending authorization data to third party websites - it's better to use a tool that you have locally.

For tools to use locally, you might wish to check out the NPM package jwt-cli which, if you install it globally, will give you a command line tool called jwt.

It's one of my standard globally-installed NPM-based tools, which you can see here, via:

npm list --global

This emits:

/home/user/.npm-global/lib
+-- @sap/cds-dk@7.0.2
+-- @sap/generator-fiori@1.9.4
+-- @sapui5/generator-sapui5-templates@1.71.6
+-- @ui5/cli@3.1.1
+-- bash-language-server@4.9.1
+-- docsify-cli@4.4.4
+-- eslint@8.39.0
+-- fx@28.0.0
+-- http-server@14.1.1
+-- httpie@1.1.2
+-- jwt-cli@2.0.0
+-- lodash@4.17.21
+-- lorem-ipsum@2.0.8
+-- markdownlint-cli@0.34.0
+-- prettier@2.8.8
+-- ramda@0.29.0
+-- url-decode-encode-cli@2.1.0
+-- yarn@1.22.19
`-- yo@4.3.1

With the jwt tool, you can decode such JWT access tokens. And with jwt's --output=json option, it's even better!

For discussion

The expires_in property, that accompanies the access token returned, has an interesting value. It's 1 second less than 12 hours. Do you think that's deliberate? Calculated?

63 REPLIES 63

MarcelloUrbani
Active Contributor
0 Kudos
3,533

5f1e9e05154e5748994ec497a85025f1503e9fd2d5987b6fdd9677077999e084

bztoy
Participant
0 Kudos
3,532

95c66e617a75fed4f2e7d04a1f9dc950f91d7fdbd052d8b97f76936bf000dd16

0 Kudos
3,529

The expires_in property, that accompanies the access token returned, has an interesting value. It's 1 second less than 12 hours. Do you think that's deliberate? Calculated?



Tried to research about why the number in expires_in is 43199 instead of 43200 but still cannot find the answer.

 

{
  "header": {
      ...
  },
  "payload": {
    "jti": "b4fdf9b04e8b4d3ea7fed026272a169b",
    ...
    "iat": 1693269106,
    "exp": 1693312306,
    "iss": "https://31877e08trial-ga.authentication.eu10.hana.ondemand.com/oauth/token",
    ...
}

 

Let's my small brain make a guess 😁

The gap of between "iat" and "exp" Epoch date from above sample is 12 hours but Is it possible that the date/time specified in "exp" is the time that this token has already expired that why OAuth tell the client the number of seconds in property expires_in is the token is still valid.

that is just my guess 😓

qmacro
Developer Advocate
Developer Advocate
3,511

A decent guess! In fact, I'm not sure why this is done - I've done some digging of my own and cannot find any explanation of this behaviour. It's not limited to the auth servers in our SAP tech ecosphere, I find this behaviour is exhibited elsewhere too. I mean, it's not a big deal, it's more of a curiosity, but it irks me slightly that I cannot find any place where this behaviour is written down 🙂

salilmehta01
Associate
Associate
0 Kudos
3,520

ca946bf15ed2c520d1dffffe98bbabcfc68e408238b90d653e9cf0ef650fb758

berserk
Explorer
0 Kudos
3,488

6f4bba382d84093e2bc531b18f9c6a72bbfdf14903f449d2c87a306fbb822d0b

ADR
Participant
0 Kudos
3,480

e8882b92b8802344aaa89011047e7ded2fb0f4c5be15f2fb9b76d41e581db2d7

PriyankaChak
Active Contributor
0 Kudos
3,461

e2780b9d5fe2d282e3d248472e42e5253102c182c72837d11f1bfb05f7ecd1bb

cdias
Product and Topic Expert
Product and Topic Expert
0 Kudos
3,453

8cdf073cd891cc66959ad543af306808052b8ef08f68795811601b85e17ca315

thomas_jung
Developer Advocate
Developer Advocate
0 Kudos
3,442

e3f3dfcb8c572f995a835e557f4f25f38b21642cb0364d57757d0270b4ddeac4

3,441

For the JWT decoding tool, I used the Microsoft DevToys. It has a JWT decoder built in. 
DevToys - A Swiss Army knife for developers

thomas_jung_0-1693344434403.png

 

garyzuo
Explorer
0 Kudos
3,440

6cb0ad62e68c990040071932331806085fd29267cd97529fd59bbec3b95bfd77

johna69
Product and Topic Expert
Product and Topic Expert
0 Kudos
3,439

6dc9ef8d8c57562f017c572aef41955535cf27208da98f671e8909bd481bbd96

ecem_yalim
Explorer
0 Kudos
3,415

ca24697e9d1effc97b0541479943584b2abcbccd05c0282d4c854b6fa0d50971

nex
Explorer
0 Kudos
3,408

a6cfd6fd619a3a3132973e8b0f4a406b371f5c02505b61e96564ee52e91cf140

Nicolas
Active Contributor
0 Kudos
3,394

229c4219f187bfc88335f0ff221418643157ab4ea38fcf8d13c36bffc38f1dd0

former_member136915
Product and Topic Expert
Product and Topic Expert
0 Kudos
3,380
86d20997d50249761b5e487cdf45031601ef2edb0a6f1632728644bffc19cd27

shotokka
Explorer
0 Kudos
3,370

76cc768d8902b2c0495c6150a1826f961a5bd1a5e24acb742b575c234be4a200

RaulVega
Participant
0 Kudos
3,358

34d7dee6dd0f9d507c30467dd45c4c91f473184983de9011c776609f3c042cab

flo_conrad
Explorer
0 Kudos
3,352

8c80bdaabb4a30583c11b0606d04f698452f801b85ba2012dc00e16aff30c313

qmacro
Developer Advocate
Developer Advocate
0 Kudos
3,338

Hey everyone! The challenge to which this task belongs is now officially closed. Head over to the original blog post SAP Developer Challenge – APIs to check out the closing info and final statistics, and to see your name in lights! 🎉  And we thank you all for participating, you made this challenge great!

satya-dev
Participant
0 Kudos
3,336

a7b851aa2941df609e00f96635dc4b254df3612647dc6b104ec1b24da1fa1880

andrew_chiam
Explorer
0 Kudos
3,327

199d2397b7f3c34518048acfb54dfb359c92eb06e222dcff919c026ce2f61988

buz
Explorer
0 Kudos
2,432

624b8aba6848b6147c77d260c76f41f8034853adfa809730f196b73a2585aa37