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
14,404

(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

vladimirs_semikins
Active Contributor
0 Kudos
9,996

16f0aec1122dac8b7da8564df068bc8c4e26f56c949f944d264ef23cc31c3b26

0 Kudos
9,993

I've used raycat plugin https://www.raycast.com/gdsmith/jwt-decoder to decode JWT token

Also, I never say "jot," but instead spell it as J-W-T. Thankfully, I'm not the only one who does this - https://www.youtube.com/watch?v=D2D9umQMKhA 

9,989

Haha, you're right. I've added the word "often" to the text, to suggest that not everyone does 🙂

harsh_itaverma
Participant
0 Kudos
9,997

ec09383eb0242b8c3cff9ce4aef8a219dd6b2941314b44e1ba64b0b0db1b72e9

9,242

I installed jwt-cli globally using npm for this task.

In the first few tasks I was using Javascript code for JSON parsing but now I have started using jq! Even to calculate the no of scopes, it was pretty easy using jq 🙂 and then preparing the resultant string directly using jq.

On the expires_in, may be that's calculated, so that exactly in 43199 secs the token expires (expires_in) and at 12hrs (43200s), we have to refresh it. The expires_in is explained as "If the access token expires, the server should reply with the duration of time the access token is granted for" . But that is just a thought, now that I already know it's 43199 😛  

Not sure if anyone else noticed there is another "exp" field in the JWT decoded token 

"exp": 1692990710, and an "auth_time": 1692947510,
The resultant seconds here is 43200 🤔
There has to be some justification 😅

ajmaradiaga
Developer Advocate
Developer Advocate
0 Kudos
10,003

5ba8c7208393835e1cf329cab9893f70a4603c1381c70090c70ccbeff046f666

9,993

I used this to decode the JWT - https://github.com/mike-engel/jwt-cli

brew install mike-engel/jwt-cli/jwt-cli

# Note that the command line for JWT will be a bit different than when using the npm package
jq -r .access_token cis_access_token.json | jwt decode -j -

prachetas
Participant
0 Kudos
9,986

c219a98398a34ee23da4a4f41e2596d9f802b9d07d73048b85a45ad33f16c19b

SandipAgarwalla
Active Contributor
0 Kudos
9,972

2aad754ea377922f55c2a729eef24c0796cda2115d35c75187f0dff537e73d8d

emiliocampo
Explorer
0 Kudos
9,971

d6b9a707cd27046564812042900385b19ce0afe50c47efb5a559349b5bfde8e8

tobiasz_h
Active Participant
0 Kudos
9,948

c6c915af93f2cb45053bf7d7b7723e336fef38d76d87ecb37b2bb8f02c2ec210

kasch-code
Participant
0 Kudos
9,945

2c5617d44946820f48299176762a43cf3e2c5289552d702295afccdd89f3f6b9

szeteng00
Explorer
0 Kudos
9,935

5fc1973f8d4cc3e2e39a3755df52a2d93d83c7047d24f459288cf5958fb77511

choujiacheng
Explorer
0 Kudos
9,938

f0b52968defdd5dcbef3d6a77f530cfefa3d03a846afad6c451e5419f419b9c1

0 Kudos
9,930

It might be deliberate since the counter might start before the access token is sent to the user, of which the countdown has already begun, but this is speculation as I do not know the process of access tokens well.

qmacro
Developer Advocate
Developer Advocate
0 Kudos
9,788

This is a good guess! On the one hand, it seems odd to me that this is a deliberate move (to subtract 1 second before sending the value in the response), but on the other hand, it makes sense.

Dan_Wroblewski
Developer Advocate
Developer Advocate
0 Kudos
9,770

9047ad05fec9cc9f019fe6a2d6d42a830ceeed41655012c7fac880effefe3781




--------------
See all my blogs and connect with me on Twitter / LinkedIn

Tomas_Buryanek
Active Contributor
0 Kudos
9,767

e1c6cd6372b2f1c60672eba17388cdbc975a546c8a556b91ed00e7ba17dbc10c

-- Tomas --

ceedee666
Active Contributor
0 Kudos
9,775
c5e4df13ad9dfc27d086bd00ab175e2741833e9f16148df21325d64224785b64

 

ceedee666
Active Contributor
0 Kudos
9,773

I struggled a lot today. Wasn't able to decode the token in Python. Still unclear way... 😔.

Furthermore I think something is wrong with my token as it only contains one scope.

qmacro
Developer Advocate
Developer Advocate
9,768

Thanks for sharing that you're struggling - others may be too, so it's good to know we're not alone! Regarding your comment on the scope(s) contained in your token, what does the value for the `scope` property look like in the actual JSON object returned when you get your access token? It might be worth checking that first, before digging into the payload of the JWT (from the access token) to see the scope info there ... 

ceedee666
Active Contributor
0 Kudos
9,754

The scope propert also only contains:

scope': 'uaa.resource'

`

qmacro
Developer Advocate
Developer Advocate
0 Kudos
9,700

Hmm, that definitely doesn't look right.

If you can (and perhaps elide some of the values for security, a bit like I did at the start of the "Background" section of this discussion thread, above), do you want to share your JSON object here? 

Also, just another thought; assuming you pasted in that scope info above, that doesn't look like JSON at all (single quotes are invalid as value wrappers in JSON), so I'm curious as to where it's coming from, and what it is. Maybe that's also a clue we can dig in deeper to?

Perhaps also:

  • double check your instance of cis is based on the central place
  • create a new service key
  • request a new access token via the OAuth flow

and check the value of the scope property from the JSON object returned?

ceedee666
Active Contributor
0 Kudos
9,681

I created the cis service in a new subaccount named Developer Challenge. For the cis service I used the central plan.

CleanShot 2023-08-28 at 14.57.41@2x.png

I recreated a service key in the cockpit and requested a new access token with it. Again, the scope is only uaa.resource. The JSON looks like this:

{'access_token': 'eyJhbGciOiJSU....',
 'token_type': 'bearer',
 'expires_in': 43199,
 'scope': 'uaa.resource',
 'jti': '0c93d45....'}

 

qmacro
Developer Advocate
Developer Advocate
0 Kudos
9,674

Hmm, I may have to dig in to what's going on here. But (and I don't want this to distract us from anything, but I have to ask): where did you get that "JSON" from? It's not valid JSON, and I'm wondering whether this is just something that is coincidental, or a sign something more fundamental is amiss ...

ceedee666
Active Contributor
9,664

I simply get it as a result from the HTTP post.

URL = "https://christiandrumm.authentication.eu10.hana.ondemand.com" + "/oauth/token"

def get_access_token(clientid, clientsecret):
    r = requests.post(URL, 
                      headers={"Content-Type": "application/x-www-form-urlencoded"},
                      auth=(clientid, clientsecret),
                      data={"grant_type": "client_credentials"}
                      )
  
    return r.status_code, r.json()

The JSON I posted is what is returned. So it is the Python representation of the JSON. This is what the JSON looks like if it is not converted to Python:

{
"access_token":"eyJhbG...",
"token_type":"bearer",
"expires_in":43199,
"scope":"uaa.resource",
"jti":"343....2"
}

9,628

The grant_type specified is incorrect. Please refer to https://groups.community.sap.com/t5/application-development/sap-developer-challenge-apis-task-10-req... which states that the ‘client_credentials’ type cannot be used for this exercise.

qmacro
Developer Advocate
Developer Advocate
9,609

OK, thanks, that makes sense, i.e. Python is emitting some other data structure that is (to quote H2G2) "almost, but not quite, entirely unlike JSON" ... instead of actual JSON. As long as we know what's going on 😉

But yes, @vladimirs_semikins's observation is correct - Client Credentials is an inappropriate grant type for this context. Definitely worth retrying the request ... which also means you'll have to rewrite that requests.post call to send the different grant type but also the other parameters that are needed for the Resource Owner Password Credentials grant type. 👍

ceedee666
Active Contributor
0 Kudos
9,602

OK, seems to work.Stupid me for not seeing

Can someone explain why th client credentials work (i.e. I get a token) with different scope?

0 Kudos
9,576

If you set up your CIS instance using the "Password" authorization type (which is the default), please make sure to include the grant_type as "password" along with the username and password. 

 

data={"grant_type": "password","username": "...","password": "..."}

 

Ashok_Easa_0-1693240948281.png

you can create an instance with client_credentials and try if you really want to use 'client_credentials'

UweFetzer_se38
Active Contributor
0 Kudos
9,758

8e57afad60a9e4c0a86ba4bb8cd4fd8360b9330e120fb891be645168450695d9

9,594

Inside your devcontainer:

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

 

sabarna17
Contributor
0 Kudos
9,751

d558695fdfc26614b63d23483c2b5751af4fe63c1b4dd1f5fc51447af49a81d6

0 Kudos
9,407

I tried with a npm library, jwt-decode to decode this JWT value. It is having two different settings. Check this - github-jwt-decode

1. for JWT Body - 

 

import jwt_decode from "jwt-decode";
var decoded = jwt_decode(token);

 

 2. for JWT header -

 

var decodedHeader = jwt_decode(token, { header: true });

 

While implementing the Node-Red Flow, I found that it's bit trickey to read the values in a single npm-node. Here is how I have done it.

sabarna17_0-1693245698398.png

geek
Participant
0 Kudos
9,700

091f223e51a3c34711a304888325abf3ae0c57ca6e8997479c06e5244dc7d5c7

Ruthiel
Product and Topic Expert
Product and Topic Expert
0 Kudos
9,617

c533ce23b128e76abe40cce8442166163787d924fc33ca1828b48bde0c18e523

Ashok459
Participant
0 Kudos
9,603

864a3f229c307ce1ccd775cfa8ec2ba6c41163d59117859b33bf7f8a345ba64a

martinstenzig
Contributor
0 Kudos
9,291

0c8c8bc050d5bea230ef2e090322db6a83857d0309412ebcbdaa711e5f133f56