メインコンテンツまでスキップ

JOSE (JWT/JWS/JWE)

このドキュメントの目的

JWT/JWS/JWEの生成・検証処理を理解することが目標です。

所要時間

⏱️ 約30分

前提知識

  • OAuth 2.0/OIDC基礎知識
  • JWT(JSON Web Token)基礎知識

JOSEとは

JOSE (JSON Object Signing and Encryption)

OAuth/OIDCで使用される暗号化・署名技術の総称:

技術正式名称用途RFC
JWTJSON Web Tokenクレーム(主張)の表現RFC 7519
JWSJSON Web SignatureJWTへの署名RFC 7515
JWEJSON Web EncryptionJWTの暗号化RFC 7516
JWKJSON Web Key鍵の表現RFC 7517

idp-serverでの使用例:

  • Access Token: JWT/JWS(署名付きトークン)
  • ID Token: JWT/JWS(署名付きトークン)
  • Refresh Token: JWT/JWS
  • Request Object: JWT/JWS(クライアントが送信するリクエストパラメータ)

使用ライブラリ

idp-serverは具体的なJOSE処理を外部ライブラリに委譲:

ライブラリバージョン用途公式サイト
Nimbus JOSE + JWT9.x+JWT/JWS/JWE生成・検証・解析connect2id.com/products/nimbus-jose-jwt

依存関係:

implementation 'com.nimbusds:nimbus-jose-jwt:9.x'

Nimbus JOSE + JWTの役割:

  • ✅ JWT/JWS/JWE の解析(parse)
  • ✅ 署名生成・検証(sign/verify)
  • ✅ 暗号化・復号(encrypt/decrypt)
  • ✅ JWKS解析
  • ✅ 鍵生成(RSA/ECDSA等)

