CollabSphere 2020 Slides and Video

Oct 29, 2020, 4:09 PM

One of the nice bonuses of an all-online conference is that session recording comes built-in, so I was able to snag that and put it up on YouTube for posterity:

Additionally, I uploaded my slides to SlideShare, though that loses out on the extremely-fancy 5-second videos I used:

CollabSphere 2020: DEV101 - Add Continuous Delivery to Domino with the NSF ODP Tooling

Oct 26, 2020, 9:54 AM

CollabSphere 2020 is starting tomorrow, this year naturally taking the form of an online conference, which has the nice benefit of meaning that you can still sign up if you haven't done so, and you're only restricted by your time zone offset for attending.

For my part, I'll be giving a presentation on the NSF ODP Tooling, currently slated for tomorrow:

DEV101 - Add Continuous Delivery to Domino with the NSF ODP Tooling

Domino applications, stored in NSFs, have been historically difficult to add to Continuous Integration tools like Jenkins and to have participate in Continous Delivery workflows. This session will discuss the NSF ODP Tooling project on OpenNTF, which allows you to take Domino-based projects - whether targetting the Notes client or web, XPages or not - and integrate them with modern tooling and flows. It will demonstrate use with projects ranging from a single NSF to a suite of a dozen OSGi plguins and two dozen NSFs, showing how they can be built and packaged automatically and consistently.

I hope you'll be able to attend - there are definitely some very-interesting topics lined up.

A Notes-Client-Friendly Way To Access JWT-Protected Resources

Oct 2, 2020, 3:32 PM

Tags: lotusscript

A Notes-Client-Friendly Way To Access JWT-Protected Resources

I recently had call to access the Zoom REST API in a Notes client app that will be maintained by other Notes programmers, so I figured it'd be as good an opportunity as any to use the HTTP and JSON classes added in V10 and 11.

The basics there are fine enough - though those classes aren't featureful, they can get the job done. However, the Zoom API needs specialized authentication, beyond the username/password type that you can kind of work your way to in LotusScript alone. Since my needs will be administrative as opposed to multiple users acting as themselves, I decided to go the JWT route instead of OAuth.

JWT

JWT stands for "JSON Web Token", and it's one of the now-common ways to do secure authorization without passing passwords around. It's simple at its core - just some JSON objects to indicate the type of token and the payload of app-specific claims you're going to make, then a cryptographic signature.

It's that last part that moves it out of the realm of LotusScript (barring some way to wrangle the SEC* functions in the C API to do it), so I went to Java and LS2J to bridge the gap.

The Java Side

I lucked out in that the Zoom API uses a pretty simple path for generating the signature - my previous experience with JWT involved public/private key pairs, which is still doable but is more annoying. Additionally, the payload is pretty simple, just asserting that you're logging in, with nothing like the specialized user ID lookups I had to do with SharePoint. This meant I could get away with writing out the token "manually" rather than going through the onerous process of creating script libraries out of one of the available libraries and its dependency tree.

One gotcha is that the JDK doesn't actually ship with JSON support. Fortunately, in this case, the only values going in were JSON-friendly and didn't need escaping, but I'd suggest using even a basic library like the agent-friendly JSON-java for normal uses.

I ended up making a static method in a single-class Java script library:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package us.iksg;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.concurrent.TimeUnit;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class JWTGenerator {
    public static final long TIMEOUT = TimeUnit.HOURS.toMillis(1);
    
    public static String generateJWT(String apiKey, String apiSecret) {
        try {
            long now = System.currentTimeMillis();
            long exp = now + TIMEOUT;
            
            // Note to the future: I apologize for writing JSON via string concatenation, but it
            //   _should_ be safe here.
            
            // Header: alg: HS256, typ: JWT
            String headerJson = "{\"alg\": \"HS256\", \"typ\": \"JWT\"}";
            String headerB64 = Base64.getUrlEncoder().encodeToString(headerJson.getBytes(StandardCharsets.UTF_8));
            
            // Payload: iss: API_KEY, exp: exp
            String payloadJson = "{" +
                    "\"iss\": \"" + apiKey + "\"," +
                    "\"exp\": \"" + exp + "\"" +
                "}";
            String payloadB64 = Base64.getUrlEncoder().encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
            
            // Codec: HMAC SHA256 (HS256)
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(spec);
            byte[] signature = mac.doFinal((headerB64 + "." + payloadB64).getBytes(StandardCharsets.UTF_8));
            String signatureB64 = Base64.getUrlEncoder().encodeToString(signature);
            
            return headerB64 + '.' + payloadB64 + '.' + signatureB64;
        } catch(Throwable t) {
            throw new RuntimeException(t);
        }
    }
}

All of those classes come with the JDK, so it's nice and self-contained.

The LotusScript Side

Back on the LotusScript side, I brought out my trusty old friend LS2J:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Uselsx "*javacon"
Use "JWT Generator"

Sub Click(Source As Button)
    On Error Goto errorHandler
    
    Dim session As New NotesSession, ws As New NotesUIWorkspace, doc As NotesDocument
    Set doc = ws.CurrentDocument.Document
    
    Dim jsession As New JAVASESSION, jwtGenerator As JavaClass
    Set jwtGenerator = jsession.GetClass("us.iksg.JWTGenerator")
    
    Dim apiKey As String, apiSecret As String
    apiKey = doc.ZoomAPIKey(0)
    apiSecret = doc.ZoomAPISecret(0)
    
    Dim generate As JavaMethod
    Set generate = jwtGenerator.GetMethod("generateJWT", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;")
    
    Dim token As String
    token = generate.Invoke(Empty, apiKey, apiSecret)
    ' In this case, it's in a "developer playground" form I made for testing.
    ' Do not store JWT tokens long-term - they should be generated for each script.
    doc.ZoomJWTToken = token
    
    Exit Sub
errorHandler:
    Msgbox Erl & ": " & Error
    End
End Sub

The only unusual bit here is that, since I used a static method, I pass Empty as the first parameter to Invoke. I tend to use the reflection-based approach like this out of habit after consistently running into trouble with LS2J's mapping of methods to their Java counterparts, but it'd probably be a little cleaner if I made it an instance method and just called it directly.

Once I had the generated token, I was able to include it in my HTTP requests:

1
2
3
4
5
6
7
8
9
Dim req As NotesHTTPRequest
Set req = session.CreateHTTPRequest()
Call req.SetHeaderField("Authorization", "Bearer " & token)
' Since we just want to plunk this into the field, request a string back
req.PreferStrings = True

Dim result As String
result = req.Get("https://api.zoom.us/v2/users")
doc.Users = result

Not too shabby overall, for the Notes client. I may end up putting all these calls into run-on-server agents regardless just to avoid trouble should the client end up having their users use the Web Assembly or mobile Notes clients, but even then this still ends up very Notes-client-developer-friendly.