はじめに
本記事では、DPoP proof JWT (RFC 9449, Section 4. DPoP Proof JWTs) を生成する Java コードを紹介します。
DPoP proof JWT 仕様
まず、コーディングに関係のある DPoP proof JWT の仕様を、簡潔に確認していきます。
ヘッダ
| パラメータ | 要否 | 説明 |
|---|---|---|
typ |
必須 | 値は dpop+jwt で固定 |
alg |
必須 | 非対称鍵系署名アルゴリズム |
jwk |
必須 | 公開鍵 |
ペイロード
| クレーム | 要否 | 説明 |
|---|---|---|
jti |
必須 | 一意識別子 |
htm |
必須 | リクエストの HTTP メソッド |
htu |
必須 | リクエストのターゲット URI |
iat |
必須 | 生成時刻 |
ath |
条件 | アクセストークンのハッシュ値 |
nonce |
条件 | DPoP nonce |
補足
-
htu— 『オリジナルリクエストのターゲットURIの解決』参照 -
ath— DPoP proof JWT をアクセストークンと一緒に使う場合、必須。アクセストークンの SHA-256 ハッシュ値を base64url エンコードしたもの。 -
nonce— サーバがDPoP-Nonceヘッダで nonce 値を提供してきた場合、必須。詳細は『DPoP Nonce』参照。
DPoP proof JWT 生成
入力パラメータ
DPoP proof JWT 生成処理の入力パラメータを整理します。
| 入力データ | 要否 | 補足 |
|---|---|---|
| 一意識別子 | 任意 | 不指定なら自動生成 |
| HTTP メソッド | 必須 | |
| ターゲット URI | 必須 | |
| アクセストークン | 任意 | |
| nonce | 任意 | |
| 発行時刻 | 任意 | 不指定なら現在時刻 |
| 署名鍵 | 必須 | 秘密鍵 |
設計
DPoP proof JWT を生成する処理を DpopProofBuilder という名前のユーティリティクラスとして実装します。
このクラスを、次の手順で使えるようにします。
-
DpopProofBuilderのインスタンスを生成する - 入力パラメータ群を設定する
-
build()メソッドを呼ぶ
最後の build() メソッドコールは DPoP proof JWT を表す SignedJWT インスタンス (Nimbus JOSE + JWT ライブラリ) を生成します。
想定する使い方は下記のとおりです。
// 入力パラメータ
String jwtId = ...; // 一意識別子
String httpMethod = ...; // HTTP メソッド
URI targetUri = ...; // ターゲット URI
String accessToken = ...; // アクセストークン
String nonce = ...; // nonce
Date issueTime = ...; // 発行時刻
JWK key = ...; // 署名鍵
// DPoP proof JWT 生成
SignedJWT jwt =
new DpopProobBuilder()
.jwtId(jwtId) // 一意識別子
.httpMethod(httpMethod) // HTTP メソッド
.targetUri(targetUri) // ターゲット URI
.accessToken(accessToken) // アクセストークン
.nonce(nonce) // nonce
.issueTime(issueTime) // 発行時刻
.key(key) // 署名鍵
.build();
実装
それほど長いコードではないので、実装を丸ごと掲載します。
import java.net.URI;
import java.util.Date;
import java.util.UUID;
import com.nimbusds.jose.Algorithm;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.KeyType;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
public class DpopProofBuilder
{
// typ ヘッダパラメータの値
private static final JOSEObjectType TYPE_DPOP_JWT = new JOSEObjectType("dpop+jwt");
// 入力パラメータ群
private String jwtId;
private String httpMethod;
private URI targetUri;
private String accessTokenHash;
private String nonce;
private Date issueTime;
private JWK key;
public DpopProofBuilder jwtId(String jwtId)
{
this.jwtId = jwtId;
return this;
}
public DpopProofBuilder httpMethod(String httpMethod)
{
this.httpMethod = httpMethod;
return this;
}
public DpopProofBuilder targetUri(URI targetUri)
{
this.targetUri = targetUri;
return this;
}
public DpopProofBuilder accessToken(String accessToken)
{
// RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP)
// Section 4.2. DPoP Proof JWT Syntax
//
// ath: Hash of the access token. The value MUST be the result of
// a base64url encoding (as defined in Section 2 of [RFC7515])
// the SHA-256 [SHS] hash of the ASCII encoding of the
// associated access token's value.
//
// BASE64URL( SHA256( accessToken ) )
this.accessTokenHash =
CodingUtility.toBase64Url(
DigestUtility.sha256(accessToken));
return this;
}
public DpopProofBuilder accessTokenHash(String accessTokenHash)
{
this.accessTokenHash = accessTokenHash;
return this;
}
public DpopProofBuilder nonce(String nonce)
{
this.nonce = nonce;
return this;
}
public DpopProofBuilder issueTime(Date issueTime)
{
this.issueTime = issueTime;
return this;
}
public DpopProofBuilder key(JWK key)
{
this.key = key;
return this;
}
public SignedJWT build() throws IllegalStateException, JOSEException
{
// 署名アルゴリズムを決める
JWSAlgorithm algorithm = determineAlgorithm(key);
// ヘッダを用意する
JWSHeader header = buildHeader(key, algorithm);
// ペイロードに入れるクレーム群を用意する
JWTClaimsSet claims = buildClaims();
// 署名鍵と署名アルゴリズムから JWSSigner を用意する
JWSSigner signer = buildSigner(key, algorithm);
// 用意したヘッダとクレーム群を持つ JWT を作る (署名前)
SignedJWT jwt = new SignedJWT(header, claims);
// 署名する
jwt.sign(signer);
return jwt;
}
private static JWSAlgorithm determineAlgorithm(JWK jwk)
{
// JWK の alg パラメータ
Algorithm alg = jwk.getAlgorithm();
// JWK に alg パラメータが含まれていれば
if (alg != null)
{
// alg パラメータの値を JWS アルゴリズムとして解釈する
return JWSAlgorithm.parse(alg.getName());
}
// JWK の Key Type からアルゴリズムを決める
return determineAlgFamily(jwk.getKeyType()).getFirst();
}
private static JWSHeader buildHeader(JWK jwk, JWSAlgorithm alg)
{
// 署名鍵をチェックする
checkKey(jwk);
// alg パラメータの設定
JWSHeader.Builder builder = new JWSHeader.Builder(alg);
// typ パラメータの設定 (値は "dpop+jwt")
builder.type(TYPE_DPOP_JWT);
// jwk パラメータの設定
// 秘密鍵を公開鍵に変換している点に注目
builder.jwk(jwk.toPublicJWK());
return builder.build();
}
private static JWSAlgorithm.Family determineAlgFamily(KeyType keyType)
{
if (keyType == KeyType.EC)
{
return JWSAlgorithm.Family.EC;
}
else if (keyType == KeyType.RSA)
{
return JWSAlgorithm.Family.RSA;
}
else if (keyType == KeyType.OCT)
{
return JWSAlgorithm.Family.HMAC_SHA;
}
else if (keyType == KeyType.OKP)
{
return JWSAlgorithm.Family.ED;
}
// サポートしていない Key Type
throw new IllegalStateException("The key type '" + keyType + "' is not supported.");
}
private static void checkKey(JWK jwk)
{
// 署名鍵が設定されていない場合
if (jwk == null)
{
throw new IllegalStateException("No key is set.");
}
// 署名鍵が秘密鍵ではない場合
if (!jwk.isPrivate())
{
throw new IllegalStateException("The key is not a private key.");
}
// 署名鍵の Key Type が OCT の場合 (対称鍵系の場合)
if (jwk.getKeyType() == KeyType.OCT)
{
throw new IllegalStateException("The key is not an asymmetric key.");
}
}
private JWTClaimsSet buildClaims()
{
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
// RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP)
// Section 4.2. DPoP Proof JWT Syntax
// jti: Unique identifier for the DPoP proof JWT. The value MUST be
// assigned such that there is a negligible probability that
// the same value will be assigned to any other DPoP proof used
// in the same context during the time window of validity. Such
// uniqueness can be accomplished by encoding (base64url or any
// other suitable encoding) at least 96 bits of pseudorandom
// data or by using a version 4 Universally Unique Identifier
// (UUID) string according to [RFC4122]. The jti can be used by
// the server for replay detection and prevention; see Section 11.1.
builder.jwtID(buildJti());
// htm: The value of the HTTP method (Section 9.1 of [RFC9110]) of
// the request to which the JWT is attached.
builder.claim("htm", buildHtm());
// htu: The HTTP target URI (Section 7.1 of [RFC9110]) of the request
// to which the JWT is attached, without query and fragment parts.
builder.claim("htu", buildHtu());
// iat: Creation timestamp of the JWT (Section 4.1.6 of [RFC7519]).
builder.issueTime(buildIat());
// ath: Hash of the access token. The value MUST be the result of a
// base64url encoding (as defined in Section 2 of [RFC7515]) the
// SHA-256 [SHS] hash of the ASCII encoding of the associated
// access token's value.
if (accessTokenHash != null)
{
builder.claim("ath", accessTokenHash);
}
// nonce: A recent nonce provided via the DPoP-Nonce HTTP header.
if (nonce != null)
{
builder.claim("nonce", nonce);
}
return builder.build();
}
private String buildJti()
{
if (jwtId != null)
{
return jwtId;
}
return UUID.randomUUID().toString();
}
private String buildHtm()
{
if (httpMethod != null)
{
return httpMethod;
}
throw new IllegalStateException("No HTTP method is set.");
}
private String buildHtu()
{
if (targetUri != null)
{
return targetUri.toASCIIString();
}
throw new IllegalStateException("No target URI is set.");
}
private Date buildIat()
{
if (issueTime != null)
{
return issueTime;
}
return new Date();
}
private static JWSSigner buildSigner(JWK jwk, JWSAlgorithm alg)
{
try
{
// 署名鍵とアルゴリズムをもとに JWSSigner を生成する
return new DefaultJWSSignerFactory().createJWSSigner(jwk, alg);
}
catch (JOSEException cause)
{
throw new IllegalStateException(
"Failed to create a JWS signer for the key: " + cause.getMessage(), cause);
}
}
}
accessToken(String accessToken) メソッドの中では、渡されたアクセストークンの SHA-256 ハッシュを計算し、それを base64url でエンコードして、accessTokenHash フィールドに設定しています。
// BASE64URL( SHA256( accessToken ) )
this.accessTokenHash =
CodingUtility.toBase64Url(
DigestUtility.sha256(accessToken));
このコードに登場する SHA-256 ハッシュを計算する処理 (DigestUtility の sha256(String) メソッド) と base64url エンコードする処理 (CodingUtility の toBase64Url(byte[]) メソッド) は次のように実装することができます。
return MessageDigest.getInstance("SHA-256")
.digest(input.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder()
.withoutPadding().encodeToString(input);
おわりに
アクセストークンを送信者限定にすることで、アクセストークンの漏洩に対する耐性を高めることができます。DPoP は送信者限定アクセストークンを実現する方法の一つです。
Authlete 社のウェブサイトで公開している『標準仕様による徹底的な API 保護』という文書で DPoP を解説していますので、詳細にご興味があればご参照ください。また、2025 年 10 月 29 日に開催したオンライン勉強会『OAuth・OpenID 標準仕様による徹底的な API 保護』でも解説しております。併せてご視聴ください。
OAuth・OpenID 標準仕様による徹底的な API 保護 | 送信者限定 / DPoP