idp-serverのJoseHandlerの役割:

  • ✅ Nimbus JOSE + JWTのラッパー
  • ✅ 型判定(JWT/JWS/JWE)の自動化
  • ✅ idp-server固有の例外処理(JoseInvalidException
  • ✅ 鍵選択ロジック(kid検索、アルゴリズム判定)

なぜラッパーが必要か:

  • Nimbus JOSE + JWTは汎用ライブラリ(低レベルAPI)
  • idp-serverの用途に特化した高レベルAPIを提供
  • 例外処理の統一(JOSEExceptionJoseInvalidException

JoseHandlerアーキテクチャ

30秒で理解する全体像

JOSE文字列(JWT/JWS/JWE)

JoseHandler.handle()

JoseType判定(plain/signature/encryption)

┌──────────────────────────────────┐
│ JoseContextCreatorを選択(Plugin)│
├──────────────────────────────────┤
│ plain → JwtContextCreator │
│ signature → JwsContextCreator │
│ encryption → JweContextCreator │
└──────────────────────────────────┘

JoseContext
├─ JsonWebSignature(署名情報)
├─ JsonWebTokenClaims(クレーム)
├─ JsonWebSignatureVerifier(検証器)
└─ JsonWebKey(鍵情報)

実装: JoseHandler.java


JoseHandler実装

クラス構造

実装場所: libs/idp-server-platform/src/main/java/org/idp/server/platform/jose/

public class JoseHandler {

Map<JoseType, JoseContextCreator> creators;

public JoseHandler() {
creators = new HashMap<>();
creators.put(JoseType.plain, new JwtContextCreator()); // 署名なしJWT
creators.put(JoseType.signature, new JwsContextCreator()); // 署名付きJWT
creators.put(JoseType.encryption, new JweContextCreator()); // 暗号化JWT
}

public JoseContext handle(String jose, String publicJwks, String privateJwks, String secret)
throws JoseInvalidException {

// 1. JoseType判定(JWT/JWS/JWE)
JoseType joseType = JoseType.parse(jose);

// 2. Creator選択(Pluginパターン)
JoseContextCreator joseContextCreator = creators.get(joseType);

// 3. JoseContext生成
return joseContextCreator.create(jose, publicJwks, privateJwks, secret);
}
}

ポイント:

  • ✅ Pluginパターン(Map<JoseType, JoseContextCreator>
  • ✅ 3種類のCreator(JWT/JWS/JWE)
  • ✅ 鍵情報を引数で受け取る(publicJwks, privateJwks, secret)

JoseType判定

3種類のJOSE形式

実装: JoseType.java

public enum JoseType {
plain, // JWT(署名なし) - 非推奨(テスト用途のみ)
signature, // JWS(署名付きJWT) - 推奨
encryption; // JWE(暗号化JWT) - 高セキュリティ

public static JoseType parse(String jose) {
String[] parts = jose.split("\\.");

if (parts.length == 3) {
return signature; // JWS形式: header.payload.signature
} else if (parts.length == 5) {
return encryption; // JWE形式: header.encrypted_key.iv.ciphertext.tag
}

return plain; // その他はplain JWT
}
}

判定ロジック:

  • 3パート: JWS(署名付き) - header.payload.signature
  • 5パート: JWE(暗号化) - header.encrypted_key.iv.ciphertext.tag
  • その他: Plain JWT(署名なし、非推奨)

JoseContext

役割

JOSEの解析結果を保持するコンテナ。

実装: JoseContext.java

public class JoseContext {

JsonWebSignature jsonWebSignature; // 署名情報
JsonWebTokenClaims claims; // クレーム(ペイロード)
JsonWebSignatureVerifier jwsVerifier; // 署名検証器
JsonWebKey jsonWebKey; // 使用された鍵

// クレーム取得
public JsonWebTokenClaims claims() {
return claims;
}

public Map<String, Object> claimsAsMap() {
return claims.toMap();
}

// 署名検証
public void verifySignature() throws JoseInvalidException {
if (hasJsonWebSignature()) {
jwsVerifier.verify(jsonWebSignature);
}
}

// 署名の有無確認
public boolean hasJsonWebSignature() {
return jsonWebSignature.exists();
}

// 対称鍵アルゴリズム判定
public boolean isSymmetricKey() {
return jsonWebSignature.isSymmetricType(); // HS256/HS384/HS512
}
}

使用例:

// JOSE文字列を解析
JoseHandler handler = new JoseHandler();
JoseContext context = handler.handle(
jwtString,
publicJwks, // 公開鍵JWKS(RS256等)
privateJwks, // 秘密鍵JWKS(復号用)
secret); // 共有鍵(HS256等)

// 署名検証
context.verifySignature();

// クレーム取得
Map<String, Object> claims = context.claimsAsMap();
String sub = (String) claims.get("sub");

JWS(署名付きJWT)処理

JwsContextCreator

実装: JwsContextCreator.java

public class JwsContextCreator implements JoseContextCreator {

@Override
public JoseContext create(String jose, String publicJwks, String privateJwks, String secret)
throws JoseInvalidException {

// 1. JWS解析
JsonWebSignature jsonWebSignature = JsonWebSignature.parse(jose);

// 2. Header解析
JsonWebSignatureHeader header = jsonWebSignature.header();
JsonWebSignatureAlgorithm algorithm = header.algorithm();
String keyId = header.keyId();

// 3. 鍵選択(アルゴリズムに応じて)
JsonWebKey jsonWebKey;
if (algorithm.isSymmetric()) {
// 対称鍵(HS256/HS384/HS512)
jsonWebKey = JsonWebKey.parseFromSecret(secret);
} else {
// 非対称鍵(RS256/ES256等)
JsonWebKeys jsonWebKeys = JsonWebKeys.parse(publicJwks);
jsonWebKey = jsonWebKeys.get(keyId); // kid で鍵を選択
}

// 4. Verifier作成
JsonWebSignatureVerifier verifier =
JsonWebSignatureVerifierFactory.create(jsonWebKey, algorithm);

// 5. クレーム取得
JsonWebTokenClaims claims = jsonWebSignature.claims();

// 6. JoseContext生成
return new JoseContext(jsonWebSignature, claims, verifier, jsonWebKey);
}
}

処理フロー:

JWS文字列

1. 解析(header.payload.signature に分割)
2. Header解析(alg, kid取得)
3. 鍵選択
- HS256等 → secret使用
- RS256等 → publicJwks から kid で検索
4. Verifier作成
5. クレーム取得

JoseContext

署名アルゴリズム

サポートされるアルゴリズム

実装: JsonWebSignatureAlgorithm.java

対称鍵アルゴリズム(共有鍵)

アルゴリズム説明鍵長用途
HS256HMAC SHA-256256bitClient Secret JWT
HS384HMAC SHA-384384bit-
HS512HMAC SHA-512512bit-

特徴:

  • 同じ鍵で署名・検証
  • 速い
  • 鍵共有が必要(セキュリティリスク)

非対称鍵アルゴリズム(公開鍵暗号)

アルゴリズム説明鍵長用途
RS256RSA SHA-2562048bit+ID Token署名(デフォルト)
RS384RSA SHA-3842048bit+-
RS512RSA SHA-5122048bit+-
ES256ECDSA P-256 SHA-256256bit高速・高セキュリティ
ES384ECDSA P-384 SHA-384384bit-
ES512ECDSA P-521 SHA-512521bit最高セキュリティ
PS256RSA-PSS SHA-2562048bit+FAPI推奨
PS384RSA-PSS SHA-3842048bit+-
PS512RSA-PSS SHA-5122048bit+-

特徴:

  • 秘密鍵で署名、公開鍵で検証
  • 鍵共有不要(公開鍵は配布可能)
  • OIDC標準(RS256がデフォルト)

JsonWebSignature

署名付きJWTの処理

実装: JsonWebSignature.java

ライブラリ委譲: Nimbus JOSE + JWTのSignedJWTクラスをラップ

public class JsonWebSignature {

SignedJWT value; // Nimbus JOSE + JWTのSignedJWT(実際の処理はこのライブラリが実行)

// JWS文字列を解析(Nimbus JOSE + JWTに委譲)
public static JsonWebSignature parse(String jose) throws JoseInvalidException {
try {
SignedJWT signedJWT = SignedJWT.parse(jose); // ← Nimbus JOSE + JWTライブラリの処理
return new JsonWebSignature(signedJWT);
} catch (ParseException e) {
throw new JoseInvalidException(e.getMessage(), e);
}
}

// クレーム取得
public JsonWebTokenClaims claims() {
JWTClaimsSet jwtClaimsSet = value.getJWTClaimsSet();
return new JsonWebTokenClaims(jwtClaimsSet);
}

// 署名検証(Nimbus JOSE + JWTに委譲)
boolean verify(JWSVerifier verifier) throws JoseInvalidException {
try {
return value.verify(verifier); // ← Nimbus JOSE + JWTライブラリの処理
} catch (JOSEException e) {
throw new JoseInvalidException(e.getMessage(), e);
}
}

// 対称鍵アルゴリズム判定
public boolean isSymmetricType() {
JWSAlgorithm algorithm = value.getHeader().getAlgorithm();
return algorithm.equals(JWSAlgorithm.HS256)
|| algorithm.equals(JWSAlgorithm.HS384)
|| algorithm.equals(JWSAlgorithm.HS512);
}

// Key ID取得
public String keyId() {
return value.getHeader().getKeyID();
}

// アルゴリズム取得
public String algorithm() {
return value.getHeader().getAlgorithm().getName();
}
}

JWS形式:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0xIn0.
eyJzdWIiOiJ1c2VyLTEyMzQ1IiwiaWF0IjoxNjk1NTUyMDAwLCJleHAiOjE2OTU1NTU2MDB9.
signature_base64url

↓ 分解

Header (Base64URL):
{
"alg": "RS256", ← 署名アルゴリズム
"typ": "JWT",
"kid": "key-1" ← Key ID(JWKSから鍵を検索)
}

Payload (Base64URL):
{
"sub": "user-12345",
"iat": 1695552000,
"exp": 1695555600
}

Signature (Base64URL):
RS256(Header + "." + Payload, privateKey)

JsonWebTokenClaims

クレーム(ペイロード)の処理

実装: JsonWebTokenClaims.java

ライブラリ委譲: Nimbus JOSE + JWTのJWTClaimsSetクラスをラップ

public class JsonWebTokenClaims {

JWTClaimsSet value; // Nimbus JOSE + JWTのJWTClaimsSet(実際の処理はこのライブラリが実行)

// 標準クレーム取得
public String subject() {
return value.getSubject();
}

public String issuer() {
return value.getIssuer();
}

public List<String> audience() {
return value.getAudience();
}

public Date expirationTime() {
return value.getExpirationTime();
}

public Date issuedAt() {
return value.getIssueTime();
}

// カスタムクレーム取得
public Object getClaim(String key) {
return value.getClaim(key);
}

// すべてのクレームをMapで取得
public Map<String, Object> toMap() {
return value.getClaims();
}

// クレームの存在確認
public boolean exists() {
return Objects.nonNull(value) && !value.getClaims().isEmpty();
}
}

標準クレーム(RFC 7519):

クレーム説明必須
subSubject(ユーザーID)"user-12345"
issIssuer(発行者)"https://idp.example.com"
audAudience(対象)["client-app-123"]
expExpiration Time(有効期限)1695555600 (Unix時刻)
iatIssued At(発行時刻)推奨1695552000
nbfNot Before(有効開始時刻)-1695552000
jtiJWT ID(一意識別子)-"jwt-uuid-abc"

カスタムクレーム例(ID Token):

{
"sub": "user-12345",
"iss": "https://idp.example.com",
"aud": ["client-app"],
"exp": 1695555600,
"iat": 1695552000,
"nonce": "random-nonce-xyz", ← カスタム
"at_hash": "abc123...", ← カスタム(Access Tokenハッシュ)
"c_hash": "def456..." ← カスタム(Codeハッシュ)
}

署名検証フロー

JwsContextCreatorの詳細処理

JWS文字列: "eyJhbGc...eyJzdWI...signature"

1. JsonWebSignature.parse()
└─ SignedJWT解析(Nimbus JOSE)

2. Header解析
├─ alg: "RS256"
└─ kid: "key-1"

3. 鍵選択
├─ algorithm.isSymmetric()?
│ YES → JsonWebKey.parseFromSecret(secret)
│ NO → JsonWebKeys.parse(publicJwks).get(kid)
└─ JsonWebKey取得

4. Verifier作成
└─ JsonWebSignatureVerifierFactory.create(key, algorithm)
├─ RS256 → RSASSAVerifier
├─ ES256 → ECDSAVerifier
└─ HS256 → MACVerifier

5. クレーム取得
└─ jsonWebSignature.claims()

JoseContext {
jsonWebSignature,
claims,
verifier,
jsonWebKey
}

6. 署名検証実行(使用側で)
context.verifySignature()

使用例

ID Token検証

// ID Token検証
public void validateIdToken(String idTokenString, String publicJwks)
throws JoseInvalidException {

// 1. JoseHandler でID Token解析
JoseHandler joseHandler = new JoseHandler();
JoseContext context = joseHandler.handle(
idTokenString,
publicJwks, // 公開鍵JWKS
null, // 秘密鍵不要(検証のみ)
null); // 共有鍵不要(RS256使用)

// 2. 署名検証
context.verifySignature(); // 署名が不正ならJoseInvalidException

// 3. クレーム検証
Map<String, Object> claims = context.claimsAsMap();

// iss検証
String iss = (String) claims.get("iss");
if (!iss.equals("https://idp.example.com")) {
throw new JoseInvalidException("Invalid issuer");
}

// aud検証
List<String> aud = (List<String>) claims.get("aud");
if (!aud.contains(expectedClientId)) {
throw new JoseInvalidException("Invalid audience");
}

// exp検証
long exp = (Long) claims.get("exp");
if (System.currentTimeMillis() / 1000 > exp) {
throw new JoseInvalidException("Token expired");
}

// nonce検証(OIDC)
String nonce = (String) claims.get("nonce");
if (!nonce.equals(expectedNonce)) {
throw new JoseInvalidException("Invalid nonce");
}
}

Access Token生成

// Access Token生成(JWS署名付き)
public String createAccessToken(
String sub,
List<String> scopes,
JsonWebKey privateKey) throws JoseInvalidException {

// 1. クレーム作成
Map<String, Object> claims = new HashMap<>();
claims.put("sub", sub);
claims.put("iss", "https://idp.example.com");
claims.put("aud", Arrays.asList("api-server"));
claims.put("scope", String.join(" ", scopes));
claims.put("iat", System.currentTimeMillis() / 1000);
claims.put("exp", System.currentTimeMillis() / 1000 + 3600); // 1時間

// 2. JWS生成
JsonWebSignatureFactory factory = new JsonWebSignatureFactory();
JsonWebSignature jws = factory.create(
claims,
privateKey,
JsonWebSignatureAlgorithm.RS256);

// 3. JWS文字列化
return jws.serialize();
}

JWK(JSON Web Key)

公開鍵の表現形式

実装: JsonWebKey.java

ライブラリ委譲: Nimbus JOSE + JWTのJWK/JWKSetクラスをラップ

public class JsonWebKey {

JWK value; // Nimbus JOSE + JWTのJWK(実際の処理はこのライブラリが実行)

// JWKS文字列から解析
public static JsonWebKeys parse(String jwks) {
JWKSet jwkSet = JWKSet.parse(jwks);
return new JsonWebKeys(jwkSet);
}

// Key ID取得
public String keyId() {
return value.getKeyID();
}

// アルゴリズム取得
public String algorithm() {
return value.getAlgorithm().getName();
}

// 鍵タイプ判定
public boolean isRSA() {
return value instanceof RSAKey;
}

public boolean isEC() {
return value instanceof ECKey;
}

public boolean isOctetSequence() {
return value instanceof OctetSequenceKey;
}
}

JWKS形式:

{
"keys": [
{
"kty": "RSA",
"kid": "key-1",
"use": "sig",
"alg": "RS256",
"n": "modulus_base64url",
"e": "exponent_base64url"
},
{
"kty": "EC",
"kid": "key-2",
"use": "sig",
"alg": "ES256",
"crv": "P-256",
"x": "x_coordinate_base64url",
"y": "y_coordinate_base64url"
}
]
}

よくある使用パターン

パターン1: ID Token検証(外部IdP)

// フェデレーションでの外部IdP(Google)のID Token検証
JoseHandler handler = new JoseHandler();
JoseContext context = handler.handle(
googleIdToken,
googlePublicJwks, // https://www.googleapis.com/oauth2/v3/certs
null,
null);

// 署名検証
context.verifySignature();

// nonce検証
Map<String, Object> claims = context.claimsAsMap();
String nonce = (String) claims.get("nonce");
if (!nonce.equals(ssoSession.nonce())) {
throw new JoseInvalidException("Nonce mismatch");
}

パターン2: Request Object検証(OIDC)

// クライアントが送信したRequest Object(JWT)の検証
JoseHandler handler = new JoseHandler();
JoseContext context = handler.handle(
requestObject,
clientPublicJwks, // クライアントの公開鍵JWKS
null,
null);

// 署名検証
context.verifySignature();

// クレーム取得(リクエストパラメータ)
Map<String, Object> claims = context.claimsAsMap();
String clientId = (String) claims.get("client_id");
String redirectUri = (String) claims.get("redirect_uri");
String scope = (String) claims.get("scope");

パターン3: client_secret_jwt検証

// クライアント認証(client_secret_jwt)
JoseHandler handler = new JoseHandler();
JoseContext context = handler.handle(
clientAssertion,
null,
null,
clientSecret); // Client Secretを共有鍵として使用(HS256)

// 署名検証
context.verifySignature();

// クレーム検証
Map<String, Object> claims = context.claimsAsMap();
String sub = (String) claims.get("sub"); // client_id
String aud = (String) claims.get("aud"); // token endpoint

よくあるエラー

エラー1: JoseInvalidException - 署名検証失敗

原因:

  • 不正な署名
  • 鍵の不一致(kidが見つからない)
  • アルゴリズムの不一致

解決策:

// 1. kid確認
String kid = jsonWebSignature.keyId();

// 2. JWKS に kid が存在するか確認
JsonWebKeys jwks = JsonWebKeys.parse(publicJwks);
if (!jwks.hasKey(kid)) {
throw new JsonWebKeyNotFoundException("Key not found: " + kid);
}

// 3. アルゴリズム確認
String alg = jsonWebSignature.algorithm(); // "RS256"

エラー2: ParseException - JWT解析失敗

原因:

  • JWT形式が不正
  • Base64URLデコード失敗

解決策:

try {
JsonWebSignature jws = JsonWebSignature.parse(jwtString);
} catch (JoseInvalidException e) {
// JWT形式が不正
log.error("Invalid JWT format: {}", e.getMessage());
}

エラー3: JsonWebKeyNotFoundException - 鍵が見つからない

原因:

  • kidがJWKSに存在しない
  • JWKS取得失敗

解決策:

// JWKS更新(外部IdPのJWKSを再取得)
String jwks = httpClient.get("https://idp.example.com/.well-known/jwks.json");
JsonWebKeys jsonWebKeys = JsonWebKeys.parse(jwks);

次のステップ

✅ JOSE(JWT/JWS/JWE)処理の実装を理解した!

📖 関連ドキュメント

🔗 詳細情報


情報源:

最終更新: 2025-10-13