Javaを用いたJWT生成ロジック実装

# 概要
認証の勉強がてらJavaを用いてJWTの生成ロジックを組んでみたので備忘録として残します。

# 各種バージョン等
| ライブラリ名                      | グループID / アーティファクトID                                   | 使用バージョン  |
| --------------------------- | ----------------------------------------------------- | -------- |
| **Spring Boot**             | `org.springframework.boot:spring-boot-starter-parent` | `3.4.5`  |
| **Java**                    | `java.version` (プロパティ)                                | `17`     |
| **JWT (JJWT)**              | `io.jsonwebtoken:jjwt`                                | `0.12.6` |
| **BouncyCastle (JCAプロバイダ)** | `org.bouncycastle:bcprov-jdk18on`                     | `1.78`   |

# 秘密鍵の生成
JWTの署名部分を生成する際に用いる秘密鍵を生成します。
適当なディレクトリ上で以下コマンドを実行し、RSA256アルゴリズムを用いてPEM形式の秘密鍵を生成します。
```terminal
openssl genrsa 2048 > RS256_private.pem
```

`RS256_private.pem`が生成されます。中身を確認すると以下のようなPEM形式の秘密鍵が確認できます。
```text
-----BEGIN RSA PRIVATE KEY-----
###########################
-----END PUBLIC KEY-----
```

# 公開鍵（検証用）の抽出
同ディレクトリ上で以下コマンドを実行し、先ほど生成した秘密鍵から公開鍵を抽出します。
```terminal
openssl rsa -in RS256_private.pem -pubout > RS256_public.pem
```

`RS256_public.pem`が生成されます。中身を確認すると以下のようなPEM形式の公開鍵が確認できます。
```text
-----BEGIN PUBLIC KEY-----
###########################
-----END PUBLIC KEY-----

```



# 秘密鍵を環境変数にセット
秘密鍵は秘匿情報のため、ハードコーディングは避けるようにしましょう。今回は環境変数に格納したものをJavaのコードから取得します。
```text
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
###########################
-----END RSA PRIVATE KEY-----"
```

# Javaコードから秘密鍵を取得
環境変数から秘密鍵を取得し、`PrivateKey`オブジェクトを生成して返す`PrivateKeyGetter`を実装します。
```java

@Component
public class PrivateKeyGetter {
    public PrivateKey getPrivateKey()
    {
        final String pem = System.getenv("JWT_PRIVATE_KEY");

        if (pem == null || pem.isEmpty()) {
            throw new IllegalArgumentException("JWT_PRIVATE_KEYの値が存在しません。");
        }

        if (!pem.startsWith("-----BEGIN RSA PRIVATE KEY-----") || !pem.endsWith("-----END RSA PRIVATE KEY-----")) {
            throw new IllegalArgumentException("JWT_PRIVATE_KEYの値が不正です。");
        }

        try {       
            // PEMデータを読み込む
            PemReader pemReader = new PemReader(new StringReader(pem));
            PemObject pemObject = pemReader.readPemObject();
            pemReader.close();
            
            if (pemObject == null || !pemObject.getType().equals("RSA PRIVATE KEY")) {
                throw new IllegalArgumentException("PEMオブジェクトがRSA秘密鍵ではありません");
            }

            byte[] keyBytes = pemObject.getContent();

            RSAPrivateKey rsaPrivateKey = RSAPrivateKey.getInstance(keyBytes);

            RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(
                rsaPrivateKey.getModulus(),
                rsaPrivateKey.getPrivateExponent()
            );

            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePrivate(keySpec);
        }
        catch(IOException e) {
            throw new RuntimeException(e.getMessage());
        }
        catch(NoSuchAlgorithmException e) {
            throw new RuntimeException(e.getMessage());
        }
        catch(InvalidKeySpecException e) {
            throw new RuntimeException(e.getMessage());
        }
    }
}

```

