Spring Security OAuth2アプリケーションでのJWS + JWK

1. 概要

*このチュートリアルでは、JSON Web Signature(JWS)、およびSpring Security OAuth2で構成されたアプリケーションでJSON Web Key(JWK)仕様を使用して実装する方法について学習します*
https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Features-Matrix#frequently-asked-questions[SpringはすべてのSpring Security OAuthの移行に取り組んでいますが、 Spring Securityフレームワークの機能]、このガイドは、これらの仕様の基本概念を理解するための出発点として適切であり、フレームワークに実装する際に役立ちます。
最初に、基本概念を理解しようとします。 JWSやJWKとは何か、その目的、このOAuthソリューションを使用するようにリソースサーバーを簡単に構成する方法など。
次に、OAuth2 Bootがバックグラウンドで実行していることを分析し、JWKを使用するように承認サーバーを設定することにより、仕様を詳細に分析します。

2. JWSとJWKの全体像を理解する

link:/uploads/bael-1239-image-simple-1-1024x858-100x84.png%20100w []
開始する前に、いくつかの基本的な概念を正しく理解することが重要です。 link:/rest-api-spring-oauth2-angular[OAuth]およびlink:/spring-security-oauth-jwt[JWT]を確認することをお勧めしますこれらのトピックはこのチュートリアルの範囲外であるため、最初の記事をご覧ください。
https://tools.ietf.org/html/rfc7515[JWS]は、IETFによって作成された仕様であり、*データの整合性を検証するためのさまざまな暗号化メカニズム*、つまりhttps://tools.ietfのデータを記述しています。 org / html / rfc7519 [JSON Web Token(JWT)]。 これを行うために必要な情報を含むJSON構造を定義します。
効果的にセキュリティで保護されていると見なされるにはクレームを署名または暗号化する必要があるため、広く使用されているJWT仕様の重要な側面です。
最初のケースでは、JWTはJWSとして表されます。 一方、暗号化されている場合、JWTはJSON Web Encryption(JWE)構造にエンコードされます。
OAuthを使用する際の最も一般的なシナリオは、署名されたJWTのみを持つことです。 これは、通常、情報を「非表示」にする必要はなく、単にデータの整合性を検証するだけだからです。
もちろん、署名付きまたは暗号化されたJWTを処理するかどうかにかかわらず、公開キーを効率的に送信できるようにするための正式なガイドラインが必要です。
*これはhttps://tools.ietf.org/html/rfc7517[JWK]*の目的です。これは、IETFでも定義されている暗号化キーを表すJSON構造です。
link:/spring-security-openid-connect [多くの認証プロバイダーは「JWKセット」エンドポイントを提供します]、これも仕様で定義されています。 これにより、他のアプリケーションは公開鍵に関する情報を見つけてJWTを処理できます。
たとえば、リソースサーバーはJWTにある_kid_(Key Id)フィールドを使用して、JWKセット内の正しいキーを見つけます。

2.1. JWKを使用したソリューションの実装

一般に、OAuth 2.0などの標準セキュリティプロトコルを使用するなどして、アプリケーションが安全な方法でリソースを提供するようにする場合は、次の手順に従う必要があります。
  1. 承認サーバーにクライアントを登録します-独自に
    サービス、またはOkta、Facebook、Githubなどの有名なプロバイダー

  2. これらのクライアントは、認証からアクセストークンを要求します
    サーバー、構成した可能性のあるOAuth戦略のいずれか

  3. 次に、トークンを提示するリソースにアクセスしようとします(
    この場合、JWTとして)リソースサーバーへ

  4. *リソースサーバーは、トークンがまだ送信されていないことを確認する必要があります
    署名*をチェックすることにより操作され、また主張を検証する

  5. そして最後に、リソースサーバーがリソースを取得します。
    クライアントに正しい権限があることを確認してください

3. JWKとリソースサーバーの構成

