結構
基本的JWT包含三個段落
- Header: 包含加簽的演算法,Key的ID等資訊
- Payload: 以名為Claim的欄位來標示相關的資訊,如產生的時間,最後可用的時間,還允加入許自訂的內容
- Signature: 利用hash機制加簽相關的訊息。
Unsecured JWT 與 JWS
純的JWT,也就是沒有第三部份Signature的JWT,可稱為Unsecured JWT。
若有第三部份Signature的JWT,就是JWS
若連內容都加密就會是JWE
加簽:內容 + secret => hash code (signature),將內容用secret算出一個hash code做為加簽的比對值,
其他人如果要驗證,則需持有同一個secret,自行再對內容hash,比對兩方的hash結果是否相同。若相同則可視為由已知對象所發出的內容。
加密:使用公鑰來進行內容的加密,要驗證的人需有私鑰。
Unsecured JWT
Concept
Unsecured JWT 的定義為,有一個"alg":"none"的Header,而且在第三部份Signature的內容為空。
Structure
BASE64URL(UTF8(JWS Protected Header)) ||
'.' ||
BASE64URL(JWS Payload) ||
'.' ||
eyJhbGciOiJub25lIn0
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.
Nimbus example
//Payload
JWTClaimsSet claimSet = new JWTClaimsSet.Builder()
.subject("sc")
.claim("user", new User("A123", "John")) //自訂內容
.build();
PlainJWT jwt = new PlainJWT(claimSet);
log.info("Plain JWT: [{}]", jwt.serialize());
log.info("Readable JWT: [{}.{}]", jwt.getHeader().toString(), jwt.getPayload().toString());
可見結果
Plain JWT: [
eyJhbGciOiJub25lIn0
.eyJzdWIiOiJzYyIsInVzZXIiOnsibmFtZSI6IkpvaG4iLCJpZCI6IkExMjMifX0
.
]
Readable JWT: [
{"alg":"none"}
.{"sub":"sc","user":{"name":"John","id":"A123"}}
.
]
JWS
Concept
先說明加簽與驗證的概念,
被加簽的內容是header + payload,而signature的部份比較像是MD5,只是需要透過特定的secret來產生。
header提供了算法名稱,系統分享了secret,再對內容重新算一次signature,與原來jws裡提供的signature比對是否相同。
Structure
BASE64URL(UTF8(JWS Protected Header)) ||
'.' ||
BASE64URL(JWS Payload) ||
'.' ||
BASE64URL(JWS Signature)
Sample
eyJhbGciOiJIUzI1NiJ9
.
eyJzdWIiOiJzYyIsInVzZXIiOnsibmFtZSI6IkpvaG4iLCJpZCI6IkExMjMifX0
.
9hSHQ_xiIY3fpHL5COKxeoq_s7SEH6n7netmTDQP4vc
Nimbus example
Nimbus Sample
byte[] secret = new byte[32];
new SecureRandom().nextBytes(secret);
JWTClaimsSet claimSet = new JWTClaimsSet.Builder()
.subject("sc")
.claim("user", new User("A123", "John")) //自訂內容
.build();
SignedJWT jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimSet);
jwt.sign(new MACSigner(secret));
JWSVerifier verifier = new MACVerifier(secret);
boolean verify1 = verifier.verify(jwt.getHeader(), jwt.getSigningInput(), jwt.getSignature());
boolean verify2 = jwt.verify(verifier);
boolean verify3 = this.verifyJWS(jwt.serialize(), secret);
Assert.isTrue(verify1, "JWSVerifier.verify failed.");
Assert.isTrue(verify2, "jwt.verify(verifier) failed.");
Assert.isTrue(verify3, "HMAC failed.");
log.info("JWS: [{}]", jwt.serialize());
log.info("secret: [{}], verify [{}]", Base64.getUrlEncoder().encodeToString(secret), verify1);
log.info("Readable JWS: [{}.{}.{}]", jwt.getHeader().toString(), jwt.getPayload().toString(), Base64.getUrlEncoder().encodeToString(jwt.getSignature().decode()));
可見結果
expectedHMAC [IvG_ZvOQb3s-Y2ZUscc6U5IXrAzF0Z8CRwhUlDE82eg=]
JWS: [eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzYyIsInVzZXIiOnsibmFtZSI6IkpvaG4iLCJpZCI6IkExMjMifX0.IvG_ZvOQb3s-Y2ZUscc6U5IXrAzF0Z8CRwhUlDE82eg]
secret: [9Iv9REtzUYadK0JI2b9KzY3p-AxqOTMuLYPRbWYE8FA=], verify [true]
Readable JWS: [{"alg":"HS256"}.{"sub":"sc","user":{"name":"John","id":"A123"}}.IvG_ZvOQb3s-Y2ZUscc6U5IXrAzF0Z8CRwhUlDE82eg=]
以Sample來看,Header與Payload的建立方式與先前的Unsecured JWT沒有太大差別。
差異是在要在Header指定加簽的演算法,同時使用特定的JWSSigner與secret產生第三部份Signature。
而Signature的驗證則需反向指定JWSVerifier與secret來驗證。
也就是說,secret是需要被分享的。
以下為Java 如何去做驗證的簡易程式範例
boolean result = false;
SignedJWT jws = SignedJWT.parse(jwt);
//MACProvider.getJCAAlgorithmName: HS256 -> HMACSHA256, HS384 -> HMACSHA384, HS512-> HMACSHA512
SecretKeySpec secretKeySpec = new SecretKeySpec(secret, "HMACSHA256");
Mac mac = Mac.getInstance(secretKeySpec.getAlgorithm()); // => Mac.getInstance("HMACSHA256")
mac.init(secretKeySpec);
mac.update(jws.getSigningInput()); //header.payload
byte[] expectedHMAC = mac.doFinal();
log.info("expectedHMAC [{}]", Base64.getUrlEncoder().encodeToString(expectedHMAC));
log.info("signature [{}]", Base64.getUrlEncoder().encodeToString(jws.getSignature().decode()));
result = ConstantTimeUtils.areEqual(expectedHMAC, jws.getSignature().decode());
JWE
Structure
BASE64URL(UTF8(JWE Protected Header)) ||
'.' ||
BASE64URL(JWE Encrypted Key) ||
'.' ||
BASE64URL(JWE Initialization Vector) ||
'.' ||
BASE64URL(JWE Ciphertext) ||
'.' ||
BASE64URL(JWE Authentication Tag)
eyJlbmMiOiJBMTI4R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0
.
bpn2DiYdqOigAwQX4Ao7s0AFqFWhMsAkJh9nuFGPU5DRdu1UD9jL5j-2WghEBrFUSsxrq67exXdzlTTCWKSdAusqj085RMzBj5tDwnvlNLMT3JPvh4iw2zpUFqS68xfWNnXk7aM_KNPJ6ryfHrF36vST3z9j2TLt0D6diz3W2NdP6sOtqdnKYlNjytgskncwVLkxfJedcOQWSj0uDq_40AakxQsA9qrurC_4VJQKMCeLgzFq4OpOOX14t0s1fxLJRaeYy2yvbDSfuPwzKtwh7az8I6ABv1InjKphd9RnejaqqOHPIXjlJ9x65JK8Ej7Pa7snuX7Zgv857hjHFAEinQ
.
YOTS6-kP30eUZoSL
.
ikFXwsKkbCmPEk2pC-gaajdlrwnwfRoMFeaVeEUOzj_XUUpPcv633_SNLAxR4AI
.
7UFdHjYJgaHsHCRlnSCzhA
Nimbus example
產生PublicKey與PrivateKey
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair kp = keyPairGenerator.genKeyPair();
RSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(kp.getPublic(), RSAPublicKeySpec.class);
RSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(kp.getPrivate(), RSAPrivateKeySpec.class);
RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(publicKeySpec);
RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(privateKeySpec);
用publicKey加密payload
JWEHeader header = new JWEHeader(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM);
JWTClaimsSet claimSet = new JWTClaimsSet.Builder()
.subject("sc")
.claim("user", new User("A123", "John"))
.build();
EncryptedJWT jwe = new EncryptedJWT(header, claimSet);
RSAEncrypter encrypter = new RSAEncrypter(publicKey);
encrypter.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance());
jwe.encrypt(encrypter);
String jwsString = jwe.serialize();
log.info("JWS: [{}]", jwsString);
用privateKey解JWS
jws = EncryptedJWT.parse(jwsString);
RSADecrypter decrypter = new RSADecrypter(privateKey);
decrypter.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance());
jwe.decrypt(decrypter);
log.info("JWE Header: [{}]", jwe.getHeader().toString());
log.info("JWE Payload: [{}]", jwe.getPayload().toString());
log.info("JWE Initial Vector: [{}]", jwe.getIV().toString());
log.info("JWE Cipher text: [{}]", jwe.getCipherText().toString());
log.info("JWE Auth Tag: [{}]", jwe.getAuthTag());
可見結果如下
JWE: [
eyJlbmMiOiJBMTI4R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0
.bpn2DiYdqOigAwQX4Ao7s0AFqFWhMsAkJh9nuFGPU5DRdu1UD9jL5j-2WghEBrFUSsxrq67exXdzlTTCWKSdAusqj085RMzBj5tDwnvlNLMT3JPvh4iw2zpUFqS68xfWNnXk7aM_KNPJ6ryfHrF36vST3z9j2TLt0D6diz3W2NdP6sOtqdnKYlNjytgskncwVLkxfJedcOQWSj0uDq_40AakxQsA9qrurC_4VJQKMCeLgzFq4OpOOX14t0s1fxLJRaeYy2yvbDSfuPwzKtwh7az8I6ABv1InjKphd9RnejaqqOHPIXjlJ9x65JK8Ej7Pa7snuX7Zgv857hjHFAEinQ
.YOTS6-kP30eUZoSL
.ikFXwsKkbCmPEk2pC-gaajdlrwnwfRoMFeaVeEUOzj_XUUpPcv633_SNLAxR4AI
.7UFdHjYJgaHsHCRlnSCzhA
]
JWE Header: [{"enc":"A128GCM","alg":"RSA-OAEP-256"}]
JWE Payload: [{"sub":"sc","user":{"name":"John","id":"A123"}}]
JWE Initial Vector: [YOTS6-kP30eUZoSL]
JWE Cipher text: [ikFXwsKkbCmPEk2pC-gaajdlrwnwfRoMFeaVeEUOzj_XUUpPcv633_SNLAxR4AI]
JWE Auth Tag: [7UFdHjYJgaHsHCRlnSCzhA]