[Java]ES256アルゴリズムを用いたJWT生成方法 メモ

* [Sign in with Apple 用クライアントシークレット](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)をES256を用いたJWT形式で生成する必要があったため、Javaでその生成方法を検証した。その際のメモを残す。



## JWTとは

- JSON Web Tokenの略称。

- 電子署名による改竄検知機能を持ったトークン。

- 認証用途などで用いられる。

- ピリオド（"."）区切りのHeader部、Payload部、Signature部から構成される。

  ```
  Header.Payload.Signature
  ```

 * Header部：JWTの検証に必要な情報から構成される。署名に使用するアルゴリズム(`alg`)や署名に使用する鍵（公開鍵/秘密鍵）を識別する値(`kid`)などが含まれる。
 * Payload部：JWTに関する属性情報から構成される。JWTの発行者(`aud`)や発行日時(`iat`)などの情報が含まれる。
 * Signature部：Header部とPayload部を合わせた文字列に署名した値。Header部に指定したアルゴリズムと秘密鍵を使用して署名を行う。

* 各部位はBase64URLエンコードされる。
* 署名アルゴリズムには、ES256やRSA-SHA256などが用いられる。
     - ES256：「P-256 および SHA-256 を使⽤した ECDSA」の別称。
            * ECDSA：Elliptic Curve Digital Signature Algorithm, 楕円曲線デジタル署名アルゴリズム
            * P-256：使⽤されるアルゴリズムのバージョン。


## サンプルコード

* [Sign in with Apple のJWT例](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)を構成情報とした生成コード。
  * デベロッパーコンソールから取得した鍵は使用せず、自前で生成した鍵を使用して署名を行う。
  * エラーハンドリングなどは省略。

```java
package jwt_test;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.commons.codec.binary.Base64;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JWTTest {
	private static ECPublicKey PUBLIC_KEY;
	private static ECPrivateKey PRIVATE_KEY;

	public static void main(String[] args) {
		try {
			String jwt = generateJWT();
			verifyJWT(jwt, PUBLIC_KEY);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (InvalidAlgorithmParameterException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (SignatureException e) {
			e.printStackTrace();
		} catch (JsonProcessingException e) {
			e.printStackTrace();
		}
	}

	// JWT生成
	private static String generateJWT()
			throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, SignatureException,
			InvalidKeyException, JsonProcessingException {

		// 署名鍵生成
		// ※Sign in with Appleの場合は、コンソールから生成した秘密鍵を利用する。https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
		final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
		keyPairGenerator.initialize(new ECGenParameterSpec("secp256r1"));
		final KeyPair keyPair = keyPairGenerator.generateKeyPair();
		PUBLIC_KEY = (ECPublicKey) keyPair.getPublic();
		PRIVATE_KEY = (ECPrivateKey) keyPair.getPrivate();
		byte[] publicKeyEncodedBytes = Base64.encodeBase64(PUBLIC_KEY.getEncoded());
		byte[] privateKeyEncodedBytes = Base64.encodeBase64(PRIVATE_KEY.getEncoded());
		System.out.println("ES256 Public Key:" + new String(publicKeyEncodedBytes));
		System.out.println("ES256 Private Key:" + new String(privateKeyEncodedBytes));

		// ヘッダー部設定
		final ObjectMapper objectMapper = new ObjectMapper();
		final Map<String, Object> jwtHeader = new LinkedHashMap();
		jwtHeader.put("kid", "ABC123DEFG");
		jwtHeader.put("alg", "ES256");
		String jwtHeaderStr = Base64.encodeBase64URLSafeString(objectMapper.writeValueAsBytes(jwtHeader));

		// ペイロード部設定
		final Map<String, Object> jwtPayload = new LinkedHashMap();
		jwtPayload.put("iss", "DEF123GHIJ");
		jwtPayload.put("iat", 1437179036);
		jwtPayload.put("exp", 1493298100);
		jwtPayload.put("aud", "https://appleid.apple.com");
		jwtPayload.put("sub", "com.mytest.app");
		String jwtPayloadStr = Base64.encodeBase64URLSafeString(objectMapper.writeValueAsBytes(jwtPayload));

		// 署名設定
		final Signature jwtSignature = Signature.getInstance("SHA256withECDSAinP1363Format");
		jwtSignature.initSign(PRIVATE_KEY);
		jwtSignature.update((jwtHeaderStr + "." + jwtPayloadStr).getBytes());
		byte[] jwtSignatureBytes = jwtSignature.sign();
		final String jwtSignatureStr = Base64.encodeBase64URLSafeString(jwtSignatureBytes);
		final String jwt = jwtHeaderStr + "." + jwtPayloadStr + "." + jwtSignatureStr;
		System.out.println("jwt:" + jwt);
		return jwt;
	}

	// 署名検証
	public static void verifyJWT(String jwt, ECPublicKey publicKey) throws NoSuchAlgorithmException,
			InvalidKeyException, SignatureException {
		final String[] splitJwt = jwt.split("\\.");
		final String jwtHeaderStr = splitJwt[0];
		final String jwtPayloadStr = splitJwt[1];
		final String jwtSignatureStr = splitJwt[2];
		final Signature jwtSignature = Signature.getInstance("SHA256withECDSAinP1363Format");
		jwtSignature.initVerify(publicKey);
		jwtSignature.update((jwtHeaderStr + "." + jwtPayloadStr).getBytes());

		if( jwtSignature.verify(Base64.decodeBase64(jwtSignatureStr))) {
			System.out.println("Verifying Signature Success");
		}else {
			System.out.println("Verifying Signature Failure");
		}
	}
}
```

### 動作確認

```
ES256 Public Key:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaGzLv1quqEfO2YfyeMKJ5y72hVYP+NgH2tSqyKtW3MoxZShcY/p3aSw9af+N4bDwutbrTtLmEn4I6GbV71/bcQ==
ES256 Private Key:MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCBS+5M9cXf4BPwM8g8JTR4Dma26Etno8lAXg7EgsWht4A==
jwt:eyJraWQiOiJBQkMxMjNERUZHIiwiYWxnIjoiRVMyNTYifQ.eyJpc3MiOiJERUYxMjNHSElKIiwiaWF0IjoxNDM3MTc5MDM2LCJleHAiOjE0OTMyOTgxMDAsImF1ZCI6Imh0dHBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJjb20ubXl0ZXN0LmFwcCJ9.WxE4w1JSf1dxcHJiDaFdRHstMveiwfqn0ZcwW_7PEJji0kRuJWCczt_AM3-RW6lgp8CDREfTnQTMRegnHohRKg
Verifying Signature Success
```

※生成されたjwtの中身を[jwt.io](https://jwt.io/)で確認した結果

* Header部

  ```json
  {
    "kid": "ABC123DEFG",
    "alg": "ES256"
  }
  ```

  

* Payload部

  ```json
  {
    "iss": "DEF123GHIJ",
    "iat": 1437179036,
    "exp": 1493298100,
    "aud": "https://appleid.apple.com",
    "sub": "com.mytest.app"
  }
  ```

  

## 参考情報

* [JWT（JSON Web Token）の紹介](https://meetup-jp.toast.com/3511)
* [What the Heck is Sign In with Apple?](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple)
* [Generate and Validate Tokens](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)