後ほど、JWTと「JWKセット」エンドポイントにサービスを提供する独自の承認サーバーを設定する方法について説明します。
ただし、この時点では、既存の承認サーバーを指す最も単純な(おそらく最も一般的な)シナリオに焦点を当てます。
私たちがしなければならないことは、JWTの署名を検証するためにどの公開鍵を使用すべきかなど、サービスが受信するアクセストークンを検証する方法を示すことだけです。
https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/htmlsingle/#boot-features-security-oauth2-resource-server[Spring Security OAuthのAutoconfig]機能を使用して、アプリケーションプロパティのみを使用して、シンプルでクリーンな方法でこれを実現します。

3.1. メーベン依存

SpringアプリケーションのpomファイルにOAuth2自動構成の依存関係を追加する必要があります。
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.6.RELEASE</version>
</dependency>
いつものように、https://search.maven.org/search?q = a:spring-security-oauth2-autoconfigure [Maven Central]でアーティファクトの最新バージョンを確認できます。
この依存関係はSpring Bootによって管理されないため、バージョンを指定する必要があることに注意してください。
とにかく使用しているSpring Bootのバージョンと一致する必要があります。

3.2. リソースサーバーの構成

次に、_ @ EnableResourceServer_アノテーションを使用して、アプリケーションのリソースサーバー機能を有効にする必要があります。
@SpringBootApplication
@EnableResourceServer
public class ResourceServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ResourceServerApplication.class, args);
    }
}
次に、アプリケーションがBearerトークンとして受信するJWTの署名を検証するために必要な公開鍵を取得する方法を示す必要があります。
OAuth2ブートは、トークンを検証するためのさまざまな戦略を提供します。
前述したように、*ほとんどの承認サーバーは、他のサービスが署名の検証に使用できるキーのコレクションでURIを公開します。*
ローカル認証サーバーのJWK Setエンドポイントを構成し、さらに先に取り組みます。
_application.properties_に次を追加しましょう。
security.oauth2.resource.jwk.key-set-uri=
  http://localhost:8081/sso-auth-server/.well-known/jwks.json
この主題を詳細に分析しながら、他の戦略を見ていきます。
*注*:新しいSpring Security 5.1リソースサーバーは、承認としてJWK署名付きJWTのみをサポートします。また、Spring Bootは、JWK Setエンドポイントを構成するための非常に類似したプロパティを提供します。
spring.security.oauth2.resourceserver.jwk-set-uri=
  http://localhost:8081/sso-auth-server/.well-known/jwks.json

3.3. ボンネットの下のスプリング構成

前に追加したプロパティは、Spring Beanの作成時に変換されます。
より正確には、OAuth2ブートは以下を作成します。
  • JWTをデコードして検証する唯一の機能を持つ_JwkTokenStore_
    その署名

  • 以前の_TokenStore_を使用するa DefaultTokenServices instance

4. 承認サーバーのJWKセットエンドポイント

次に、JWTを発行してそのJWKセットエンドポイントを提供する承認サーバーを構成する際に、JWKおよびJWSのいくつかの重要な側面を分析して、この主題についてさらに詳しく説明します。
Spring Securityはまだ認証サーバーをセットアップする機能を提供していないため、この段階ではSpring Security OAuth機能を使用して認証サーバーを作成することしかできません。 ただし、Spring Security Resource Serverと互換性があります。

4.1. 認可サーバー機能の有効化

最初の手順は、必要に応じてアクセストークンを発行するように承認サーバーを構成することです。
Resource Serverで行ったように、__spring-security-oauth2-autoconfigure __dependencyも追加します。
最初に、__ @ EnableAuthorizationServer __annotationを使用して、OAuth2 Authorization Serverメカニズムを構成します。
@Configuration
@EnableAuthorizationServer
public class JwkAuthorizationServerConfiguration {

    // ...

}
そして、プロパティを使用してOAuth 2.0クライアントを登録します。
security.oauth2.client.client-id=bael-client
security.oauth2.client.client-secret=bael-secret
これにより、アプリケーションは、対応する資格情報で要求されたときにランダムトークンを取得します。
curl bael-client:bael-secret\
  @localhost:8081/sso-auth-server/oauth/token \
  -d grant_type=client_credentials \
  -d scope=any
