Last active
May 3, 2023 19:48
-
-
Save thomasdarimont/888015a56689fcaf88f6d54c070a03bc to your computer and use it in GitHub Desktop.
PoC for encoding ReferenceTokens with expirationDate and signature
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Reference Token: bXl0b2tlbjEyMzoxNjgzMTMyODczNDgyOk1FUUNJR1hUTEwwS2xBS0kvNUh1eDdMWXFYMExuV1NFOVN4dTNtUnFyU2lsVU1wVEFpQjZEb2NYMXZZUTZ5WXVYR0VHM2xwQW42dFFqN1ZPMDgrOHRBajYzUldScEE9PQ | |
Reference token ID: mytoken123 | |
Timestamp: 1683132873482 | |
Verified: true | |
Expired: false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package demo; | |
import java.nio.charset.StandardCharsets; | |
import java.security.InvalidAlgorithmParameterException; | |
import java.security.KeyFactory; | |
import java.security.KeyPair; | |
import java.security.KeyPairGenerator; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.PrivateKey; | |
import java.security.PublicKey; | |
import java.security.Signature; | |
import java.security.spec.ECGenParameterSpec; | |
import java.security.spec.PKCS8EncodedKeySpec; | |
import java.security.spec.X509EncodedKeySpec; | |
import java.time.Clock; | |
import java.time.Instant; | |
import java.time.temporal.ChronoUnit; | |
import java.util.Base64; | |
public class ReferenceTokenEncoder { | |
private final Clock clock; | |
private final PrivateKey privateKey; | |
private final PublicKey publicKey; | |
private final String signatureAlgorithm; | |
public ReferenceTokenEncoder(Clock clock, PrivateKey privateKey, PublicKey publicKey, String signatureAlgorithm) { | |
this.clock = clock; | |
this.privateKey = privateKey; | |
this.publicKey = publicKey; | |
this.signatureAlgorithm = signatureAlgorithm; | |
} | |
public String encodeToken(String referenceTokenId, long expirationTimestamp) throws Exception { | |
String referenceTokenString = referenceTokenId + ":" + expirationTimestamp; | |
byte[] signature = signData(referenceTokenString); | |
String signatureString = Base64.getEncoder().encodeToString(signature); | |
return Base64.getEncoder().withoutPadding().encodeToString((referenceTokenString + ":" + signatureString).getBytes(StandardCharsets.UTF_8)); | |
} | |
public ReferenceToken decodeToken(String input) throws Exception { | |
String referenceTokenString = new String(Base64.getDecoder().decode(input), StandardCharsets.UTF_8); | |
String[] parts = referenceTokenString.split(":"); | |
if (parts.length != 3) { | |
throw new IllegalArgumentException("Invalid reference token string"); | |
} | |
String referenceTokenId = parts[0]; | |
long timestamp = Long.parseLong(parts[1]); | |
String signatureString = parts[2]; | |
byte[] signature = Base64.getDecoder().decode(signatureString); | |
String referenceTokenData = referenceTokenId + ":" + timestamp; | |
boolean verified = verifyData(referenceTokenData, signature); | |
boolean expired = timestamp < clock.millis(); | |
return new ReferenceToken(referenceTokenId, timestamp, verified && !expired); | |
} | |
private byte[] signData(String data) throws Exception { | |
Signature sig = Signature.getInstance(signatureAlgorithm); | |
sig.initSign(privateKey); | |
sig.update(data.getBytes(StandardCharsets.UTF_8)); | |
return sig.sign(); | |
} | |
private boolean verifyData(String data, byte[] signature) throws Exception { | |
Signature sig = Signature.getInstance(signatureAlgorithm); | |
sig.initVerify(publicKey); | |
sig.update(data.getBytes(StandardCharsets.UTF_8)); | |
return sig.verify(signature); | |
} | |
public record ReferenceToken(String referenceTokenId, long timestamp, boolean verified) { | |
public boolean isExpired(Clock clock) { | |
return timestamp < clock.millis(); | |
} | |
} | |
public static void main(String[] args) throws Exception { | |
String signatureAlgorithm = "SHA256withECDSA"; | |
KeyPair keyPair = Keys.generateKeyPair(); | |
String privateKeyString = Keys.privateKeyToString(keyPair.getPrivate()); | |
String publicKeyString = Keys.publicKeyToString(keyPair.getPublic()); | |
Clock clock = Clock.systemDefaultZone(); | |
ReferenceTokenEncoder tokenEncoder = new ReferenceTokenEncoder( // | |
clock, // | |
Keys.getPrivateKey(privateKeyString), // | |
Keys.getPublicKey(publicKeyString), // | |
signatureAlgorithm // | |
); | |
String referenceTokenId = "mytoken123"; | |
long expiresAtTimestamp = Instant.now().plus(5, ChronoUnit.MINUTES).toEpochMilli(); | |
String referenceTokenString = tokenEncoder.encodeToken(referenceTokenId, expiresAtTimestamp); | |
System.out.println("Reference Token: " + referenceTokenString); | |
ReferenceToken referenceToken = tokenEncoder.decodeToken(referenceTokenString); | |
System.out.println("Reference token ID: " + referenceToken.referenceTokenId()); | |
System.out.println("Timestamp: " + referenceToken.timestamp()); | |
System.out.println("Verified: " + referenceToken.verified()); | |
System.out.println("Expired: " + referenceToken.isExpired(clock)); | |
} | |
static class Keys { | |
public static final String KEY_ALGORITHM = "EC"; | |
private static final String CURVE_NAME = "secp256r1"; | |
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { | |
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KEY_ALGORITHM); | |
ECGenParameterSpec ecSpec = new ECGenParameterSpec(CURVE_NAME); | |
keyGen.initialize(ecSpec); | |
return keyGen.generateKeyPair(); | |
} | |
public static String publicKeyToString(PublicKey publicKey) { | |
return Base64.getEncoder().encodeToString(publicKey.getEncoded()); | |
} | |
public static String privateKeyToString(PrivateKey privateKey) { | |
return Base64.getEncoder().encodeToString(privateKey.getEncoded()); | |
} | |
public static PrivateKey getPrivateKey(String privateKey) throws Exception { | |
byte[] keyBytes = Base64.getDecoder().decode(privateKey); | |
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); | |
KeyFactory keyFactory = KeyFactory.getInstance(Keys.KEY_ALGORITHM); | |
return keyFactory.generatePrivate(keySpec); | |
} | |
public static PublicKey getPublicKey(String publicKey) throws Exception { | |
byte[] keyBytes = Base64.getDecoder().decode(publicKey); | |
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); | |
KeyFactory keyFactory = KeyFactory.getInstance(Keys.KEY_ALGORITHM); | |
return keyFactory.generatePublic(keySpec); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
JWK: {"kty":"EC","d":"doCDsWOra9c8OmOnyP8ASWWkiziLId6W5Qc5U8I32jE","crv":"P-256","kid":"123","x":"WBhLezpT8YmLPvMxR8-HexJmD26ZDIaiLEGmTIEtrI8","y":"cvYF_nnKY-AIH4FHgTArNwYPS-dx2YGiDTgC3jOMo2w"} | |
Token: eyJraWQiOiIxMjMiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJodHRwczovL2lkLmFjbWUudGVzdC9hdXRoL3JlYWxtcy9kZW1vIiwiZXhwIjoxNjgzMTQzNDI3MzM0LCJqdGkiOiJjNjI2MDEzYi0yMTkyLTQ0ZGUtYTg1Ny02MGJjM2IwOTdkYTEifQ.y_GbepRNEKRRQ2Rq60hPCELBHXhvEj3xDKYu4DxjbiQcsmrAWM6waHLdRCZCs33hYjLJScjOwB-TLC5M5udynA | |
Valid: true | |
Payload: {"iss":"https://id.acme.test/auth/realms/demo","exp":1683143427334,"jti":"c626013b-2192-44de-a857-60bc3b097da1"} | |
Expired: false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package demo; | |
import com.nimbusds.jose.JOSEObjectType; | |
import com.nimbusds.jose.JWSAlgorithm; | |
import com.nimbusds.jose.JWSHeader; | |
import com.nimbusds.jose.JWSObject; | |
import com.nimbusds.jose.JWSSigner; | |
import com.nimbusds.jose.JWSVerifier; | |
import com.nimbusds.jose.Payload; | |
import com.nimbusds.jose.crypto.ECDSASigner; | |
import com.nimbusds.jose.crypto.ECDSAVerifier; | |
import com.nimbusds.jose.jwk.Curve; | |
import com.nimbusds.jose.jwk.ECKey; | |
import com.nimbusds.jose.jwk.gen.ECKeyGenerator; | |
import com.nimbusds.jwt.JWTClaimNames; | |
import java.time.Instant; | |
import java.time.temporal.ChronoUnit; | |
import java.util.Map; | |
import java.util.UUID; | |
public class NimbusReferenceTokenExample { | |
public static void main(String[] args) throws Exception { | |
ECKey ecJWK = new ECKeyGenerator(Curve.P_256).keyID("123").generate(); | |
System.out.println("JWK: " + ecJWK); | |
JWSSigner signer = new ECDSASigner(ecJWK); | |
String referenceTokenId = UUID.randomUUID().toString(); | |
long expiresAtTimestamp = Instant.now().plus(5, ChronoUnit.MINUTES).toEpochMilli(); | |
String issuer = "https://id.acme.test/auth/realms/demo"; | |
Map<String, Object> data = Map.of( // | |
JWTClaimNames.JWT_ID, referenceTokenId, // | |
JWTClaimNames.EXPIRATION_TIME, expiresAtTimestamp, // | |
JWTClaimNames.ISSUER, issuer | |
); | |
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(ecJWK.getKeyID()).type(JOSEObjectType.JWT).build(); | |
Payload payload = new Payload(data); | |
JWSObject jwsObject = new JWSObject(header, payload); | |
jwsObject.sign(signer); | |
String token = jwsObject.serialize(); | |
System.out.println("Token: " + token); | |
JWSVerifier verifier = new ECDSAVerifier(ecJWK.toPublicJWK()); | |
boolean valid = jwsObject.verify(verifier); | |
System.out.println("Valid: " + valid); | |
Payload verifiedPayload = jwsObject.getPayload(); | |
System.out.println("Payload: " + verifiedPayload); | |
long expiresAt = Long.parseLong(String.valueOf(verifiedPayload.toJSONObject().get(JWTClaimNames.EXPIRATION_TIME))); | |
System.out.println("Expired: " + (expiresAt < System.currentTimeMillis())); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment