JWT

結構

基本的JWT包含三個段落

  1. Header: 包含加簽的演算法,Key的ID等資訊
  2. Payload: 以名為Claim的欄位來標示相關的資訊,如產生的時間,最後可用的時間,還允加入許自訂的內容
  3. 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]

發表迴響

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料