ご覧のとおり、Spring Security OAuth * JWTエンコードではなく、デフォルトでランダムな文字列値を取得します:*
"access_token": "af611028-643f-4477-9319-b5aa8dc9408f"

4.2. JWTの発行

コンテキストで_JwtAccessTokenConverter_ Beanを作成することで、これを簡単に変更できます。
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    return new JwtAccessTokenConverter();
}
_JwtTokenStore_インスタンスで使用する:
@Bean
public TokenStore tokenStore() {
    return new JwtTokenStore(accessTokenConverter());
}
そのため、これらの変更を加えて、新しいアクセストークンをリクエストしましょう。今回は、正確にするために、JWSとしてエンコードされたJWTを取得します。
JWSを簡単に識別できます。それらの構造は、ドットで区切られた3つのフィールド(ヘッダー、ペイロード、および署名)で構成されています。
"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  .
  eyJzY29wZSI6WyJhbnkiXSwiZXhwIjoxNTYxOTcy...
  .
  XKH70VUHeafHLaUPVXZI9E9pbFxrJ35PqBvrymxtvGI"
*デフォルトでは、Springはメッセージ認証コード(MAC)アプローチを使用してヘッダーとペイロードに署名します。*
これを確認するには、https://kjur.github.io/jsrsasign/tool/tool_jwtveri.html [JWTデコーダー/検証ツールオンラインツール]のいずれかでJWTを分析します。
取得したJWTをデコードすると、_alg_属性の値が_HS256_であることがわかります。これは、_HMAC-SHA256_アルゴリズムがトークンへの署名に使用されたことを示します。
このアプローチでJWKを必要としない理由を理解するには、MACハッシュ関数の仕組みを理解する必要があります。

4.3. デフォルトの対称署名

  • MACハッシュでは、同じキーを使用してメッセージに署名し、その整合性を検証します。それは対称ハッシュ関数です。*

    したがって、セキュリティ上の理由から、アプリケーションは署名キーを公開して共有することはできません。
    学術的な理由のために、Spring Security OAuth _ / oauth / token_key_エンドポイントを公開します。
security.oauth2.authorization.token-key-access=permitAll()
__JwtAccessTokenConverter __beanを構成するときに、署名キーの値をカスタマイズします。
converter.setSigningKey("bael");
使用されている対称キーを正確に知るため。
注:署名キーを公開しない場合でも、弱い署名キーを設定すると、辞書攻撃に対する潜在的な脅威になります。
署名キーがわかれば、前述のオンラインツールを使用してトークンの整合性を手動で確認できます。
Spring Security OAuthライブラリーは、デコードされたJWTを検証および取得する_ / oauth / check_token_エンドポイントも構成します。
このエンドポイントも_denyAll()_アクセスルールで構成されており、意識的に保護する必要があります。 この目的のために、以前にトークンキーに対して行ったように、__ security.oauth2.authorization.check-token-access __propertyを使用できます。

4.4. リソースサーバー構成の代替

セキュリティのニーズに応じて、最近言及されたエンドポイントの1つを適切に保護する(リソースサーバーにアクセスできるようにする)だけで十分であると考える場合があります。
その場合は、承認サーバーをそのままにして、リソースサーバーに別のアプローチを選択できます。
リソースサーバーは、承認サーバーにセキュリティで保護されたエンドポイントがあることを期待するため、最初に、承認サーバーで使用したものと同じプロパティを使用して、クライアント資格情報を提供する必要があります。
security.oauth2.client.client-id=bael-client
security.oauth2.client.client-secret=bael-secret
次に、_ / oauth / check_token_エンドポイントを使用することを選択できます(a.k.a. イントロスペクションエンドポイント)または_ / oauth / token_key_から単一のキーを取得します:
## Single key URI:
security.oauth2.resource.jwt.key-uri=
  http://localhost:8081/sso-auth-server/oauth/token_key
## Introspection endpoint:
security.oauth2.resource.token-info-uri=
  http://localhost:8081/sso-auth-server/oauth/check_token