# JWT生成ロジック
JWTの生成を担う`JwtGenerator`を実装します。**subject**、**profile**、そして**発行日**・**有効期限**などの各種クレーム情報をセットします。そして、先ほど実装した`PrivateKeyGetter`を用いてJWTにRSA署名を付与します。これにより、JWTの内容改竄を防ぎます。

```java

@Component
public class JwtGenerator {
    private final PrivateKeyGetter privateKeyGetter;

    public InternalAuthIdTokenGenerator(PrivateKeyGetter privateKeyGetter) {
        this.privateKeyGetter = privateKeyGetter;
    }

    public String generate(
        final String subject,
        final String email,
        final Map<String, String> profile
    ) {
        Instant now = Instant.now();

        PrivateKey privateKey = privateKeyGetter.getPrivateKey();

        return Jwts.builder()
            .subject(subject)
            .claim("profile", profile)
            .issuedAt(Date.from(now))
            .expiration(Date.from(now.plus(7, ChronoUnit.DAYS)))
            .signWith(privateKey, Jwts.SIG.RS256)
            .compact();
    }
}

```

# JWT用エンドポイントの実装
Spring MVCを用いて、JWTをレスポンスとして返却するコントローラーを実装します。
```java
@RestController
@RequestMapping(path = "/api")
public class GetJwtController {
    private final JwtGenerator jwtGenerator;

    public GetJwtController(JwtGenerator jwtGenerator) {
        this.jwtGenerator = jwtGenerator;
    }

    @GetMapping("/auth/token")
    public ResponseEntity<?> getJwt(@RequestBody PasswordAuthRequest request) {
        Map<String, String> profile = new HashMap<>();
        profile.put("email", "tanaka@example.com");
        profile.put("name", "田中太郎");
        profile.put("image_url", "https://example.com/hoge.png");
        String jwt = jwtGenerator.generate("68b26f50-e046-31dd-5cc4-2109f7c64e74", profile);
        return ResponseEntity.ok(jwt);
    }
}
```

# 実行結果
Postman等のAPIクライアントを用いて`/api/auth/token`にリクエストを投げます。JWTトークンが返却されることを確認できます。

```
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI2OGIyNmY1MC1lMDQ2LTMxZGQtNWNjNC0yMTA5ZjdjNjRlNzQiLCJwcm9maWxlIjp7ImltYWdlX3VybCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vaG9nZS5wbmciLCJuYW1lIjoi55Sw5Lit5aSq6YOOIiwiZW1haWwiOiJ0YW5ha2FAZXhhbXBsZS5jb20ifSwiaWF0IjoxNzQ4NzAxODI1LCJleHAiOjE3NDkzMDY2MjV9.aIngltR0cq2feolJwiVeuFu5OXcFl26wr-JlojFY5ziXoEF6ka-ZedTbWpNM7vs0vZB8wa07-4E6ELfpsjKZyyum9veBnEvplU12KsHX7mexIo7Rknn5XTdVnAxUGHyziFgsUWrPb7TOXIH2rSkAI_rvSt7HlVkSn-bk-anuQg3z39NW9LKIu4_xdX_cU7q2iY0fzmISf-2tzGUwZtYKS02yhV4-FH8ol2t_zMbZ46pAOk7Z3swl1LJ4AyM7iVylqJNmaI_IfC2MbAGsLHDxkveTibrIN7HIOTEqvgi7R_ySjVkctZbri1Vp-O81SwZ01suPGRLOQP5Q5NGIcEbSLw
```

# JWTの内容確認
以下サイトにアクセスし、JWTを確認します。JWT欄に先ほどの実行結果を貼り付けると右側にヘッダーとペイロードの値が表示されます。ペイロードには、`JwtGenerator`に渡した値が含まれていることが確認できます。
また、パブリックキー欄に`RS256_public.pem`の中身を貼り付けることで署名検証に成功することも確認可能です。

https://logto.io/ja/jwt-decoder

![jwt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4084887/39d7955b-710e-4bcd-a59c-021fbcd0e5f7.png)