1. 概要

このチュートリアルでは、SpringセキュリティSAMLOktaをIDプロバイダー(IdP)として使用する方法について説明します。

2. SAMLとは何ですか?

Security Assertion Markup Language(SAML)は、IdPがユーザーの認証と承認の詳細をサービスプロバイダー(SP)に安全に送信できるようにするオープンスタンダードです。 IdPとSP間の通信にXMLベースのメッセージを使用します。

つまり、ユーザーがサービスにアクセスしようとすると、IdPを使用してログインする必要があります。 ログインすると、IdPは承認と認証の詳細を含むSAML属性をXML形式でSPに送信します。

安全な認証送信メカニズムを提供するほかに、 SAMLはシングルサインオン(SSO)も促進し、ユーザーが一度ログインして同じ資格情報を再利用して他のサービスプロバイダーにログインできるようにします。

3. OktaSAMLセットアップ

まず、前提条件として、Okta開発者アカウントを設定する必要があります。

3.1. 新しいアプリケーションを作成する

次に、SAML2.0をサポートする新しいWebアプリケーション統合を作成します。

次に、アプリ名やアプリのロゴなどの一般的な情報を入力します。

3.2. SAML統合の編集

このステップでは、SSOURLやオーディエンスURIなどのSAML設定を提供します。

最後に、統合に関するフィードバックを提供できます。

3.3. セットアップ手順を表示する

完了すると、SpringBootAppのセットアップ手順を表示できます。

注:SpringSecurity構成でさらに必要となるIdP発行者URLやIdPメタデータXMLなどの手順をコピーする必要があります。

4. スプリングブートセットアップ

spring-boot-starter-webspring-boot-starter-security、などの通常のMaven依存関係以外に、spring-が必要になります。 security-saml2-core 依存関係:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.6.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.6.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.security.extensions</groupId>
    <artifactId>spring-security-saml2-core</artifactId>
    <version>1.0.10.RELEASE</version>
</dependency>

また、Shibbolethリポジトリを追加して、spring-security-saml2-core依存関係に必要な最新のopensamljarをダウンロードしてください。

<repository>
    <id>Shibboleth</id>
    <name>Shibboleth</name>
    <url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
</repository>

または、Gradleプロジェクトで依存関係を設定することもできます。

compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: "2.5.1" 
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: "2.5.1"
compile group: 'org.springframework.security.extensions', name: 'spring-security-saml2-core', version: "1.0.10.RELEASE"

5. Springセキュリティ構成

Okta SAMLセットアップとSpring Bootプロジェクトの準備ができたので、SAML2.0とOktaの統合に必要なSpringセキュリティ構成から始めましょう。

5.1. SAMLエントリポイント

まず、SAML認証のエントリポイントとして機能するSAMLEntryPointクラスのbeanを作成します。

@Bean
public WebSSOProfileOptions defaultWebSSOProfileOptions() {
    WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
    webSSOProfileOptions.setIncludeScoping(false);
    return webSSOProfileOptions;
}

@Bean
public SAMLEntryPoint samlEntryPoint() {
    SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
    samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions());
    return samlEntryPoint;
}

ここで、 WebSSOProfileOptions Beanを使用すると、SPからIdPに送信されてユーザー認証を要求する要求のパラメーターを設定できます。

5.2. ログインとログアウト

次に、/ Discovery、 / login 、/ logoutなどのSAMLURI用のフィルターをいくつか作成しましょう。

@Bean
public FilterChainProxy samlFilter() throws Exception {
    List<SecurityFilterChain> chains = new ArrayList<>();
    chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
        samlWebSSOProcessingFilter()));
    chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"),
        samlDiscovery()));
    chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
        samlEntryPoint));
    chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
        samlLogoutFilter));
    chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
        samlLogoutProcessingFilter));
    return new FilterChainProxy(chains);
}

次に、対応するフィルターとハンドラーをいくつか追加します。

@Bean
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
    SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
    samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
    samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
    samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
    return samlWebSSOProcessingFilter;
}

@Bean
public SAMLDiscovery samlDiscovery() {
    SAMLDiscovery idpDiscovery = new SAMLDiscovery();
    return idpDiscovery;
}

@Bean
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
    SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    successRedirectHandler.setDefaultTargetUrl("/home");
    return successRedirectHandler;
}

@Bean
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
    SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
    failureHandler.setUseForward(true);
    failureHandler.setDefaultFailureUrl("/error");
    return failureHandler;
}

これまで、認証用のエントリポイント( samlEntryPoint )といくつかのフィルターチェーンを構成しました。 それでは、それらの詳細を深く掘り下げてみましょう。

ユーザーが初めてログインしようとすると、samlEntryPointがエントリ要求を処理します。 次に、 samlDiscovery Bean(有効な場合)は、認証のために接続するIdPを検出します。

次に、ユーザーがログインすると、IdPはSAML応答を/saml / sso URIにリダイレクトしてを処理し、対応するsamlWebSSOProcessingFilterは関連する認証トークンを認証します。

成功すると、 successRedirectHandler はユーザーをデフォルトのターゲットURL( / home )にリダイレクトします。 それ以外の場合、authenticationFailureHandlerはユーザーを/errorURLにリダイレクトします。

最後に、シングルおよびグローバルログアウト用のログアウトハンドラーを追加しましょう。

@Bean
public SimpleUrlLogoutSuccessHandler successLogoutHandler() {
    SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
    successLogoutHandler.setDefaultTargetUrl("/");
    return successLogoutHandler;
}

@Bean
public SecurityContextLogoutHandler logoutHandler() {
    SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
    logoutHandler.setInvalidateHttpSession(true);
    logoutHandler.setClearAuthentication(true);
    return logoutHandler;
}

@Bean
public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
    return new SAMLLogoutProcessingFilter(successLogoutHandler(), logoutHandler());
}

@Bean
public SAMLLogoutFilter samlLogoutFilter() {
    return new SAMLLogoutFilter(successLogoutHandler(),
        new LogoutHandler[] { logoutHandler() },
        new LogoutHandler[] { logoutHandler() });
}

5.3. メタデータ処理

次に、IdPメタデータXMLをSPに提供します。 ユーザーがログインしたときにリダイレクトするSPエンドポイントをIdPに知らせると役立ちます。

したがって、 MetadataGenerator beanを構成して、SpringSAMLがメタデータを処理できるようにします。

public MetadataGenerator metadataGenerator() {
    MetadataGenerator metadataGenerator = new MetadataGenerator();
    metadataGenerator.setEntityId(samlAudience);
    metadataGenerator.setExtendedMetadata(extendedMetadata());
    metadataGenerator.setIncludeDiscoveryExtension(false);
    metadataGenerator.setKeyManager(keyManager());
    return metadataGenerator;
}

@Bean
public MetadataGeneratorFilter metadataGeneratorFilter() {
    return new MetadataGeneratorFilter(metadataGenerator());
}

@Bean
public ExtendedMetadata extendedMetadata() {
    ExtendedMetadata extendedMetadata = new ExtendedMetadata();
    extendedMetadata.setIdpDiscoveryEnabled(false);
    return extendedMetadata;
}

MetadataGenerator Beanには、SPとIdP間の交換を暗号化するためにKeyManagerのインスタンスが必要です。

@Bean
public KeyManager keyManager() {
    DefaultResourceLoader loader = new DefaultResourceLoader();
    Resource storeFile = loader.getResource(samlKeystoreLocation);
    Map<String, String> passwords = new HashMap<>();
    passwords.put(samlKeystoreAlias, samlKeystorePassword);
    return new JKSKeyManager(storeFile, samlKeystorePassword, passwords, samlKeystoreAlias);
}

ここでは、キーストアを作成してKeyManagerBeanに提供する必要があります。 JREコマンドを使用して、自己署名キーとキーストアを作成できます。

keytool -genkeypair -alias baeldungspringsaml -keypass baeldungsamlokta -keystore saml-keystore.jks

5.4. MetadataManager

次に、 ExtendedMetadataDelegate インスタンスを使用して、IdPメタデータをSpring Bootアプリケーションに構成します。

@Bean
@Qualifier("okta")
public ExtendedMetadataDelegate oktaExtendedMetadataProvider() throws MetadataProviderException {
    org.opensaml.util.resource.Resource resource = null
    try {
        resource = new ClasspathResource("/saml/metadata/sso.xml");
    } catch (ResourceException e) {
        e.printStackTrace();
    }
    Timer timer = new Timer("saml-metadata")
    ResourceBackedMetadataProvider provider = new ResourceBackedMetadataProvider(timer,resource);
    provider.setParserPool(parserPool());
    return new ExtendedMetadataDelegate(provider, extendedMetadata());
}

@Bean
@Qualifier("metadata")
public CachingMetadataManager metadata() throws MetadataProviderException, ResourceException {
    List<MetadataProvider> providers = new ArrayList<>(); 
    providers.add(oktaExtendedMetadataProvider());
    CachingMetadataManager metadataManager = new CachingMetadataManager(providers);
    metadataManager.setDefaultIDP(defaultIdp);
    return metadataManager;
}

ここでは、セットアップ手順を表示しながらOkta開発者アカウントからコピーされたIdPメタデータXMLを含むsso.xmlファイルのメタデータを解析しました。

同様に、 defaultIdp 変数には、Okta開発者アカウントからコピーされたIdP発行者のURLが含まれています。

5.5. XML解析

XML解析には、StaticBasicParserPoolクラスのインスタンスを使用できます。

@Bean(initMethod = "initialize")
public StaticBasicParserPool parserPool() {
    return new StaticBasicParserPool();
}

@Bean(name = "parserPoolHolder")
public ParserPoolHolder parserPoolHolder() {
    return new ParserPoolHolder();
}

5.6. SAMLプロセッサ

次に、HTTPリクエストからのSAMLメッセージを解析するプロセッサが必要です。

@Bean
public HTTPPostBinding httpPostBinding() {
    return new HTTPPostBinding(parserPool(), VelocityFactory.getEngine());
}

@Bean
public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() {
    return new HTTPRedirectDeflateBinding(parserPool());
}

@Bean
public SAMLProcessorImpl processor() {
    ArrayList<SAMLBinding> bindings = new ArrayList<>();
    bindings.add(httpRedirectDeflateBinding());
    bindings.add(httpPostBinding());
    return new SAMLProcessorImpl(bindings);
}

ここでは、Okta開発者アカウントの構成に関してPOSTおよびリダイレクトバインディングを使用しました。

5.7. SAMLAuthenticationProviderの実装

最後に、 SAMLAuthenticationProvider クラスのカスタム実装が必要で、 ExpiringUsernameAuthenticationToken クラスのインスタンスをチェックし、取得した権限を設定します。

public class CustomSAMLAuthenticationProvider extends SAMLAuthenticationProvider {
    @Override
    public Collection<? extends GrantedAuthority> getEntitlements(SAMLCredential credential, Object userDetail) {
        if (userDetail instanceof ExpiringUsernameAuthenticationToken) {
            List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
            authorities.addAll(((ExpiringUsernameAuthenticationToken) userDetail).getAuthorities());
            return authorities;
        } else {
            return Collections.emptyList();
        }
    }
}

また、CustomSAMLAuthenticationProviderSecurityConfigクラスのBeanとして構成する必要があります。

@Bean
public SAMLAuthenticationProvider samlAuthenticationProvider() {
    return new CustomSAMLAuthenticationProvider();
}

5.8. SecurityConfig

最後に、すでに説明したsamlEntryPointおよびsamlFilterを使用して、基本的なHTTPセキュリティを構成します。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();

    http.httpBasic().authenticationEntryPoint(samlEntryPoint);

    http
      .addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
      .addFilterAfter(samlFilter(), BasicAuthenticationFilter.class)
      .addFilterBefore(samlFilter(), CsrfFilter.class);

    http
      .authorizeRequests()
      .antMatchers("/").permitAll()
      .anyRequest().authenticated();

    http
      .logout()
      .addLogoutHandler((request, response, authentication) -> {
          response.sendRedirect("/saml/logout");
      });
}

出来上がり! ユーザーがIdPにログインし、IdPからXML形式でユーザーの認証の詳細を受信できるようにするSpringセキュリティSAML構成を完了しました。 最後に、ユーザートークンを認証して、Webアプリへのアクセスを許可します。

6. HomeController

Spring Security SAML構成とOkta開発者アカウントのセットアップの準備ができたので、シンプルなコントローラーをセットアップしてランディングページとホームページを提供できます。

6.1. インデックスと認証のマッピング

まず、デフォルトのターゲットURI (/)および/ authURIにマッピングを追加しましょう。

@RequestMapping("/")
public String index() {
    return "index";
}

@GetMapping(value = "/auth")
public String handleSamlAuth() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth != null) {
        return "redirect:/home";
    } else {
        return "/";
    }
}

次に、ユーザーがloginリンクを使用してOktaSAML認証をリダイレクトできるようにする単純なindex.htmlを追加します。

<!doctype html>
<html>
<head>
<title>Baeldung Spring Security SAML</title>
</head>
<body>
    <h3><Strong>Welcome to Baeldung Spring Security SAML</strong></h3>
    <a th:href="@{/auth}">Login</a>
</body>
</html>

これで、Spring Bootアプリを実行し、 http:// localhost:8080/でアクセスする準備が整いました。

ログインリンクをクリックすると、Oktaサインインページが開きます。

6.2. ホームページ

次に、マッピングを / home URIに追加して、認証に成功したときにユーザーをリダイレクトしましょう。

@RequestMapping("/home")
public String home(Model model) {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    model.addAttribute("username", authentication.getPrincipal());
    return "home";
}

また、 home.html を追加して、ログインしているユーザーとログアウトリンクを表示します。

<!doctype html>
<html>
<head>
<title>Baeldung Spring Security SAML: Home</title>
</head>
<body>
    <h3><Strong>Welcome!</strong><br/>You are successfully logged in!</h3>
    <p>You are logged as <span th:text="${username}">null</span>.</p>
    <small>
        <a th:href="@{/logout}">Logout</a>
    </small>
</body>
</html>

正常にログインすると、ホームページが表示されます。

7. 結論

このチュートリアルでは、SpringSecuritySAMLとOktaの統合について説明しました。

まず、SAML2.0Web統合を使用してOkta開発者アカウントを設定します。 次に、必要なMaven依存関係を持つSpringBootプロジェクトを作成しました。

次に、samlEntryPoint、samlFilter、メタデータ処理、SAMLプロセッサなど、SpringSecuritySAMLに必要なすべてのセットアップを実行しました。

最後に、コントローラーと、indexhomeなどのいくつかのページを作成して、OktaとのSAML統合をテストしました。

いつものように、ソースコードはGitHubから入手できます。