JWS / JWT with Android biometric or PIN prompt

The Android keystore has an API designed to prevent leaks of private keys as well as minimise the risk of unauthorised key use. By creating the key with the setUserAuthenticationRequired(true) option the keystore will require each key signing operation to pass successful authentication (PIN, password, biometric, etc) of the app user. After that the key will be unlocked to complete a single signing. If the signing doesn't take place the key will become disabled after a while.

Support for user authentication when performing JSON Web Signature (JWS) operations appeared in v9.4 of Nimbus JOSE+JWT (see ticket).

In order to perform JWS with user authentication the JWSSigner needs to be created with the UserAuthenticationRequired option. During the signing a special exception will get thrown to interrupt the flow and let the app code invoke an authentication prompt and then resume signing by calling a provided Completable interface.

At the time of writing this article the two step signing is available for the JWS RSxxx, PSxx and ESxxx family of algorithms.

Example Java code for signing an object with a private RSA key which must be unlocked by the user after successful biometric, PIN or some other authentication mechanism:

import java.security.*;
import java.util.*;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jose.crypto.opts.*;

// Option to trigger an Android user authentication prompt after the
// signature gets initiated
Set<JWSSignerOption> opts = new HashSet<>();
opts.add(UserAuthenticationRequired.getInstance());

// Create an RSA signer with a reference to a private RSA 2048-bit key in the
// Android key store
JWSSigner signer = new RSASSASigner(privateKey, opts);

// The payload to sign
Payload payload = new Payload("Hello, world!");

// Create the JWS object
JWSObject jwsObject = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), payload);

// Initiate the signing and watch for a prompt exception
ActionRequiredForJWSCompletionException actionRequired = null;
try {
    jwsObject.sign(signer);
} catch (ActionRequiredForJWSCompletionException arException) {
    // Copy the exception for processing
    actionRequired = arException;
} catch (JOSEException e) {
    throw new RuntimeException("Internal signing error: " + e.getMessage(), e);
}

if (actionRequired != null) {
    // Perform user authentication to unlock the private RSA key,
    // e.g. with biometric prompt
    Signature sig = actionRequired.getInitializedSignature();

    // Pass the Signature in a CryptoObject to the biometric prompt
    // ...

    // Complete the signing when the private key is unlocked
    actionRequired.getCompletableJWSObjectSigning().complete();
}

// Output the JWS
System.out.println(jwsObject.serialize());