または、リソースサービスでトークンを確認するために使用するキーを設定するだけです。
## Verifier Key
security.oauth2.resource.jwt.key-value=bael
このアプローチでは、承認サーバーとのやり取りはありませんが、もちろん、これはトークン署名構成の変更の柔軟性が低いことを意味します。
キーURI戦略と同様に、この最後のアプローチは、非対称署名アルゴリズムにのみ推奨される場合があります。

4.5. キーストアファイルの作成

最終目標を忘れないでください。 最も有名なプロバイダーが提供するように、JWK Setエンドポイントを提供したいと思います。
*キーを共有する場合は、非対称暗号化(特に、デジタル署名アルゴリズム)を使用してトークンに署名するとよいでしょう。*
これに向けた最初のステップは、キーストアファイルの作成です。
これを実現する簡単な方法の1つは次のとおりです。
  1. JDKまたはJREの_ / bin_ディレクトリでコマンドラインを開きます
    便利です:

cd $JAVA_HOME/bin
  1. 対応するパラメーターを使用して、_keytool_コマンドを実行します。

./keytool -genkeypair \
  -alias bael-oauth-jwt \
  -keyalg RSA \
  -keypass bael-pass \
  -keystore bael-jwt.jks \
  -storepass bael-pass
ここでは非対称のRSAアルゴリズムを使用していることに注意してください。
  1. インタラクティブな質問に答えて、キーストアファイルを生成します

4.6. キーストアファイルをアプリケーションに追加する

キーストアをプロジェクトリソースに追加する必要があります。
これは簡単な作業ですが、これはバイナリファイルであることに注意してください。 つまり、https://maven.apache.org/plugins/maven-resources-plugin/examples/filter.html [filtered]にできないか、破損することになります。
Mavenを使用している場合、1つの代替方法は、テキストファイルを別のフォルダーに入れ、それに応じて_pom.xml_を構成することです。
<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>false</filtering>
        </resource>
        <resource>
            <directory>src/main/resources/filtered</directory>
            <filtering>true</filtering>
        </resource>
    </resources>
</build>

4.7. _TokenStore_の構成

次のステップは、_TokenStore_をキーのペアで構成することです。トークンに署名するプライベート、および整合性を検証するパブリック。
クラスパスのキーストアファイルと、_。jks_ファイルの作成時に使用したパラメーターを使用して、__KeyPair ___instanceを作成します。
ClassPathResource ksFile =
  new ClassPathResource("bael-jwt.jks");
KeyStoreKeyFactory ksFactory =
  new KeyStoreKeyFactory(ksFile, "bael-pass".toCharArray());
KeyPair keyPair = ksFactory.getKeyPair("bael-oauth-jwt");
そして、それを_JwtAccessTokenConverter_ Beanで構成し、他の構成を削除します。
converter.setKeyPair(keyPair);
変更された_alg_パラメータを確認するために、JWTを再度要求してデコードできます。
トークンキーエンドポイントを見ると、キーストアから取得した公開キーが表示されます。
https://tools.ietf.org/html/rfc1421[PEM]「Encapsulation Boundary」ヘッダーで簡単に識別できます。 「_—– BEGIN PUBLIC KEY —–_“ _._」で始まる文字列

4.8. JWK Set Endpoint Dependencies

  • Spring Security OAuthライブラリは、すぐにJWKをサポートしません。*

    したがって、プロジェクトに別の依存関係_nimbus-jose-jwt_を追加する必要があります。これは、いくつかの基本的なJWK実装を提供します。
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>7.3</version>
</dependency>
https://search.maven.org/search?q=g:com.nimbusds%20AND%20a:nimbus-jose-jwt[Maven Central Repository Search Engine]を使用して、ライブラリの最新バージョンを確認できることを忘れないでください。

4.9. JWKセットエンドポイントの作成

前に構成した_KeyPair_インスタンスを使用して_JWKSet_ Beanを作成することから始めましょう。
@Bean
public JWKSet jwkSet() {
    RSAKey.Builder builder = new RSAKey.Builder((RSAPublicKey) keyPair().getPublic())
      .keyUse(KeyUse.SIGNATURE)
      .algorithm(JWSAlgorithm.RS256)
      .keyID("bael-key-id");
    return new JWKSet(builder.build());
}
エンドポイントの作成は非常に簡単です:
@RestController
public class JwkSetRestController {

    @Autowired
    private JWKSet jwkSet;

    @GetMapping("/.well-known/jwks.json")
    public Map<String, Object> keys() {
        return this.jwkSet.toJSONObject();
    }
}
__JWKSet __instanceで構成したキーIDフィールドは、_kid_パラメーターに変換されます。
*この_kid_はキーの任意のエイリアスです*。また、同じキーを* JWTヘッダーに含める必要があるため、通常リソースサーバーがコレクションから正しいエントリを選択するために使用します*。
現在、新しい問題に直面しています。 Spring Security OAuthはJWKをサポートしていないため、発行されたJWTには_kid_ Headerは含まれません。
これを解決する回避策を見つけましょう。

4.10。 _kid_値をJWTヘッダーに追加する

使用してきたa__JwtAccessTokenConverter ___を拡張する新しい_class_を作成します。これにより、ヘッダーエントリをJWTに追加できます。
public class JwtCustomHeadersAccessTokenConverter
  extends JwtAccessTokenConverter {

    // ...

}
まず、次のことを行う必要があります。
  • 私たちがやってきたように親クラスを設定し、
    _KeyPair_構成しました

  • キーストアから秘密鍵を使用する_Signer_オブジェクトを取得します

  • もちろん、追加するカスタムヘッダーのコレクション
    構造

    これに基づいてコンストラクターを構成しましょう:
private Map<String, String> customHeaders = new HashMap<>();
final RsaSigner signer;

public JwtCustomHeadersAccessTokenConverter(
  Map<String, String> customHeaders,
  KeyPair keyPair) {
    super();
    super.setKeyPair(keyPair);
    this.signer = new RsaSigner((RSAPrivateKey) keyPair.getPrivate());
    this.customHeaders = customHeaders;
}
ここで、__ encode __methodをオーバーライドします。 実装は親実装と同じですが、_String_トークンを作成するときにカスタムヘッダーも渡す点が異なります。
private JsonParser objectMapper = JsonParserFactory.create();

@Override
protected String encode(OAuth2AccessToken accessToken,
  OAuth2Authentication authentication) {
    String content;
    try {
        content = this.objectMapper
          .formatMap(getAccessTokenConverter()
          .convertAccessToken(accessToken, authentication));
    } catch (Exception ex) {
        throw new IllegalStateException(
          "Cannot convert access token to JSON", ex);
    }
    String token = JwtHelper.encode(
      content,
      this.signer,
      this.customHeaders).getEncoded();
    return token;
}
_JwtAccessTokenConverter_ Beanを作成するときに、このクラスを使用してみましょう。
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    Map<String, String> customHeaders =
      Collections.singletonMap("kid", "bael-key-id");
    return new  JwtCustomHeadersAccessTokenConverter(
      customHeaders,
      keyPair());
}
準備ができました。 リソースサーバーのプロパティを元に戻すことを忘れないでください。 チュートリアルの最初に設定した_key-set-uri_プロパティのみを使用する必要があります。
アクセストークンを要求し、_kid_ valueを確認し、それを使用してリソースを要求できます。
*公開鍵が取得されると、リソースサーバーはそれを内部に保存し、将来の要求のためにキーIDにマッピングします。*

5. 結論

この包括的なガイドでは、JWT、JWS、およびJWKについて多くのことを学びました。 Spring固有の構成だけでなく、一般的なセキュリティの概念も実際の例で実際に見てください。
JWK Setエンドポイントを使用してJWTを処理するリソースサーバーの基本的な構成を見てきました。
最後に、JWK Setエンドポイントを効率的に公開する承認サーバーを設定することにより、基本的なSpring Security OAuth機能を拡張しました。
いつものように、両方のサービスをhttps://github.com/Baeldung/spring-security-oauth/tree/master/oauth-jws-jwk[OAuth Github repo]で見つけることができます。