0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

本記事では、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 という名前のユーティリティクラスとして実装します。

このクラスを、次の手順で使えるようにします。

  1. DpopProofBuilder のインスタンスを生成する
  2. 入力パラメータ群を設定する
  3. build() メソッドを呼ぶ

最後の build() メソッドコールは DPoP proof JWT を表す SignedJWT インスタンス (Nimbus JOSE + JWT ライブラリ) を生成します。

想定する使い方は下記のとおりです。

DpopProofBuilderの使い方
// 入力パラメータ
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 ハッシュを計算する処理 (DigestUtilitysha256(String) メソッド) と base64url エンコードする処理 (CodingUtilitytoBase64Url(byte[]) メソッド) は次のように実装することができます。

SHA-256ハッシュを計算する処理
return MessageDigest.getInstance("SHA-256")
        .digest(input.getBytes(StandardCharsets.UTF_8));
base64urlエンコードする処理
return Base64.getUrlEncoder()
        .withoutPadding().encodeToString(input);

おわりに

アクセストークンを送信者限定にすることで、アクセストークンの漏洩に対する耐性を高めることができます。DPoP は送信者限定アクセストークンを実現する方法の一つです。

dpop_bound_access_token.png

Authlete 社のウェブサイトで公開している『標準仕様による徹底的な API 保護』という文書で DPoP を解説していますので、詳細にご興味があればご参照ください。また、2025 年 10 月 29 日に開催したオンライン勉強会『OAuth・OpenID 標準仕様による徹底的な API 保護』でも解説しております。併せてご視聴ください。

OAuth・OpenID 標準仕様による徹底的な API 保護 | 送信者限定 / DPoP
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?