JHipster によるエンタープライズアプリケーションの構築(7) Amazon Cognitoを利用したユーザー認証

カバー

この記事では、エンタープライズシステムに JHipster を適用する際の認証の問題解決方法について考えていきます。

エンタープライズシステム開発における認証方式

システム開発において、認証方式は既存システム連携や統合の際に一貫性とセキュリティを確保するための重要な課題となります。
今日のエンタープライズシステムは多くの場合、オンプレミス、クラウドサービスを組み合わせ、複数のアプリケーションやサービスと連携しています。
しかし、アプリケーションやサービス間で認証方法が異なると、それぞれの認証情報を管理する作業が増加し、ユーザビリティが損なわれるとともに、セキュリティリスクが増大します。
この問題を解決するために、多くのシステムでは統合認証が使われています。
統合認証を用いることで、ユーザーは一度の認証だけで複数のシステムにアクセスでき、それぞれのシステムで異なる認証情報を管理する必要がなくなります。
また、認証情報の管理が一元化されることで、セキュリティポリシーの適用や監査、異常検知が容易になります。
統合認証を実現するためには、各システム間で認証情報を安全に共有するための適切な認証手段が必要です。
その手段として、認証プロトコルである OpenID Connect(OIDC)が多くのシステムで利用されています。

OIDC と JHipster

OIDC は、ユーザー認証を行うためのオープンスタンダードなプロトコルであり、OAuth2.0 (アクセストークンを利用し、セキュアなサービスの利用ができる認可プロセス)を拡張して認証を行うための仕様となっています。
Spring ベースのアプリケーションを作成する場合、セキュリティを強化するためのフレームワークとして、OIDC をサポートしている Spring Security を採用することが選択肢になります。
しかし、Spring Security を組み込んだ上で認証処理を実装するのは容易ではありません。
一方、JHipster では、デフォルトで Spring Security を活用しており、認証処理に関するコードや、他の認証サービスとの連携に必要なコードを自動生成してくれます。
そのため、JHipster を使用すると、認証処理を簡単に構築することができます。

クラウドサービス上での認証

JHipster は、上記で述べたように、OIDC をサポートしていることが特徴の一つです。
これにより JHipster は、OIDC を用いて認証を行う多様なクラウドサービスとの連携が可能となります。
今回、エンタープライズシステムに JHipster を適用するにあたり、その多様なクラウドサービスの中から、より需要が高いと予想される Amazon Web Services(AWS)を選択しました。 AWS は、その広範なサービスと高度な信頼性により、世界中の多くのエンタープライズシステムで利用されています。

次に、AWS 上で OIDC を用いて認証を行うにあたり、認証基盤の検討が必要となります。 JHipster では、デフォルトの OIDC サーバーとして Keycloak や Okta が利用可能(後述の認証フローについてを参照)ですが、 AWS 上で JHipster を動かすにあたっては、 AWS の認証基盤サービスである Amazon Cognito を利用してユーザー認証を行うこととしました。
以下、それらに関する各種設定方法について検証、説明します。
また、本内容を基に、他のメジャーなクラウドサービスである Azure、GCP にも適用が可能と考えます。

システム構成

AWS のシステム構成は、以下のような構成とします。

エンタープライズシステムでの運用を想定し、AWS が推奨するシステム構成のベストプラクティス(AWS Well-Architected)に準じた構成とします。

  • ELB や Web サーバー、データベースをパブリック/プライベートネットワークにサブネット分類することで、耐障害性やセキュリティを向上させます。

  • Amazon ECS on AWS Fargate を JHipster アプリケーションの実行環境とします。
    マネージドサービスにより EC2 へのサーバー構築と比べセキュリティ運用、キャパシティ管理などの運用コストが削減されます。 AWS のマルチ AZ 上に分散配置することにより、業務継続性や稼働率の向上などの高可用性を実現します。

  • RDS for PostgreSQL をデータベースの実行環境とします。
    費用対効果の観点から DB は PostgreSQL とし、 マネージドサービスにより、性能スケーリング、障害復旧、バックアップやリストアなどにおいて運用コストが削減されます。

上記構成図を基に、Amazon Cognito と JHipster アプリケーションの生成および設定方法について説明します。 その他の AWS サービスについては説明を割愛します。

JHiptster アプリケーションの生成

OAuth 2.0/OIDC 認証に使用する JHipster アプリケーションを生成します。
ここでは、JHipster8.1 を対象に検証を行います。
認証タイプの選択で「OAuth 2.0 / OIDC Authentication (stateful, works with Keycloak and Okta)」を指定し生成します。
また、クライアントは React、DB はシステム構成のとおり PostgreSQL を選択します。
以下、生成例を示します。

? What is the base name of your application? cognitoApp
? Which *type* of application would you like to create? Monolithic application (recommended for simple projects)
? Do you want to generate a feign client? No
? Besides Junit, which testing frameworks would you like to use?
? Do you want to make it reactive with Spring WebFlux? No
? What is your default Java package name? com.mycompany.myapp
? Which *type* of authentication would you like to use? OAuth 2.0 / OIDC Authentication (stateful, works with Keycloak and Okta)
? Which *type* of database would you like to use? SQL (H2, PostgreSQL, MySQL, MariaDB, Oracle, MSSQL)
? Which *production* database would you like to use? PostgreSQL
? Which *development* database would you like to use? H2 with disk-based persistence
? Which cache do you want to use? (Spring cache abstraction) No cache - Warning, when using an SQL database, this will disable the Hibernate 2nd level
cache!
? Would you like to use Maven or Gradle for building the backend? Gradle
? Which other technologies would you like to use?
INFO! Disabling hibernate cache for cache provider no
? Which *Framework* would you like to use for the client? React
? Besides Jest/Vitest, which testing frameworks would you like to use?
? Do you want to generate the admin UI? Yes
? Would you like to use a Bootswatch theme (https://bootswatch.com/)? Default JHipster
WARNING! Could not fetch bootswatch themes from API. Using default ones.
? Would you like to enable internationalization support? Yes
? Please choose the native language of the application Japanese
? Please choose additional languages to install English

AWS 側の設定

Amazon Cognito は、AWS などに構築した、Web アプリケーションやモバイルアプリケーションに認証、認可、ユーザー管理機能を提供するサービスとなります。
Amazon Cognito の主な機能の一つである、ユーザープール(ウェブおよびモバイルユーザーにサインアップとサインインオプションを提供するユーザーディレクトリー) と連携し、ユーザー認証を実現します。

認証フローについて

OIDC は、OAuth のフローを基にしており、本来、クライアント側で行っていた認証処理を、他サーバー(OpenID Provider)にお任せし、認証結果を ID トークンとしてクライアントが受け取って認証します。
OIDC を利用するクライアントを Relying Party (RP) 、OpenID Connect をサポートする認証サーバーを OpenID Provider (OP) と呼びます。
JHipster では、OP として、デフォルトの OIDC サーバーである Keycloak や Okta が利用できますが、今回はユーザープールを OP、JHipster を RP として利用します。
また、Hosted UI(Amazon Cognito が用意した認証画面を利用、認証処理も実施)を活用し、OIDC の主流である、認可コードフロー:Authorization Code Flow の形式をとります。
※Hosted UI は、Amazon Cognito が提供するウェブベースのログイン画面です。ユーザーはここで認証情報を入力し、認証を行います。Hosted UI を使用することで、Amazon Cognito がユーザー管理や認証のフローをすべて管理してくれるため、開発者は認証に関するコードを書く必要がありません。

ここまでをまとめると、以下のようなフローとなります。

ユーザープールの作成

OP としての設定は以下のとおりです。
AWS マネジメントコンソールより、Cognito を選択、"ユーザープールを作成"を選択します。
作成にあたり、以下の点を押さえて作成します。

  • 必須属性として"preferred_username"を追加。

  • Hosted UI(Cognito のホストされた UI を使用)を選択します。

    • Amazon Cognito 側で用意されている認証画面を使用できます。
  • アプリケーションクライアントの設定は以下のとおりとします。

    • アプリケーションタイプは"秘密クライアント"で作成する(クライアントのシークレットを生成する)。
    • 許可されているコールバック URL:https://{ALB の DNS 名}/login/oauth2/code/oidc
    • 許可されているサインアウト URL:https://{ALB の DNS 名}
    • OAuth 2.0 許可タイプ:"認証コード付与"を選択。
    • OpenID Connect のスコープ:以下全ての項目にチェックします。
      • aws.cognito.signin.user.admin
      • email
      • openid
      • phone
      • profile
  • JHipster の権限に合わせ、2 つのグループ(ROLE_ADMIN、ROLE_USER)を作成します。

  • 検証用にユーザーを作成し、email、preferred_username を設定、グループへ割り当てます。

  • email、preferred_username は JHipster の管理テーブル上、一意になる値で設定します。

JHipster アプリケーション側の設定

RP としての設定は以下のとおりです。
冒頭で述べたように JHipster は Spring Security を活用しており、更に認証処理に関するコードや他の認証サービスとの連携に必要なコードも自動生成してくれます。
よって基本的には設定ファイル(YAML)の記述のみで、認証処理を簡単に実現することができます。
ただし、OP を JHipster のデフォルトである Keycloak、Okta ではなく、Amazon Cognito ユーザープールを利用する場合は、若干のコード修正も必要になります。

application.yml の設定

src/main/resources/config/application.yml の該当箇所を以下のように設定します。
※以下、"AWS マネジメントコンソール"を"マネコン"と表記します。

  • issuer-uri
    • https://cognito-idp.REGION.amazonaws.com/{ユーザープール ID}を設定します。
    • ユーザープール ID は、ユーザープールのユーザープール ID(マネコンから参照可能)を設定してください。
  • client-id
    • ユーザープールのアプリケーションクライアントより、クライアント ID(マネコンから参照可能)を設定します。
  • client-secret
    • ユーザープールのアプリケーションクライアントより、クライアントシークレット(マネコンから参照可能)を設定します。
  • scope
    • ユーザープールで指定したスコープと合わせて、phone, email, openid, aws.cognito.signin.user.admin, profile を設定します。
  • redirect-uri
    • https://{ALB の DNS 名}/login/oauth2/code/oidc を設定します。
    • ユーザープールで設定した"許可されているコールバック URL"と同じものを設定します。

以下設定イメージとなります。

security:
  oauth2:
    client:
      provider:
        oidc:
          issuer-uri: https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXX
      registration:
        oidc:
          client-id: XXXXXXXXXXXX
          client-secret: XXXXXXXXXXXXX
          scope: phone, email, openid, aws.cognito.signin.user.admin, profile
          redirect-uri: https://XXXXXXX.ap-northeast-1.elb.amazonaws.com/login/oauth2/code/oidc

また、src/main/resources/config/application-prod.ymlの datasource の設定も、AWS 上の DB(PostgreSQL)向けに設定します。

datasource:
  type: com.zaxxer.hikari.HikariDataSource
  url: jdbc:postgresql://XXXXXXXX.ap-northeast-1.rds.amazonaws.com:5432/XXXXXX
  username: XXXXXX
  password: XXXXXX
  hikari:
    poolName: Hikari
    auto-commit: false

コードの修正

権限

Amazon Cognito でのロールは cognito:groups 属性に格納されているため、 JHipster がトークンからロールを抽出する際に、Amazon Cognito に合わせた修正が必要となります。
以下、修正例となります。

  • src/main/java/com/mycompany/myapp/config/SecurityConfiguration.java

以下修正メソッドの抜粋となります。

    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapper() {
        return authorities -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

            authorities.forEach(authority -> {
                // Check for OidcUserAuthority because Spring Security 5.2 returns
                // each scope as a GrantedAuthority, which we don't care about.
                if (authority instanceof OidcUserAuthority) {
                    OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
                    mappedAuthorities.addAll(SecurityUtils.extractAuthorityFromClaims(oidcUserAuthority.getAttributes()));
                }
            });
            return mappedAuthorities;
        };
    }
  • src/main/java/com/mycompany/myapp/security/SecurityUtils.java

