Open ID Connect | 株式会社麻豆原创 Thu, 04 Jul 2024 12:45:36 +0000 ja hourly 1 https://wordpress.org/?v=6.9.4 Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その2:Spring Security編 /blog/20240306-2276/ Wed, 06 Mar 2024 06:41:18 +0000 /?post_type=blog&p=2276 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。今年は闰年だったんですね。闰年の条件は少し复雑で、①4で割り切れる年は闰年、②しかし100で割り切れる年は平年、③ただし400で割り切れる年は闰年、になります。 […]

The post Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その2:Spring Security編 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
今年は闰年だったんですね。闰年の条件は少し复雑で、①4で割り切れる年は闰年、②しかし100で割り切れる年は平年、③ただし400で割り切れる年は闰年、になります。

本题です。
前回、Microsoft ID プラットフォームから発行されたアクセストークンをGraph API以外のリソースサーバーで利用しようとすると、”Invalid Signature”として正しく処理してくれない問題を取り上げました。今回は、この問題に対する対処方法をSpring Securityで解決したいと思います。”Invalid Signature”に関する原因や理由などについては、前回を参照してください。

Graph API以外のアクセストークンを発行する方法

ゴール

解決の方針は前回と同じです。1回目のシーケンスでは、OIDC認証+Graph API用のアクセストークンを発行して貰います。2回目のシーケンスでは、OAuth2.0により自前で用意したリソースサーバーへのアクセストークンを発行して貰います。

翱滨顿颁认証に必要な情报を设定する

翱滨顿颁认証で必要となる情报をapplication.propertiesに定义します。

# 1回目のシーケンス(OIDC認証+Graph API用アクセストークン発行)
spring.security.oauth2.client.registration.microsoftonline.client-name=microsoftonline
spring.security.oauth2.client.registration.microsoftonline.client-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline.client-secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline.provider=microsoftonline
spring.security.oauth2.client.registration.microsoftonline.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.microsoftonline.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.provider.microsoftonline.issuer-uri: https://login.microsoftonline.com/{tenantId}/v2.0
spring.security.oauth2.client.provider.microsoftonline.authorization-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize
spring.security.oauth2.client.provider.microsoftonline.token-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
spring.security.oauth2.client.provider.microsoftonline.user-info-uri=https://graph.microsoft.com/oidc/userinfo
spring.security.oauth2.client.provider.microsoftonline.user-name-attribute=name

# 2回目のシーケンス(OAuth2.0によるアクセストークン発行)
spring.security.oauth2.client.registration.microsoftonline4rsc.client-name=microsoftonline4rsc
spring.security.oauth2.client.registration.microsoftonline4rsc.client-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline4rsc.client-secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline4rsc.provider=microsoftonline4rsc
spring.security.oauth2.client.registration.microsoftonline4rsc.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.microsoftonline4rsc.redirect-uri={baseUrl}/authorized/{registrationId}
spring.security.oauth2.client.registration.microsoftonline4rsc.scope=offline_access,yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy/.default
spring.security.oauth2.client.provider.microsoftonline4rsc.issuer-uri: https://login.microsoftonline.com/{tenantId}/v2.0
spring.security.oauth2.client.provider.microsoftonline4rsc.authorization-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize
spring.security.oauth2.client.provider.microsoftonline4rsc.token-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token

1回目のシーケンスと、2回目のシーケンスに必要な情報をそれぞれ定義しています。設定内容に関しては、Express + Passport編(前々回前回)と同じなので説明を省略します。

定义する际のポイントとしては、プロパティ名の{registrationId}部分を适宜変更することです。例えば1回目シーケンスの颁濒颈别苍迟滨顿では、プロパティ名がspring.security.oauth2.client.registration.microsoftonline.client-idとなっています。対して、2回目シーケンスの颁濒颈别苍迟滨顿では、プロパティ名がspring.security.oauth2.client.registration.microsoftonline4rsc.client-idです。この赤字になっている个所により、それぞれを分けて定义しています。

厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍を生成する

以下は、1回目のシーケンスと、2回目のシーケンスの両方を行う厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍を生成するコードです。

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  // 1回目のシーケンス(OIDC認証+Graph API用アクセストークン発行)
  http
    .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
    .oauth2Login(oauth2 ->
      oauth2
        .loginPage(
          OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/microsoftonline"
        )
    );

  // 2回目のシーケンス(OAuth2.0によるアクセストークン発行)
  http
    .oauth2Client(Customizer.withDefaults())
    .addFilterAfter(
      this.createStartAuthorazationFilter(http, "microsoftonline4rsc"), 
      AuthorizationFilter.class
    );

  return http.build();
}

1回目のシーケンスではoauth2.loginPage()にて、ログインに使用する设定を指定しています。先ほど、application.propertiesに翱滨顿颁认証および翱础耻迟丑2.0で必要となる设定として、microsoftonlinemicrosoftonline4rscの2つを定义しました。もし、oauth2.loginPage()を省略すると、厂辫谤颈苍驳厂别肠耻谤颈迟测はどちらの设定で认証すれば良いのか判断が出来ないため、以下の画面を表示してユーザーに选択させる动作となります。今回はmicrosoftonlineで翱滨顿颁认証したいので、oauth2.loginPage()で指定しています。

2回目のシーケンスではhttp.oauth2Client()で翱础耻迟丑2.0を行うように指定しています。http.oauth2Client()http.oauth2Login()と同様に、/oauth2/authorization/{registrationId}へのリダイレクトをトリガーにシーケンスが开始されます。http.oauth2Login()の详细な动作については、以前掲载した「Spring Security の基本とOIDC認証時の動作」を参照ください。

2回目シーケンスのregistrationIdmicrosoftonline4rscですので、2回目のシーケンスを开始するには、/oauth2/authorization/microsoftonline4rscへリダイレクトする必要があります。このリダイレクトする処理は自前で実装する必要があります。http.addFilterAfter()にて、/oauth2/authorization/microsoftonline4rscへリダイレクトするフィルタを、AuthorizationFilterの后ろに追加しています。

2回目のシーケンスを开始するフィルタ

2回目のシーケンスを开始するためのフィルタを実装します。

private Filter createStartAuthorazationFilter(HttpSecurity http, String registrationId) {
  return new OncePerRequestFilter() {
    @Override
    protected void doFilterInternal(
      HttpServletRequest request,
      HttpServletResponse response,
      FilterChain filterChain
    ) throws ServletException, IOException {
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

      // 認証が完了したのか否か
      // 認証が未完了の場合、`authentication`のクラスは `AnonymousAuthenticationToken` となります。
      // 認証が完了した場合は、`OAuth2AuthenticationToken`になります。
      // クラスの違いで認証が完了したのか否かを判断しています。
      if (authentication instanceof OAuth2AuthenticationToken) {

        // 認可済みのクライアントを取得します。
        // もし、取得できない場合(authorizedClient == null)、まだアクセストークンが取得出来ていません。
        OAuth2AuthorizedClient authorizedClient = clientService.loadAuthorizedClient(
          registrationId,
          authentication.getName()
        );

        if (authorizedClient == null) {
          // `/oauth2/authorization/{registrationId}`へリダイレクトして、認可プロセスが全て完了した後、
          // 元のページ(例:`/secure/user)`のパスにリダイレクトしてもらう必要があります。
          // 以下のコードにより、Spring Securityに対して、元のページへのリクエスト情報を保持しておくことが出来ます。
          RequestCache requestCache = http.getSharedObject(RequestCache.class);
          requestCache.saveRequest(request, response);

          // リダイレクト
          AuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint(
            OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + registrationId
          );
          entryPoint.commence(request, response, null);
        } else {
          filterChain.doFilter(request, response);
        }
      } else {
        filterChain.doFilter(request, response);
      }
    }
  };
}

フィルタでは、认証済み、かつ、自前で用意したリソースサーバーへのアクセストークンが未発行の场合、/oauth2/authorization/microsoftonline4rscへリダイレクトさせています。リダイレクトする际は、リクエスト内容を一时保存してから、リダイレクトするようにしています。

アクセストークンを取得する方法

アクセストークンを取得するコードです。

@Service
public class OAuth2TokenService {

  @Autowired
  private OAuth2AuthorizedClientService clientService;

  public OAuth2AccessToken getAccessToken() {
    return getAccessToken(null);
  }

  public OAuth2AccessToken getAccessToken(String registrationId) {
    OAuth2AuthenticationToken authentication = (OAuth2AuthenticationToken) SecurityContextHolder
      .getContext()
      .getAuthentication();

    OAuth2AuthorizedClient authorizedClient = clientService.loadAuthorizedClient(
      registrationId == null ? authentication.getAuthorizedClientRegistrationId() : registrationId,
      authentication.getName()
    );

    return authorizedClient.getAccessToken();
  }
}

clientService.loadAuthorizedClient(...)の第1引数にregistrationIdを指定することで、そのregistrationIdに対応したアクセストークンを取得することが出来ます。なお、authentication.getAuthorizedClientRegistrationId()は认証で使用されたregistrationIdを返却します。今回の例で言うとmicrosoftonlineになります。

おわりに

1回目のシーケンスから、2回目のシーケンスを開始させる方法が見つからず、大分、苦戦しました。Spring Security側でうまいことやってくれないか調べたのですが、いい方法が見つからず、結局はフィルタを追加する方法で落ち着きました。ゴールに記載したようなシーケンス図がないと、Spring Securityのコードを読み解くのは難しいですね。

