Learn OIDC - Part 1 - JWS

4 minute read

Introduction

For a project I was working on, I was working a lot with OIDC-related technology.

Somewhere in the project there was the need for an easy to use and easy to configure OIDC server mock. We used Keycloak1, which is highly configurable, to act as an OIDC server for our end-to-end test needs. This worked quite well.

But at some point we needed something which would be easy to mock for our (integration) tests. Googling around didn’t reveal any lightweight, easy to configure mock OIDC server, so I decided to try to build one myself.

WARNING: The code shown in this series is for educational purposes only and is not meant to be used in production.

JWS

One of the building blocks of OIDC is JWS (JSON Web Signature)2. JWS is the underlying structure for JWTs (JSON Web Tokens). JWT is the structure being used to transmit authorization in OAuth and user identity info in OIDC. We’ll dive into JWT in a later post.

But I thought that it would be a good idea to take a quick look at JWS first.

Structure

JSON Web Signature is basically a digitally signed payload and a header describing the type of contents and the type of signature. The most common way to represent a JWS is Compact Serialization. The specification describes this as:

BASE64URL(UTF8(JWS Protected Header)) || ‘.’ || BASE64URL(JWS Payload) || ‘.’ || BASE64URL(JWS Signature)

Where JWS Payload can be any kind of payload. This could be JSON but could also be some arbitrary byte-array.

The most common signature algorithms being used on examples on the web are HS256 and RS256 which stands HMAC using SHA-256 and RSASSA-PKCS1-v1_5 using SHA-256 respectively. Implementing a JWS generation method could be done using standard Java APIs.

HS256

The following function generates a valid JWT compact representation for any byte[] payload using the provided secret key. This secret key is supposed to be shared between sender and recipient of the JWS.

static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding();

public static String compactHS256(byte[] payload, SecretKey secretKey) throws GeneralSecurityException {
    var header = """
            {"alg": "HS256"}
            """;
    var encodedHeader = ENCODER.encodeToString(header.getBytes(StandardCharsets.UTF_8));
    var encodedPayload = ENCODER.encodeToString(payload);
    var signingInput = encodedHeader + "." + encodedPayload;

    var hmac = Mac.getInstance("HmacSHA256");
    hmac.init(secretKey);
    var signature = ENCODER.encodeToString(hmac.doFinal(signingInput.getBytes()));
    return signingInput + "." + signature;
}

RS256

Since the previous signature method (rs256) requires a shared secret to be shared between both parties, it might not be the best fit for implementing OIDC servers. A better approach would be to rely on (RSA) public/private key pairs. This could be achieved with the rs256 algorithm. This implementation looks quite similar to the hs256 one, but uses a Signature object instead of a Mac object. And of course the input parameter is an RSA privateKey instead of some shared secret.

static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding();

public static String compactRS256(byte[] payload, RSAPrivateKey privateKey) throws GeneralSecurityException {
    var header = """
            {"alg": "RS256"}
            """;
    var encodedHeader = ENCODER.encodeToString(header.getBytes(StandardCharsets.UTF_8));
    var encodedPayload = ENCODER.encodeToString(payload);
    var signingInput = encodedHeader + "." + encodedPayload;

    var signer = Signature.getInstance("SHA256withRSA");
    signer.initSign(privateKey);
    signer.update(signingInput.getBytes());
    var signature = ENCODER.encodeToString(signer.sign());
    return signingInput + "." + signature;
}

Example usage

The method above is quite easy to use. It requires a payload, let’s use the string Hello World! for this example. The most complex part would be to load the private key to use for the signing. For now let’s assume that the file is present in the file signing.der3.

// Load private key
var keyFactory = KeyFactory.getInstance("RSA");
var keyFileName = "signing.der";
byte[] privateKeyBytes = Files.readAllBytes(new File(keyFileName).toPath());
var privateKey = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));

// Example payload
byte[] payload = "Hello World!".getBytes(StandardCharsets.UTF_8);

var compactJws = Jws.compactRS256(payload, privateKey);
System.out.println(compactJws);

This would print something like:

eyJhbGciOiAiUlMyNTYifQo.SGVsbG8gV29ybGQh.<signature-bytes>

This compactJws could be sent to a receiving party which could verify the integrity of the message by checking the signature using the public key of the sender.

Multiple keys

In a lot of systems keys tend to be rotated every now and then. This is also the case for signing keys for JWS.

To give recipients an indication of which key was used to sign the message, a special kid (Key ID) claim in the JWS header could be utilized. The kid is an identifier of the key that was used to sign the specific message.

NOTE: The specification doesn’t say anything about how key ids map to a specific key. This has to be agreed upon by the sending and receiving side (typically chosen by the sender side).

One elegant way to communicate the mapping of kid to the public key is the use of JWK Set4. This JWK Set can list multiple ‘kid’ to public key mappings.

Add kid claim

Adding support for the kid claim to our JWS implementation is quite straightforward. The only thing that we need to change is the header variable:

public static String compactRS256(byte[] payload, RSAPrivateKey privateKey, String kid) throws GeneralSecurityException {
    var header = """
            {"alg": "RS256","kid": "%s"}
            """.formatted(kid);
    var encodedHeader = ENCODER.encodeToString(header.getBytes(StandardCharsets.UTF_8));
    var encodedPayload = ENCODER.encodeToString(payload);
    var signingInput = encodedHeader + "." + encodedPayload;

    var signer = Signature.getInstance("SHA256withRSA");
    signer.initSign(privateKey);
    signer.update(signingInput.getBytes());
    var signature = ENCODER.encodeToString(signer.sign());
    return signingInput + "." + signature;
}

Summary

We implemented a very bare-bones method to create valid JWS objects which basically contains a digitally signed payload. We looked at two separate mechanisms to sign the JWS: shared secret (hs256) en public/private key (rs256).

This should serve as a good starting point for implementing JWT on top of it. … Which will be a topic of an upcoming blog post.

  1. Keycloak is an Open Source Identity and Access Management, see https://keycloak.org 

  2. JWS Specification: RFC 7515 

  3. The file signing.der could be generated using the openssl command: openssl genrsa -outform der -out signing.der 2048 && openssl rsa -in signing.pem -outform DER -out signing.der 

  4. JSON Web Key Set: RFC 7517 - Section 5 

Updated: