2023 Aug 28 7:04 AM - edited 2023 Aug 28 7:43 AM
(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.
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?
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:
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 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?
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:
For example:
ABCDE:XYZ:42
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!
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?
2023 Aug 28 7:35 AM
2023 Aug 28 7:38 AM - edited 2023 Aug 28 7:38 AM
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
2023 Aug 28 7:43 AM
Haha, you're right. I've added the word "often" to the text, to suggest that not everyone does 🙂
2023 Aug 28 7:45 AM
2023 Aug 28 7:12 PM
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
2023 Aug 28 7:56 AM
2023 Aug 28 8:02 AM
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 -
2023 Aug 28 8:43 AM
2023 Aug 28 9:12 AM
2023 Aug 28 9:20 AM
2023 Aug 28 9:57 AM
2023 Aug 28 10:03 AM
2023 Aug 28 10:18 AM
2023 Aug 28 10:32 AM
2023 Aug 28 10:36 AM
2023 Aug 28 10:56 AM
2023 Aug 28 11:50 AM
2023 Aug 28 12:03 PM
2023 Aug 28 12:41 PM - edited 2023 Aug 28 3:51 PM
2023 Aug 28 12:43 PM
2023 Aug 28 12:52 PM
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 ...
2023 Aug 28 1:09 PM
2023 Aug 28 1:49 PM - edited 2023 Aug 28 1:57 PM
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:
and check the value of the scope property from the JSON object returned?
2023 Aug 28 2:04 PM - edited 2023 Aug 28 2:06 PM
I created the cis service in a new subaccount named Developer Challenge. For the cis service I used the central plan.
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....'}
2023 Aug 28 2:20 PM
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 ...
2023 Aug 28 2:33 PM
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"
}
2023 Aug 28 2:37 PM
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.
2023 Aug 28 3:40 PM - edited 2023 Aug 28 3:41 PM
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. 👍
2023 Aug 28 3:45 PM
2023 Aug 28 9:03 PM
Finally, managed to get it done in Python as well:
https://github.com/ceedee666/sap-dev-challenge-apis-in-python/blob/main/sap-dev-challange-apis.ipynb
2023 Aug 28 5:41 PM - last edited on 2023 Aug 28 5:45 PM by Former Member
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": "..."}
you can create an instance with client_credentials and try if you really want to use 'client_credentials'
2023 Aug 28 1:02 PM
2023 Aug 28 6:21 PM
Inside your devcontainer:
jq -r .access_token tokendata.json | jwt --output=json | jq -r .payload.scope
2023 Aug 28 1:13 PM
2023 Aug 28 7:05 PM - edited 2023 Aug 28 7:13 PM
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.
2023 Aug 28 1:50 PM
2023 Aug 28 5:04 PM
2023 Aug 28 5:53 PM
2023 Aug 28 10:20 PM