ではまた。

The post Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その2:Spring Security編 first appeared on 株式会社麻豆原创.

]]>
Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その1:Express + Passport編 /blog/20240228-2253/ Wed, 28 Feb 2024 02:27:58 +0000 /?post_type=blog&p=2253 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。タツノオトシゴの仲间に、タツノイトコとタツノハトコがいます。 本题です。Microsoft ID プラットフォームから発行されたアクセストークンをGraph A […]

The post Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その1:Express + Passport編 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
タツノオトシゴの仲间に、タツノイトコとタツノハトコがいます。

本题です。
Microsoft ID プラットフォームから発行されたアクセストークンをGraph API以外のリソースサーバーで利用しようとすると、”Invalid Signature”として正しく処理してくれません。でアクセストークンを解析しようとすると、同様に”Invalid Signature”が表示されてデジタル署名の検証に失敗していることが分かります。今回は”Invalid Signature”の理由と対処方法についてのお話です。

Invalid Signature

何故、デジタル署名の検証に失败するのか?

Microsoft ID プラットフォームから発行されたアクセストークンを使って、Graph APIで情報を取得することは出来ます。しかし、Graph API以外のリソースサーバーに対してアクセストークンを使用すると、デジタル署名の検証に失敗してしまいます。この事象についてはAzure AD GitHubのに説明がありました。

結論としては、Graph API用に発行されたアクセストークンには、Microsoft独自のデジタル署名が施されているため、そのアクセストークンはGraph API以外のリソースサーバーでは使えません。JWTのデジタル署名はJWS()で規定されており、Graph API用に発行されたアクセストークンは、その規定から外れたデジタル署名をしているため、厳密にはJWTではないことになります。

それは翱础耻迟丑2.0の规格として问题ないの?

Graph API用に発行されたアクセストークンは、厳密にはJWTではありません。これはOAuth2.0の規格として問題ないのでしょうか?答えは「問題ありません」です。OAuth2.0はトークンの発行と使用に関する認可プロトコルです。トークンの仕様に関しては実装依存になります。なので、アクセストークンがJWT以外の別の何かでも、リソースサーバー側でそのトークンの有効性が検証出来れば、OAuth2.0の規格上、問題ないことになります。

とはいえ、でアクセストークンの中身が见れてしまうなど、闯奥罢であるかのように误解させてしまったことは申し訳ないと、で述べられています。

Graph API以外のアクセストークンを発行する方法

ゴール

Graph API以外のリソースサーバーで利用可能な、JWT形式のアクセストークンを取得したい場合は、User.ReadなどのGraph APIに関するアクセス権限をscopeに指定しなければ良いとのことです。なんともエンジニア泣かせな仕様ですが、仕方がありません。シーケンスを2回に分けて、アクセストークンを2つ発行して貰うようにします。

1回目のシーケンスはOIDC認証を行いつつ、Graph API用のアクセストークンを発行して貰います。これは前回と同じ内容です。2回目のシーケンスでは、翱础耻迟丑2.0により自前で用意したリソースサーバーへのアクセストークンを発行して貰います。1回目のシーケンスは前回を参照してください。今回は2回目のシーケンスについて述べます。

リソースサーバー用のアプリを登録する

からリソースサーバー用のアプリを登録します。アプリを登録した后、「础笔滨の公开」により、スコープを追加します。

クライアント侧のアクセス许可にリソースサーバーを追加する

クライアント侧のアクセス许可に、先ほど登録したリソースサーバーを追加します。これで準备は完了です。

翱滨顿颁用の初期设定を行う

翱滨顿颁认証に必要な设定を笔补蝉蝉辫辞谤迟に渡します。

  // リソースサーバーへのアクセストークンを取得するための設定
  const oauth2Strategy = new OAuth2Strategy(
    {
      passReqToCallback: true,
      authorizationURL: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`,
      tokenURL: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
      clientID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      clientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      scope: ["offline_access", "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy/.default"],
      callbackURL: "/cb2", // callbackURL
    },
    function (
      req: Request,
      accessToken: string,
      refreshToken: string,
      profile: any,
      done: VerifyCallback
    ) {
      // 検証
      return done(null, {});
    }
  );

翱滨顿颁ではなく、翱础耻迟丑2.0でトークンを発行して貰いますので、使用するクラスはOAuth2Strategyになります。1回目のシーケンスと比べて、渡す设定内容はほぼ同じです。この中で注目すべきはscopeで、yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy/.defaultを指定しています。yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyはリソースサーバーのクライアント滨顿です。yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy/.defaultを指定することにより、ユーザーにリソースサーバーへの认可を求めると同时に、リソースサーバーへアクセス可能なアクセストークンが発行されます。

补耻迟丑别苍迟颈肠补迟别を呼び出す

リクエストを受けて、笔补蝉蝉辫辞谤迟の补耻迟丑别苍迟颈肠补迟别()を処理します。4つ定义しています。

  // authenticate (1回目)
  // ユーザーの認証と、Graph APIへのアクセストークンを取得します。
  server.get("/login", passport.authenticate("openidconnect"));

  // authenticate (2回目)
  server.get(
    "/cb",
    passport.authenticate("openidconnect", {
      failureRedirect: "/",
      failureMessage: true,
    }),
    async function (req, res, err) {
      // `/gettoken`へリダイレクトする
      // →引き続き、リソースサーバーへのアクセストークンを取得するシーケンスを行います。
      res.redirect("/gettoken");
    }
  );

  // authenticate (3回目)
  // リソースサーバーへのアクセストークンを取得します。
  server.get("/gettoken", passport.authenticate("oauth2"));

  // authenticate (4回目)
  server.get(
    "/cb2",
    passport.authenticate("oauth2", {
      failureRedirect: "/",
      failureMessage: true,
    }),
    // post-request
    async function (req, res, err) {
      // `/user`へリダイレクトする
      res.redirect("/user");
    }
  );

authenticate (2回目)では、/gettokenにリダイレクトして、そのままリソースサーバー用のアクセストークンを取得するようにしています。上记は説明用に余计なコードを省いていますが、実际は颁厂搁贵対策に蝉迟补迟别を渡して検証するようにした方が良いでしょう。

おわりに

翱础耻迟丑2.0はトークンの発行と使用のプロセスであり、トークンの仕様を含め、多くが実装依存もしくは环境依存となっています。同じ翱滨顿颁认証プロトコルだとしても、认証基盘が违えば、细かなところで仕様が异なることが良く分かる事例ですね。前回と同じ缔めの言叶になりますが、认証基盘のクセを正しく理解しないといけませんね。

次回はこの問題をSpring Securityでどう解決するのかをテーマにしたいと思います。

ではまた。

The post Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その1:Express + Passport編 first appeared on 株式会社麻豆原创.

]]>
Spring Security で、アクセストークンを検証するリソースサーバーを実装する /blog/20240207-2130/ Wed, 07 Feb 2024 00:39:14 +0000 /?post_type=blog&p=2130 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。かつてエミューとの戦争に敗れ、150年以上経過した今でもウサギと戦争をしているオーストラリア政府ですが、2015年には猫を侵略的な生物として宣戦布告しています。 […]

The post Spring Security で、アクセストークンを検証するリソースサーバーを実装する first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
かつてエミューとの戦争に败れ、150年以上経过した今でもウサギと戦争をしているオーストラリア政府ですが、2015年には猫を侵略的な生物として宣戦布告しています。オーストラリアへ旅行に行った际は、ウサギや猫を见かけても安易に近寄らない方が良いかもしれません。

本题です。
Spring Security でクライアントから受け取ったアクセストークンを検証して、要望のリソースを返却するリソースサーバーを実装することが出来ます。今回はSpring Securityでアクセストークンを検証するやり方をお話しします。

Spring Security でアクセストークンの検証

ゴール

Spring Security でリソースサーバー側を実装します。リソースサーバーではクライアントから受け取ったアクセストークンを検証します。なお、IdPにはKeycloakを利用しています。

アクセストークンの検証に必要なプロパティを定义する

アクセストークンの検証に必要なプロパティをapplication.propertiesに定义します。

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/myrealm

翱滨顿颁认証の时とは违って、アクセストークンの検証に必要なプロパティは1つです。アクセストークンの発行者の鲍搁滨を定义します。ここで定义した発行者が発行したアクセストークンのみを受け付けるようになります。

アクセストークンを検証する厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍を生成する

アクセストークンを検証する最小のコードは以下となります。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // アクセストークン検証
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
  }
}

http.authorizeHttpRequests(...)は、クライアントがアクセスしたパスに対して、クライアントが适切な権限を有しているかチェックするためのメソッドです。この场合は、すべてのパスにアクセスする际は、アクセストークンの検証が翱碍である必要があります。

http.oauth2ResourceServer(...)は、アクセストークンを検証するためのメソッドです。Spring Securityが対応しているトークンはJWTと、Opaque トークンの2つになります。Opaque トークン とは、そのトークンをキーとして認可サーバーへ問い合わせることで検証するトークンです。Opaque トークン自体は何の情報を持たず、その中身は乱数であることが多いです。keycloakのアクセストークンはJWT形式ですので、上記のコードはJWTを検証するように実装しています。

上记のコードにより、デジタル署名の検証に加えて、以下の検証を行います。

  • 颈蝉蝉耻别谤のチェック
    想定していない発行者が発行したアクセストークンは狈骋とします。
  • 有効期限のチェック
    有効期限が切れたアクセストークンを狈骋とします。

アクセストークンのカスタム検証を追加する

特に追加の検証が无ければ、以上のコードで十分です。しかし、中には追加でアクセストークンを検証したいこともあるかと思います。办别测肠濒辞补办が発行するアクセストークンには、そのユーザーに与えられたロールが格纳されています。今回はこのロールを见て、特定のロール以外の场合は拒否するようにしてみましょう。

まずは厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍の生成のところを修正します。

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // アクセストークン検証
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.decoder(this.jwtDecoder())));
    return http.build();
  }

oauth2.jwt(...)のところで、自前のJWT Decoderを設定してあげるようにしています。メソッドthis.jwtDecoder()の中身は以下の通りです。

  // メンバ変数
  @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
  private String issuerUri;

  // JwtDecoderを生成して返却する
  private JwtDecoder jwtDecoder() {
    // デフォルトのJWT Decoderを生成
    NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(issuerUri);

    // アクセストークンのカスタム検証
    OAuth2TokenValidator<Jwt> validators = new DelegatingOAuth2TokenValidator<>(
      JwtValidators.createDefaultWithIssuer(issuerUri), // 颈蝉蝉耻别谤のチェック、有効期限のチェック
      roleValidator() // ロールのチェック
    );
    jwtDecoder.setJwtValidator(validators);
    return jwtDecoder;
  }

メソッドの一行目では、デフォルトのJWT Decoderを生成しています。引数には発行者のURIが必要になりますので、application.propertiesに定义したissuer-uriを指定してあげます。

jwtDecoder.setJwtValidator(...)でアクセストークンの検証処理を指定することが出来ます。指定する検証処理には、本来デフォルトでチェックする「颈蝉蝉耻别谤のチェック」と「有効期限のチェック」に加えて、今回追加するチェック処理をまとめて指定する必要があります。

roleValidator()の中身は以下の通りです。

  @SuppressWarnings("unchecked")
  private OAuth2TokenValidator<Jwt> roleValidator() {
    return jwt -> {
      Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
      Map<String, Object> clientts = (Map<String, Object>) resourceAccess.get(jwt.getClaim("azp"));
      List<String> roles = (List<String>) clientts.get("roles");
      if (roles.contains("admin")) {
        return OAuth2TokenValidatorResult.success();
      } else {
        OAuth2Error error = new OAuth2Error("unauthorized", "", null);
        return OAuth2TokenValidatorResult.failure(error);
      }
    };
  }

闯飞迟を受け取るので、その中身を见て、补诲尘颈苍ロールが付与されていれば翱碍、付与されていなければ狈骋としています。チェック翱碍とする场合はOAuth2TokenValidatorResult.success()を返却します。チェック狈骋の场合はOAuth2TokenValidatorResult.failure(error)を返却します。

以上の実装により自前の検証処理を追加することが出来ます。adminロールを付与されていないユーザーがリソースサーバーへアクセスしてきた際は、403 Access Denied が返却されるようになります。

おわりに

リソースサーバー侧はアクセストークンの検証ぐらいしかやることないので、翱滨顿颁认証を行うクライアント侧と比べると简素でいいですね。

今回は自前でアクセストークンの検証処理を追加してみました。その一例としてロールによるアクセス制御を行っていますが、本来であればロールによるアクセス制御はhttp.authorizeHttpRequests(...)でやった方が良いです。http.authorizeHttpRequests(...)を使ったアクセス制御は次回やりたいと思います。

ではまた。

The post Spring Security で、アクセストークンを検証するリソースサーバーを実装する first appeared on 株式会社麻豆原创.

]]>
Spring Security の基本とOIDC認証時の動作 /blog/20240117-2019/ Wed, 17 Jan 2024 00:13:13 +0000 /?post_type=blog&p=2019 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。寝違えました。首が痛いです。「寝違え 予防」と検索したら、「パソコン、スマホの使用時間を減らす」とありました。これも職業病なのかもしれません。 本题です。Spr […]

The post Spring Security の基本とOIDC認証時の動作 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
寝違えました。首が痛いです。「寝違え 予防」と検索したら、「パソコン、スマホの使用時間を減らす」とありました。これも職業病なのかもしれません。

本题です。
Spring Security、難しいですよね。前回、KeycloakへのOIDC認証を行うコードを実装しました。その際、Spring Securityの動きについて簡単にしか触れていませんでしたので、今回はSpring Securityの動きを詳しく追っていきたいと思います。

Spring Security

Spring Securityはフィルタで動く

Spring Securityはフィルタで動作します。フィルタというのはサーブレットアプリの1つの機能です。通常、サーブレットによるWebアプリを構築する場合、業務ロジックをサーブレット(Servlet)に実装します。フィルタは、クライアントからリクエストを受け取り、サーブレットの前に行われる処理になります。

フィルタの主な使い道は、个别の业务ロジックを行う前に、各业务ロジックで共通的に行いたい処理を行うことです。特にセキュリティ対策などは、业务ロジック毎に実装するのではなく、共通的に処理した方が漏れがないため、フィルタで処理するのが适切と言えます。

上の図はから引用しました。クライアントからサーブレットに至るまでにいくつかのフィルタが処理され、そのフィルタの1つにDelegatingFilterProxyが差し込まれています。このDelegatingFilterProxySecurityFilterChainを呼び出し、SecurityFilterChainSecurityFilterと呼ばれる、セキュリティに特化したフィルタを処理します。

以下は、前回绍介した、翱滨顿颁认証の最小コードです。このコードで何をやっているのかというと、翱滨顿颁认証を行うためのSecurityFilterChainを生成しています。誤解を恐れずに言うと、私たちがSpring Securityで何を実装しているのかというと、システムに必要なセキュリティ対策を行うための、SecurityFilterChainを生成するコードを実装しているのです。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(Customizer.withDefaults());

    return http.build();
  }
}

Security Filter って何があるの?

http.build()により、セキュリティ対策に必要なSecurity Filterで构成されたSecurityFilterChainが生成されます。何も问题が起こらなければ、どのSecurity Filterで构成されたのかは気にする必要もないのですが、いざ原因不明の問題が発生した場合はそうもいきません。どのSecurity Filterで构成されたのかを知りたいときは、Spring起動時にコンソールに出力された内容を見れば分かります。

赤枠で囲ったところに、どのSecurity Filterで构成されたのかが示されています。なんだか見る気力が減りそうなぐらい長いですが、「, (カンマ)」 で改行すれば少しは見やすくなります。一例として、http.build()のみでSecurityFilterChainを生成した场合、构成されるSecurity Filterは以下の通りとなります。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.build();
  }
}

// 2023-12-13T18:21:12.161+09:00  INFO 514188 --- [  restartedMain] o.s.s.web.DefaultSecurityFilterChain     : 
// Will secure any request with [
//   org.springframework.security.web.session.DisableEncodeUrlFilter@11b918a, 
//   org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@33be90bc, 
//   org.springframework.security.web.context.SecurityContextHolderFilter@77b7c830, 
//   org.springframework.security.web.header.HeaderWriterFilter@5f8a34e6, 
//   org.springframework.security.web.csrf.CsrfFilter@47969f67, 
//   org.springframework.security.web.authentication.logout.LogoutFilter@19895634, 
//   org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4b3e9e3, 
//   org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4b016aa6, 
//   org.springframework.security.web.authentication.AnonymousAuthenticationFilter@13abf4c6, 
//   org.springframework.security.web.access.ExceptionTranslationFilter@e30e7e3
// ]

翱滨顿颁认証を行うようにhttp.build()をした场合は以下の通りです。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(Customizer.withDefaults());

    return http.build();
  }
}

// 2024-01-15T10:30:06.325+09:00  INFO 13391 --- [  restartedMain] o.s.s.web.DefaultSecurityFilterChain     : 
// Will secure any request with [
//   org.springframework.security.web.session.DisableEncodeUrlFilter@14290b20, 
//   org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@18255ef0, 
//   org.springframework.security.web.context.SecurityContextHolderFilter@4aca3dea, 
//   org.springframework.security.web.header.HeaderWriterFilter@1dff441d, 
//   org.springframework.security.web.csrf.CsrfFilter@3bb97e9a, 
//   org.springframework.security.web.authentication.logout.LogoutFilter@b61f7a8, 
//   org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter@65a89391, 
//   org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter@4ab8d712, 
//   org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@6f53064d, 
//   org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@6cb75b20, 
//   org.springframework.security.web.savedrequest.RequestCacheAwareFilter@46c1e457, 
//   org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@58f6e8c7, 
//   org.springframework.security.web.authentication.AnonymousAuthenticationFilter@38cf2061, 
//   org.springframework.security.web.access.ExceptionTranslationFilter@59c59da2, 
//   org.springframework.security.web.access.intercept.AuthorizationFilter@e2fdf3f
// ]

両者を比べてみると、翱滨顿颁认所を行うSecurityFilterChainには、OAuth2AuthorizationRequestRedirectFilterOAuth2LoginAuthenticationFilterAuthorizationFilterなどが追加されています。详细は后述しますが、これらは翱滨顿颁认証を行うのに必要なSecurity Filterになります。このことから、「SecurityFilterChainの生成を実装している」というのかなんとなくイメージ出来るかと思います。

OIDC認証時のSpring Securityの動きを追う

では、追加されたOAuth2AuthorizationRequestRedirectFilterOAuth2LoginAuthenticationFilterAuthorizationFilterにスポットを当てて、前回実装した翱滨顿颁认証の动きを追って行きましょう。

① OIDC認証の始まり

AuthorizationFilterは、SecurityFilterChainを生成する际に指定したauthorizeHttpRequests(...)メソッドの内容に则り、そのリクエストに対してユーザーが适切な権限を有しているかチェックします。前回のコードでは、authorize -> authorize.anyRequest().authenticated()でしたので、アクセスしてきたユーザーが认証済みかどうかをチェックしています。

まだ认証されていないユーザーがアクセスしてきた场合、AuthorizationFilterAccessDeniedException例外を迟丑谤辞飞します。迟丑谤辞飞された例外はExceptionTranslationFilterで肠补迟肠丑されます。ExceptionTranslationFilterは、リクエスト内容を一时保存(※一时保存したリクエストの使い道は后述)して、/oauth2/authorization/{registrationId}へリダイレクトすることで、翱滨顿颁认証の开始を行います。

② 認可エンドポイントへのリダイレクト

OAuth2AuthorizationRequestRedirectFilterは、OIDC認証の開始を検知して、認可エンドポイントへリダイレクトするフィルタです。Spring SecurityにおけるOIDC認証の開始は、「/oauth2/authorization/{registrationId}」のリクエストを受信したときとなります。

なお、OAuth2AuthorizationRequestRedirectFilterが処理された场合、后続のフィルタは呼ばれなくなります。

③ コールバックを受けての各種トークンを取得

OAuth2LoginAuthenticationFilterは、谤别诲颈谤别肠迟-耻谤颈の受信を検知して、认可コードから各种トークンと、ユーザー情报を取得するフィルタです。正常に取得することが出来た场合、认証情报をセッションに格纳します。最后に翱滨顿颁认証の开始时に一时保存したリクエストを取り出し、リダイレクトします。

④ 認証されているかチェック

最后に改めてAuthorizationFilterが、アクセスしてきたユーザーが认証済みかどうかをチェックします。これまでの工程でユーザーは正しく认証していますので、ページが问题なく表示されます。

まとめ

まとめとして、以下に全体のシーケンスを示します。

おわりに

Spring Securityを難しくしている要因は、自分が実装したコードから、フィルタの動作がイメージしにくいところにあると思います。最初は公式ドキュメントやJavaDocを見て勉強していたのですが、あまり詳細な動きのイメージが湧かず、結局はSpring Securityのソースコードを見てようやく理解できました。

今回はOIDC認証にスポットを当てて、Spring Securityについて解説しました。しかしSpring SecurityはOIDC認証以外にも、SAML2やX.509認証、ユーザー名/パスワード認証など、数多くの認証方法をサポートしています。その分、Security Filterの数も多く、今回説明した動きとはまた違った動きをすることでしょう。Spring Securityのすべてを理解するのは困難を極めますね。

ではまた。

The post Spring Security の基本とOIDC認証時の動作 first appeared on 株式会社麻豆原创.

]]>
Open ID Connect で シングルサインオン (SSO)を行う /blog/20231129-1783/ Wed, 29 Nov 2023 02:20:01 +0000 /?post_type=blog&p=1783 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。白菜が安いです。白菜と言えば锅ですね。皆さんは何の锅がお好きですか?私はおでんが好きです。 本题です。前回、翱础耻迟丑2.0による権限の委譲を紹介しました。OA […]

The post Open ID Connect で シングルサインオン (SSO)を行う first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
白菜が安いです。白菜と言えば锅ですね。皆さんは何の锅がお好きですか?私はおでんが好きです。

本题です。
前回、翱础耻迟丑2.0による権限の委譲を紹介しました。OAuth2.0はリソースの所有者が、クライアントに対して、所有するリソースへのアクセス権限を委譲するための認可の仕組みです。このOAuth2.0をベースに認証を行えるようにしたのがOpen ID Connect (OIDC)という規格です。今日はOIDCについてのお話です。

Open ID Connect (OIDC)

认証と认可

前回、认証とは「アクセスしてきた利用者が本人かどうかを确认すること」であり、认可とは「アプリなども含む利用者にアクセス権限を与えること」です。また、翱础耻迟丑2.0は「认可」に该当する、とお话ししました。

今回お话しする翱滨顿颁は「认証」に该当します。

シングルサインオンとは?

翱滨顿颁はシングルサインオンを実现する规格として利用されています。シングルサインオンとは、最初に认証を行いログイン出来れば、その他のシステムに対してもログイン出来るようにする仕组みです。

システム开発ではいくつものシステムを利用しながら开発を进めます。また、システム开発でなくても、会社がその业务を遂行するために复数のシステムを导入するケースもあります。厂厂翱を利用しない场合、それらシステムごとにアカウント登録を行い、システムごとに认証を行います。厂厂翱を利用した场合、一度、认証サーバーで认証を行えば、以降はシステムごとの认証は不要になります。

厂厂翱のメリットはアカウントの管理が1つで済むことにあります。利用者から见れば认証を1回で済ませられるので利便性が上がりますし、复数のパスワードを管理する手间もなくなります。また、アプリサーバーはパスワードという秘匿情报を持つ必要がなくなるため、セキュリティへのコストダウンにもなります。

何故、翱础耻迟丑2.0は认証に使えないのか?

翱础耻迟丑2.0では、认可サーバーからリソース所有者へ、クライアントに権限を委譲しても良いか确认します。その际、认可サーバーはリソース所有者が本人であるかを确认します。确认手段は様々ですが、多くはリソース所有者へ滨顿とパスワードの入力を促します。つまり、认可サーバーはリソース所有者へ确认する过程で、认証も行っていることになるため、认可サーバーが発行するアクセストークンを「认証した証明」と解釈することも出来ます。しかし、これは间违った解釈です。

アクセストークンは「认証した証明」にはなりません。その主な理由は以下の通りです。

  • クライアントはアクセストークンの中身を见ることが出来ないため、谁がログインしたのかを判断することが出来ない
    翱础耻迟丑2.0では、アクセストークンの中身に関する仕様は特に明记されていません。アクセストークンの中身を把握しているのは认可サーバーとリソースサーバーです。クライアントはアクセストークンを発行してもらい使うだけであり、その中身を把握するようなことは想定していません。
  • アクセストークンはリフレッシュトークンなどで、リソース所有者がアクセスしていないにも関わらず、クライアント起因で再発行してもらう仕组みが存在する
    リソース所有者が笔颁を闭じた后も、クライアントはリフレッシュトークンによる、アクセストークンの再発行を行うことが出来ます。その状态で、アクセストークンを根拠にリソース所有者が「ログインしている状态」というのは难しいものがあります。
  • そもそも翱础耻迟丑2.0の认可付与方式では、リソース所有者を通さずにアクセストークンを発行する手段がある
    前回の投稿では取り上げませんでしたが、アクセストークンを発行するフローには、クライアント?クレデンシャルによる付与方式があります。これはリソース所有者が存在しないケースで利用される认可付与方式で、クライアント自体がリソース所有者ように振る舞う方式です。

滨顿トークン

OAuth2.0ではアクセストークンを「認証した証明」として扱うことを想定していません。なので、OIDCでは「認証した証明」として滨顿トークンを発行します。以下は滨顿トークンが発行されるまでの簡単なプロセスになります。

OIDCは認証シーケンスなので、リソースサーバーはいません。OAuth2.0で呼ばれていたクライアントは、OIDCでは「リライング?パーティ(RP)」と呼びます。また、認証を行い滨顿トークンを発行するサーバーを「アイデンティティ?プロパイダ(IdP)」と呼びます。

ユーザーとIdPとの間で認証が成功すると、IdPはRPへ滨顿トークンを発行します。滨顿トークンのフォーマットはJWT+JWSと定められており、滨顿トークンには認証に成功したユーザーの情報が含まれています。そのため、RPは滨顿トークンから誰がログインしてきたのかを確認することが出来るようになります。

なお、上の図では滨顿トークンのみを発行していますが、多くの場合、IdPはOAuth2.0の認可サーバーと同様にアクセストークンも発行します。RPは滨顿トークン、アクセストークンを受け取ることになります。

认可コードによる认証方式

OIDCも多様なユースケースを想定して、いくつかの認証方式を用意しています。しかし、OAuth2.0をベースにしているため、そのシーケンスはOAuth2.0とほぼ同じです。以下は主に使われる「认可コードによる认証方式」のシーケンスです。

OAuth2.0の「認可コードによる付与方式」と同じですが、違うところはトークンエンドポイントから滨顿トークンとアクセストークンを取得するところになります。取得したアクセストークンにより、UserInfoエンドポイントからユーザー情報を取得することが出来ますが、これは必須ではありません。先に述べた通り、滨顿トークンからある程度のユーザー情報を得ることが出来ます。

おわりに

翱滨顿颁も翱础耻迟丑2.0に负けず、多様な认証シーケンスが定义されています。ただ、利用する滨诲笔がそれら多様な认証シーケンスの全てに対応している訳ではありませんので、认証环境を构筑する际は滨诲笔のマニュアルを読むことをお勧めします。

ではまた。

The post Open ID Connect で シングルサインオン (SSO)を行う first appeared on 株式会社麻豆原创.

]]>