Skip to content

Instantly share code, notes, and snippets.

@douglascayers
Last active October 21, 2022 19:19
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save douglascayers/4d35c50850a2ddeea2134da14d73ebf8 to your computer and use it in GitHub Desktop.
Save douglascayers/4d35c50850a2ddeea2134da14d73ebf8 to your computer and use it in GitHub Desktop.
Sign a JWT token with only a private key
/**
* Inspired by the JWT repo by Salesforce Identity
* https://github.com/salesforceidentity/jwt/
*
* Inspired by the JWT repo by Auth0
* https://github.com/auth0/java-jwt
*
* Learn more about JWT at https://jwt.io
*/
public inherited sharing class JWT {
public interface IAlgorithm {
/**
* Gets the name of the signing algorithm
* to specify as the 'alg' JWT header property.
* Must be one of https://tools.ietf.org/html/rfc7518#section-3
*/
String getName();
/**
* Cryptographically signs the header and payload
* and returns the signature.
*
* The header and payload string values should already
* be base64 URL encoded per JWT specifications.
*/
Blob sign(
String base64UrlEncodedHeader,
String base64UrlEncodedPayload
);
}
public class RSA256Algorithm implements IAlgorithm {
private String privateKey;
/**
* Provide a base64 encoded RSA-SHA256 private key.
*/
public RSA256Algorithm(String base64EncodedPrivateKey) {
this.privateKey = base64EncodedPrivateKey;
}
public String getName() {
// This is the algorithm name per
// the JWT spec https://tools.ietf.org/html/rfc7518#section-3
return 'RS256';
}
public Blob sign(
String base64UrlEncodedHeader,
String base64UrlEncodedPayload
) {
// The valid values for algorithm for Crypto.sign method
// are RSA-SHA1, RSA-SHA256, or RSA.
return Crypto.sign(
'RSA-SHA256',
Blob.valueOf(
base64UrlEncodedHeader +
'.' +
base64UrlEncodedPayload
),
EncodingUtil.base64Decode(
this.privateKey
)
);
}
}
// ------------------------------------------------------------------------
@TestVisible private IAlgorithm algorithm;
@TestVisible private Map<String, String> headerClaims;
@TestVisible private Auth.JWT payloadClaims;
public JWT(
IAlgorithm algorithm,
Auth.JWT payloadClaims
) {
this.algorithm = algorithm;
this.headerClaims = new Map<String, String>{
'alg' => algorithm.getName(),
'typ' => 'JWT'
};
this.payloadClaims = payloadClaims;
}
public String sign() {
String header = base64UrlEncode(
Blob.valueOf(JSON.serialize(this.headerClaims))
);
String payload = base64UrlEncode(
Blob.valueOf(this.payloadClaims.toJSONString())
);
String signature = base64UrlEncode(
this.algorithm.sign(header, payload)
);
return String.format(
'{0}.{1}.{2}',
new String[] { header, payload, signature }
);
}
/**
* Base64 URL encodes data.
* The main difference between "Base64" and "Base64 URL"
* is that '+' is replaced with '-'
* and that '/' is replaced with '_'.
* https://tools.ietf.org/html/rfc4648#section-5
*/
private String base64UrlEncode(Blob input) {
String output = EncodingUtil.base64Encode(input);
output = output.replace('+', '-');
output = output.replace('/', '_');
return output;
}
}
@IsTest
private class JWTTest {
@IsTest
static void test_sign_jwt() {
// grabbed a fake key from
// http://phpseclib.sourceforge.net/rsa/2.0/examples.html
String mockPrivateKey = String.join(new String[] {
'MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp',
'wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5',
'1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh',
'3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2',
'pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX',
'GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il',
'AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF',
'L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k',
'X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl',
'U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ',
'37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0='
}, '');
Test.startTest();
JWT.IAlgorithm alg = new JWT.RSA256Algorithm(
mockPrivateKey
);
Auth.JWT jwtClaims = new Auth.JWT();
jwtClaims.setValidityLength(5 * 60); // seconds
jwtClaims.setAud('your audience claim here');
jwtClaims.setAdditionalClaims(new Map<String, Object>{
'some_key' => 'some value'
});
JWT j = new JWT(alg, jwtClaims);
String token = j.sign();
System.debug(token);
Test.stopTest();
System.assertNotEquals(null, token);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment