1. 概要

このチュートリアルでは、JSON Web署名(JWS)と、SpringセキュリティOAuth2で構成されたアプリケーションでJSON Webキー(JWK)仕様を使用して実装する方法について学習します。

SpringはすべてのSpringSecurityOAuth機能をSpringSecurityフレームワークに移行するように取り組んでいますが、このガイドはこれらの仕様の基本概念を理解するための良い出発点であり、それらを任意のフレームワークに実装するときに役立ちます。

まず、基本的な概念を理解しようとします。 JWSやJWKとは何か、それらの目的、このOAuthソリューションを使用するようにリソースサーバーを簡単に構成する方法などです。

次に、さらに深く掘り下げて、OAuth2ブートがバックグラウンドで実行していることを分析し、JWKを使用するように承認サーバーを設定することによって仕様を詳細に分析します。

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

始める前に、いくつかの基本的な概念を正しく理解することが重要です。 OAuthJWTの記事はこのチュートリアルの範囲に含まれていないため、これらの記事を最初に確認することをお勧めします。

JWS は、IETFによって作成された仕様であり、は、データ、つまり JSON Web Token(JWT)のデータの整合性を検証するためのさまざまな暗号化メカニズムを記述しています。 これは、そうするために必要な情報を含むJSON構造を定義します。

効果的に保護されていると見なされるには、クレームに署名または暗号化する必要があるため、これは広く使用されているJWT仕様の重要な側面です。

最初のケースでは、JWTはJWSとして表されます。 一方、暗号化されている場合、JWTはJSON Web Encryption(JWE)構造でエンコードされます。

OAuthを使用する場合の最も一般的なシナリオは、JWTに署名したばかりです。 これは、通常、情報を「非表示」にする必要はなく、データの整合性を検証するだけであるためです。

もちろん、署名されたJWTを処理する場合でも、暗号化されたJWTを処理する場合でも、公開鍵を効率的に送信できるようにするための正式なガイドラインが必要です。

これは、IETFによっても定義されている暗号化キーを表すJSON構造であるJWKの目的です。

多くの認証プロバイダーは、仕様で定義されている「JWKセット」エンドポイントを提供しています。 これにより、他のアプリケーションはJWTを処理するための公開鍵に関する情報を見つけることができます。

たとえば、リソースサーバーはJWTにある kid (キーID)フィールドを使用して、JWKセット内の正しいキーを検索します。

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

通常、OAuth 2.0などの標準のセキュリティプロトコルを使用するなど、アプリケーションが安全な方法でリソースを提供するようにする場合は、次の手順に従う必要があります。

  1. 独自のサービス、またはOkta、Facebook、Githubなどの有名なプロバイダーのいずれかでクライアントを認証サーバーに登録します
  2. これらのクライアントは、構成した可能性のあるOAuth戦略のいずれかに従って、認証サーバーにアクセストークンを要求します。
  3. 次に、トークンを(この場合はJWTとして)リソースサーバーに提示するリソースにアクセスしようとします。
  4. リソースサーバーは、署名をチェックしてトークンが操作されていないことを確認し、クレームを検証する必要があります
  5. そして最後に、リソースサーバーがリソースを取得し、クライアントが正しい権限を持っていることを確認します

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

後で、JWTと「JWKセット」エンドポイントにサービスを提供する独自の承認サーバーをセットアップする方法を説明します。

ただし、この時点では、既存の承認サーバーを指している最も単純な、そしておそらく最も一般的なシナリオに焦点を当てます。

JWTの署名を検証するために使用する公開鍵など、サービスが受信したアクセストークンをどのように検証する必要があるかを示すだけです。

SpringセキュリティOAuthのAutoconfig機能を使用して、アプリケーションプロパティのみを使用し、シンプルでクリーンな方法でこれを実現します。

3.1. Mavenの依存関係

SpringアプリケーションのpomファイルにOAuth2自動構成の依存関係を追加する必要があります。

<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.6.RELEASE</version>
</dependency>

いつものように、アーティファクトの最新バージョンは MavenCentralで確認できます。

この依存関係はSpringBootによって管理されていないため、バージョンを指定する必要があることに注意してください。

とにかく使用しているSpring Bootのバージョンと一致するはずです。

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

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

@SpringBootApplication
@EnableResourceServer
public class ResourceServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ResourceServerApplication.class, args);
    }
}

次に、アプリケーションがベアラートークンとして受け取るJWTの署名を検証するために必要な公開鍵を取得する方法を示す必要があります。

OAuth2ブートは、トークンを検証するためのさまざまな戦略を提供します。

前に述べたように、ほとんどの承認サーバーは、他のサービスが署名を検証するために使用できるキーのコレクションを含むURIを公開します。

今後作業するローカル承認サーバーのJWKセットエンドポイントを構成します。

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のみをサポートし、SpringBootはJWKセットエンドポイントを構成するための非常に類似したプロパティも提供します。

spring.security.oauth2.resourceserver.jwk-set-uri=
  http://localhost:8081/sso-auth-server/.well-known/jwks.json

3.3. フードの下の春の構成

以前に追加したプロパティは、いくつかのSpringBeanの作成に変換されます。

より正確には、OAuth2ブートは以下を作成します。

  • JwkTokenStore は、JWTをデコードしてその署名を検証する唯一の機能を備えています
  • 以前のTokenStoreを使用するDefaultTokenServicesインスタンス

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

次に、このテーマについてさらに深く掘り下げ、JWTを発行してJWKセットエンドポイントにサービスを提供する承認サーバーを構成する際に、JWKとJWSのいくつかの重要な側面を分析します。

Springセキュリティはまだ認証サーバーをセットアップする機能を提供していないため、SpringセキュリティOAuth機能を使用して認証サーバーを作成することがこの段階での唯一のオプションであることに注意してください。 ただし、Spring SecurityResourceServerと互換性があります。

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

最初のステップは、必要に応じてアクセストークンを発行するように認証サーバーを構成することです。

また、Resource Serverで行ったように、 spring-security-oauth2-autoconfigure依存関係を追加します。

まず、 @EnableAuthorizationServer アノテーションを使用して、OAuth2認証サーバーメカニズムを構成します。

@Configuration
@EnableAuthorizationServer
public class JwkAuthorizationServerConfiguration {

    // ...

}

そして、プロパティを使用してOAuth2.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の発行

これは、次のコンテキストで JwtAccessTokenConverterbeanを作成することで簡単に変更できます。

@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)アプローチを使用してヘッダーとペイロードに署名します。

これは、そこにある多くのJWTデコーダー/ベリファイアオンラインツールの1つでJWTを分析することで検証できます。

取得したJWTをデコードすると、alg属性の値がHS256であることがわかります。これは、HMAC-SHA256アルゴリズムがトークンに署名します。

このアプローチでJWKが必要ない理由を理解するには、MACハッシュ関数がどのように機能するかを理解する必要があります。

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

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

したがって、セキュリティ上の理由から、アプリケーションは署名キーを公に共有することはできません。

学術的な理由でのみ、Springセキュリティ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プロパティを使用できます。

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

セキュリティのニーズによっては、最近言及したエンドポイントの1つを適切に保護し、リソースサーバーにアクセスできるようにするだけで十分であると考える場合があります。

その場合は、承認サーバーをそのままにして、リソースサーバーに別のアプローチを選択できます。

リソースサーバーは、承認サーバーにセキュリティで保護されたエンドポイントがあることを想定しているため、最初に、承認サーバーで使用したのと同じプロパティを使用して、クライアントの資格情報を提供する必要があります。

security.oauth2.client.client-id=bael-client
security.oauth2.client.client-secret=bael-secret

次に、 / oauth / check_token エンドポイント(別名 イントロスペクションエンドポイント)または / 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セットエンドポイントを提供したいと考えています。

キーを共有する場合は、非対称暗号化(特にデジタル署名アルゴリズム)を使用してトークンに署名するとよいでしょう。

これに向けた最初のステップは、キーストアファイルを作成することです。

これを実現する簡単な方法の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. キーストアファイルをアプリケーションに追加する

プロジェクトリソースにキーストアを追加する必要があります。

これは簡単な作業ですが、これはバイナリファイルであることに注意してください。 つまり、フィルタリングできないか、破損します。

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を構成することです。 トークンに署名するプライベートと、整合性を検証するパブリック。

クラスパスのキーストアファイルを使用してKeyPairインスタンスを作成し、.jksファイルを作成したときに使用したパラメーターを作成します。

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を再度リクエストしてデコードできます。

トークンキーエンドポイントを見ると、キーストアから取得した公開キーがわかります。

PEMの「カプセル化境界」ヘッダーで簡単に識別できます。 「-BEGINPUBLICKEY-で始まる文字列

4.8. JWKはエンドポイントの依存関係を設定します

SpringセキュリティOAuthライブラリは、そのままの状態でJWKをサポートしていません。

したがって、プロジェクトに別の依存関係 nimbus-jose-jwt を追加する必要があります。これは、いくつかの基本的なJWK実装を提供します。

<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>7.3</version>
</dependency>

Maven中央リポジトリ検索エンジンを使用して、ライブラリの最新バージョンを確認できることを忘れないでください。

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

以前に構成したKeyPairインスタンスを使用して、 JWKSetbeanを作成することから始めましょう。

@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 インスタンスで構成したキーIDフィールドは、kidパラメーターに変換されます。

この子はキーの任意のエイリアスであり、同じキーがに含まれている必要があるため、通常、リソースサーバーがコレクションから正しいエントリを選択するために使用します JWTヘッダー。

私たちは今、新たな問題に直面しています。 SpringセキュリティOAuthはJWKをサポートしていないため、発行されたJWTにはkidヘッダーが含まれません。

これを解決するための回避策を見つけましょう。

4.10. kid値をJWTヘッダーに追加する

これまで使用してきたJwtAccessTokenConverterを拡張する新しいクラスを作成します。これにより、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メソッドをオーバーライドします。 実装は親の実装と同じですが、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 値であることを確認し、それを使用してリソースを要求できます。

公開鍵が取得されると、リソースサーバーはそれを内部に保存し、将来の要求のために鍵IDにマッピングします。

5. 結論

この包括的なガイドでは、JWT、JWS、およびJWKについて多くのことを学びました。 Spring固有の構成だけでなく、一般的なセキュリティの概念も、実際の例で実際に動作していることを確認します。

JWKSetエンドポイントを使用してJWTを処理するリソースサーバーの基本構成を見てきました。

最後に、JWK Setエンドポイントを効率的に公開する承認サーバーを設定することにより、基本的なSpringセキュリティOAuth機能を拡張しました。

いつものように、両方のサービスはOAuthGithubリポジトリにあります。