Guru: Web Concepts For The RPG Developer, Part 4
October 21, 2024 Chris Ringer
The final article in this series is about the JWT (JSON Web Token). If you wish to review the previous articles, you can drill back from part three. For unattended machine to machine (M2M) processes, a JWT string is a formatted container for requesting access to a resource server. I think of a resource server as the applications. And in our case, the JWT has an asymmetric signature.
But really, what is a JWT? It’s similar to a temporary driver’s license. Think about your last visit to a hotel (figure 1).
Steps:
- You present your ID (JWT) during check-in to the hotel. But a JWT is not a government issued ID, it’s a self-issued ID. You create it.
- The hotel receptionist verifies that your photo ID is valid.
- If valid, the hotel receptionist issues you a room key (an access token).
- The room key is active at 3pm and gives you access to some rooms but not others. The linen closet is off-limits!
- When the room key expires, you no longer have access to any rooms, so don’t delay.
For accessing an application on a resource server, your JWT and access token follow a path similar to the hotel analogy (figure 2).
Steps:
- The client creates and signs the JWT (this article) and sends it to an authorization server. The HTTP request also contains the resource (application) you would like to access.
- The authorization server verifies the JWT and requested resource based on your granted authorities.
- If valid, an access token is returned (with HTTP status 200) as a JSON string. This access token will likely expire in a few minutes. I’ve seen access token sizes up to 1000 bytes but could be even longer.
- Pass the access token to the resource server in an HTTP request as a bearer token. “Bearer” just means presenting your access token, your temporarily issued “room key”.
- The resource server response contains the result of your request.
JWT Sections
A JWT is a long string of text containing 3 sections (header, payload, and signature), each separated by a period. And each section is encoded as base64. A very popular website for visualizing and debugging JWTs is jwt.io. If you navigate to that site you’ll see each of the three sections on the right side and encoded base64 on the left side (figure 3). Also notice that the base64 is URL safe. All ‘+/’ characters were translated into ‘-_’ respectively and trailing equal signs have been trimmed off each section. You should take some time to explore the jwt.io site and become familiar with it. Now let’s examine each section with code examples.
The Header Section
The JWT header describes the algorithm used to generate the signature and has a type “JWT”. A private RSA key combined with a sha-256 message digest corresponds to JWT algorithm “RS256”. The code snippet here (figure 4) uses the JSON_OBJECT scalar function to build a JWT header into a JSON format then converts it to base64 with the BASE64_ENCODE scalar function.
01 dcl-s JwtHdrAlg varchar(10) Inz('RS256'); 02 dcl-s JwtHdrType varchar(10) Inz('JWT'); 03 dcl-s JwtHdrJson varchar(100); 04 dcl-s wkAscii varChar(500) ccsid(*UTF8); 05 dcl-s wkAsciiBase64 varChar(700) ccsid(*UTF8); 06 dcl-s JwtHdrBase64 varchar(150); 07 Exec SQL Set Option Commit = *NONE, Naming = *SYS, DLYPRP = *YES, CLOSQLCSR = *ENDACTGRP, DATFMT = *ISO, TIMFMT = *HMS, USRPRF = *OWNER, DYNUSRPRF = *OWNER; 08 Exec SQL values(JSON_OBJECT('alg': :JwtHdrAlg,'typ': :JwtHdrType)) into :JwtHdrJson; 09 wkAscii = JwtHdrJson; 10 exec sql set :wkAsciiBase64 = BASE64_ENCODE(:wkAscii); 11 JwtHdrBase64 = wkAsciiBase64; 12 JwtHdrBase64 = %xlate('+/':'-_':JwtHdrBase64); 13 JwtHdrBase64 = %trimr(JwtHdrBase64:'=');
Lines 01-03: Variables to hold the algorithm, JWT type and JSON header.
Lines 04-06: Variables to convert the JSON header to base64. Remember, to end up with ASCII, you must start with ASCII (part one). Encoding a string into base64 is just shifting and grouping bits.
Line 07: My standard SQL pre-compile options.
Line 08: Builds the JSON header string into the variable JwtHdrJson. The string returned here by the JSON_OBJECT function is ‘{“alg”:”RS256″,”typ”:”JWT”}’.
Line 09: Converts the JSON EBCDIC to ASCII.
Line 10: Converts our JSON to base64.
Line 11: Converts the ASCII back to EBCDIC.
Lines 12-13: Converts some characters to URL safe characters and also trims any trailing equal signs. The final base64 header is value ‘eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9’.
For debugging purposes, the JWT that your code generates should match the JWT generated by the jwt.io site. Go there now and select algorithm RS256 (figure 4).
You can see the base64 header value in the “Encoded” input field (figure 5).
But if you paste the jwt.io JSON header into a base64 encoder, the result (figure 7) will not match the jwt.io value.
Why is that? It’s because jwt.io actually flattens the JSON string by removing line feeds and indentation before encoding the JSON into base64. This is actually good news for us because the JSON generated with the JSON_OBJECT scalar function matches the JSON that jwt.io uses to encode JSON into base64 (figure 5 vs figure 7). This helps to simplify any debugging you may need to do.
The Payload Section
The JWT payload contains claims which in simple terms is our data. Some claims are standard claims and other claims may be specific to your business needs. Here I describe the most common standard claims. Like the header, the payload JSON must be converted to base64. This code snippet (figure 9) builds upon the prior header code.
01 dcl-s JwtPayLdAud varchar(100) Inz('https://my.resource.com/sales'); 02 dcl-s JwtPayLdIss varchar(50) Inz('0oabcdefg123456dRTvR'); 03 dcl-s JwtPayLdSub varchar(50) Inz('0oabcdefg123456dRTvR'); 04 dcl-s JwtPayLdIat packed(10:0) Inz(1726361713); 05 dcl-s JwtPayLdExp packed(10:0); 06 dcl-s JwtPayLdJson varchar(500); 07 dcl-s JwtPayLdBase64 varchar(700); 08 JwtPayLdExp = JwtPayLdIat + 600; // 10 minutes 09 Exec SQL values(JSON_OBJECT('aud' : :JwtPayLdAud, 'iss' : :JwtPayLdIss, 'sub' : :JwtPayLdSub, 'iat' : :JwtPayLdIat, 'exp' : :JwtPayLdExp)) into :JwtPayLdJson; 10 wkAscii = JwtPayLdJson; 11 Exec SQL set :wkAsciiBase64 = BASE64_ENCODE(:wkAscii); 12 JwtPayLdBase64 = wkAsciiBase64; 13 JwtPayLdBase64 = %xlate('+/':'-_':JwtPayLdBase64); 14 JwtPayLdBase64 = %trimr(JwtPayLdBase64:'=');
Line 01: The audience is the URL you are requesting to access on the resource server.
Lines 02-03: The issuer and subject are your unique client ID on the authorization server. This ID maps to your public key on the authorization server and will be used to verify the signature.
Line 04: “iat” means “issued at”. We know this as the current epoch seconds (part two) – when the JWT was created.
Line 05: The “exp” is when the JWT will expire, based on the “iat”.
Lines 06-07: The variables to hold the JWT JSON payload, and the base64 value.
Line 08: Our JWT will expire 600 seconds after the iat.
Line 09: Builds the JWT JSON payload string into the variable JwtPayLdJson. Here the string returned by JSON_OBJECT function is:
'{"aud":"https://my.resource.com/sales","iss":"0oabcdefg123456dRTvR", "sub":"0oabcdefg123456dRTvR","iat":1726361713,"exp":1726362313}'.
Line 10-14: Converts the payload to base64. As you probably noticed, we are repeating code here to convert our string to base64 so this code block would be a good candidate for a sub-procedure.
The Signature Section
The third and final section of a JWT is the signature. Part three discussed how to generate a signature for an IFS stream file but in our case here the requirements are slightly different. We want to generate a signature from a string so the QShell command needs tweaked.
01 dcl-s wkTempFile varchar(100); 02 dcl-s QShellCmd varchar(2000); 03 dcl-s JwtSignature varchar(2000); 04 dcl-s Jwt varchar(3000); 05 dcl-pr QCMDEXC extpgm; Cmd char(2000) options(*varsize) const; CmdLen packed(15:5) const; end-pr; 06 Exec SQL values(hex(generate_unique())) into :wkTempFile; 07 QShellCmd = 'cd /tmp && printf "%s" ' + 08 '"' + JwtHdrBase64 + '.' + JwtPayLdBase64 + '" ' + 09 '| openssl dgst -sha256 -binary -sign ' + 10 '"my_private_key.pem" ' + 11 '-out "' + wkTempFile + '" && ' + 12 'openssl enc -base64 -A -in "' + wkTempFile + '" ' + 13 '| tr -d ''''\n='''' | tr ''''+/'''' ''''-_'''' > "' + 14 wkTempFile + '"'; 15 QShellCmd = 'STRQSH CMD(''' + QShellCmd + ''')'; 16 CallP(e) QCMDEXC(QShellCmd:%Len(QShellCmd)); 17 wkTempFile = '/tmp/' + wkTempFile; 18 Exec SQL Select LINE Into :JwtSignature From Table(QSYS2.IFS_READ(PATH_NAME => : wkTempFile, END_OF_LINE => 'ANY', MAXIMUM_LINE_LENGTH => 2000)) Limit 1; 19 Jwt = JwtHdrBase64 + '.' + JwtPayLdBase64 + '.' + JwtSignature;
Line 01: Will contain a unique IFS stream file name.
Line 02: We will generate the signature using a QShell command.
Line 03: Our JWT signature, in base64.
Line 04: Our final JWT value.
Line 05: A QCMDEXC prototype for running the QShell command.
Line 06: Our QShell command will write the JWT signature to an IFS stream file. To make our file name unique so simultaneously running jobs don’t step on each other, the GENERATE_UNIQUE scalar function will return a unique string 26 characters long. Example: 0780129106AC5B7743B4A10001
Line 07-14: Here we build our QShell command. This will be similar to the example from part three.
Lines 07-08: QShell looks in the current directory (CD) for any unqualified file names. Here the CD is /tmp. && means only run the next command if the prior was successful. Then the header and payload (separated by a single period) are printed to standard output.
Line 09: The standard output (header + ‘.’ + payload) is piped into the openssl command to sign that string.
Line 10: The string is signed using a private key file, assumed to be in /tmp. You could also include an explicit path here to the private key.
Line 11: The binary signature is written out to a unique stream file name in /tmp.
Line 12: The binary stream file is converted to base64.
Line 13: The base64 output is piped into the translate command to delete any linefeeds and equal signs, then converts the unsafe characters to URL safe characters. So why all the extra single quotes? The literals like \n= need to be enclosed in single quotes. And to add a single quote inside an RPG string, the single quote must be doubled up. And to run a command containing a single quote, that single quote must also be doubled up. So our QShellCmd variable will contain this substring when viewed in debug: tr -d ”\n=”.
Line 14: The base64 is redirected to the unique file name. This effectively truncates the existing file and writes new content to it. The unique file name is being repurposed.
Lines 15-16: The command is wrapped in STRQSH and executed. If you want robust error handling, you can instead run the QShell command in a CL program.
Lines: 17-18: The signature is read from the /tmp file into the JwtSignature variable using the IFS_READ table function.
Line 19: And finally, we have a JWT! The JWT would be passed in an HTTP request to an authorization server in exchange for an access token.
Debugging
At this point, it would be helpful to know if our new code is truly constructing a syntactically valid JWT. This is where the jwt.io website shines. First, select your JWT algorithm (figure 4). Then, paste the JSON header and payload into the corresponding sections (figure 8). And lastly paste your public and private keys into the two scrollable text areas (figure 8 again).
A JWT will be built and displayed. You want to see “Signature Verified” below the JWT (figure 9). Also verify that the JWT generated by your code matches the value generated by the jwt.io web page. If any JWT sections do not match your values, focus on debugging those. If the JWT looks correct but the signature is invalid then make sure your public and private keys are the correct pair and you pasted them correctly into the scrollable text areas.
The Access Token
Here we finish with a code snippet to pluck the access token from the HTTP response returned by the authorization server.
01 dcl-s wkHttpBody varChar(5000); 02 dcl-s wkHttpResp varChar(5000); 03 dcl-s wkHttpStatus packed(3:0); 04 dcl-s wkAccessToken varChar(2000); 05 wkHttpBody = 'Build HTTP body here including the JWT'; 06 // wkHttpResp = http_send_post(wkHttpBody); 07 wkHttpResp = '{"access_token":"eyJraWQi etc wAA8R1g","expires":300}'; 08 If (wkHttpStatus = 200); 09 Exec SQL SELECT COALESCE(temp_table.AccessToken, '') INTO :wkAccessToken FROM JSON_TABLE(:wkHttpResp, '$' COLUMNS( AccessToken VarChar(2000) PATH '$.access_token' ) ) as temp_table Limit 1; 10 EndIf;
Line 01: The HTTP body to send to the authorization server. The format will depend on the server but you will be including the JWT somewhere in the body.
Line 02: The HTTP JSON response from the authorization server.
Line 03: The 3 digit HTTP response status from the server. We hope to see a 200 “OK’.
Line 04: A variable to hold the access token.
Line 05: This is just pseudo-code. Build the HTTP body per the authorization server specs. Again, the JWT will be somewhere in the HTTP body.
Lines 06-07: This is more pseudo-code. Use the HTTP function of your choice.
Line 08: A 200 means the JWT was valid and an access token was returned in the JSON response.
Line 09: The JSON_TABLE table function is used to extract the access token from the JSON.
Line 10: The closing EndIf.
Outro
I was inspired to write this series of articles because I saw JWTs as a gap in the IBM i developer tool belt. I hope you enjoyed the articles. If you read all 4 of them, you should have the knowledge and confidence to create a JWT and pass it to an authorization server. If that HTTP POST request requires the body to include some URL encoding or base64 in a JSON format, you can handle that too. Of course, I would suggest putting any repeating code into a service program. Well, that’s a wrap. Happy coding!
Chris Ringer began coding RPG programs in 1989, and after a recent unexpected but valuable detour to C# is happy to be back in the IBM world. In his spare time he enjoys cycling and running – and taking the family dog Eddie for walks.
RELATED STORIES
Guru: Web Concepts For The RPG Developer, Part 3
Guru: Web Concepts For The RPG Developer, Part 2
Guru: Web Concepts For The RPG Developer, Part 1
Guru: The PHP Path To Victory, Part 1
would be really nice an example using the native IBM Cryptographic Services APIs without resorting to hacking PASE, for performance. Crypto API can easily also offload to optimized processor units (now or in future). Would be nice and educational for the audience! 😀
I used the crypto API in the past to sign AWS things (HMAC), and the overall throughput was good.