以下修正メソッドの抜粋となります。

    @SuppressWarnings("unchecked")
    private static Collection<String> getRolesFromClaims(Map<String, Object> claims) {
        return (Collection<String>) claims.getOrDefault("cognito:groups", new ArrayList<>());
    }

ログアウト

ログアウト時の logoutUrl が okta、keycloak 用に生成されている({ログアウトエンドポイント}?id_token_hint=XXXXX&post_logout_redirect_uri=XXXXX の形式)ため、これを Amazon Cognito 向けに修正します。 以下修正例となります。

  • src/main/java/com/mycompany/myapp/web/rest/LogoutResource.java
package com.mycompany.myapp.web.rest;

import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * REST controller for managing global OIDC logout.
 */
@RestController
public class LogoutResource {

    private final ClientRegistration registration;

    public LogoutResource(ClientRegistrationRepository registrations) {
        this.registration = registrations.findByRegistrationId("oidc");
    }

    /**
     * {@code POST  /api/logout} : logout the current user.
     *
     * @param request the {@link HttpServletRequest}.
     * @return the {@link ResponseEntity} with status {@code 200 (OK)} and a body with a global logout URL.
     */
    @PostMapping("/api/logout")
    public ResponseEntity<?> logout(HttpServletRequest request) {
        StringBuilder logoutUrl = new StringBuilder();

        //"Amazon Cognitoのドメイン名/logout"を設定、ドメイン名はマネコンから参照可能
        logoutUrl.append("https://XXXXX.auth.REGION.amazoncognito.com/logout");

        String originUrl = request.getHeader(HttpHeaders.ORIGIN);

        logoutUrl.append("?client_id=").append(registration.getClientId()).append("&logout_uri=").append(originUrl);

        request.getSession().invalidate();
        return ResponseEntity.ok().body(Map.of("logoutUrl", logoutUrl.toString()));
    }
}

動作確認

上記設定、修正を実施後、AWS 上に JHipster アプリケーションをデプロイし、動作確認を行います。 ビルドについては、公式サイトを参考に実施し、Docker イメージを Amazon Elastic Container Registry (Amazon ECR) へプッシュ、ECS サービスを起動します。
ECR へのプッシュ、ECS サービスの構築方法については割愛します。

JHipster アプリケーションにアクセスし、トップ画面からログインします。

Amazon Cognito の認証画面が表示されるので、サインインします。

JHipster でのログインが可能となります。

最後に

ユーザープールと連携し、Amazon Cognito 側の認証画面を通して JHipster のユーザー認証を行いました。 クラウドサービスとして AWS を対象にしましたが、今後は Azure など他のクラウドサービスについても検証していく予定です。

JHipster 関連記事

参考文献


TOP
アルファロゴ 株式会社アルファシステムズは、ITサービス事業を展開しています。このブログでは、技術的な取り組みを紹介しています。X(旧Twitter)で更新通知をしています。