多要素認証

多要素認証 (MFA) [OWASP] (英語) では、ユーザーが認証のために要素を提供する必要があります。OWASP では、要素を以下のカテゴリに分類しています。

  • ユーザーが知っていること (例: パスワード)

  • ユーザーが持っているもの (例: SMS またはメールへのアクセス)

  • 何か (例: 生体認証)

  • いるどこか (例: 地理位置情報)

  • すること (例: 行動プロファイリング)

FactorGrantedAuthority

Spring Security の認証メカニズムは、認証時に FactorGrantedAuthority (Javadoc) を追加します。たとえば、ユーザーがパスワード認証を行うと、FactorGrantedAuthority.PASSWORD_AUTHORITY の authority を含む FactorGrantedAuthority が Authentication に自動的に追加されます。Spring Security で MFA を必須にするには、以下の手順が必要です。

  • 複数の要素を必要とする認可ルールを指定する

  • 各要素の認証を設定する

@EnableMultiFactorAuthentication

@EnableMultiFactorAuthentication (Javadoc) を使えば、多要素認証を簡単に有効化できます。以下に、すべての認証ルールにパスワードと OTT の両方の要件を追加する設定例を示します。

  • Java

  • Kotlin

@EnableMultiFactorAuthentication(authorities = {
	FactorGrantedAuthority.PASSWORD_AUTHORITY,
	FactorGrantedAuthority.OTT_AUTHORITY })
@EnableMultiFactorAuthentication( authorities = [
    FactorGrantedAuthority.PASSWORD_AUTHORITY,
    FactorGrantedAuthority.OTT_AUTHORITY])

常に複数の要素を必要とする構成を簡潔に作成できるようになりました。

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests((authorize) -> authorize
			(1)
			.requestMatchers("/admin/**").hasRole("ADMIN")
			(2)
			.anyRequest().authenticated()
		)
		(3)
		.formLogin(Customizer.withDefaults())
		.oneTimeTokenLogin(Customizer.withDefaults());
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            (1)
            authorize("/admin/**", hasRole("ADMIN"))
            (2)
            authorize(anyRequest, authenticated)
        }
        (3)
        formLogin { }
        oneTimeTokenLogin {  }
    }
    return http.build()
}
1/admin/** で始まる URL には、権限 FACTOR_OTTFACTOR_PASSWORDROLE_ADMIN が必要です。
2 その他の URL には、FACTOR_OTTFACTOR_PASSWORD の権限が必要です。
3 必要な要素を提供できる認証メカニズムを設定します。

Spring Security は、どの権限が不足しているかに応じて、どのエンドポイントにアクセスするかを認識しています。ユーザーが最初にユーザー名とパスワードでログインした場合、Spring Security はワンタイムトークンログインページにリダイレクトします。ユーザーが最初にトークンでログインした場合、Spring Security はユーザー名 / パスワードログインページにリダイレクトします。

WebAuthn ユーザーに対して条件付きで MFA を要求する

場合によっては、WebAuthn 認証情報 (パスキー) を登録したユーザーのみに条件付きで MFA を要求したい場合があります。これは、EnableMultiFactorAuthentication.when() (Javadoc) を指定することで実現できます。

  • Java

  • Kotlin

@EnableMultiFactorAuthentication(
	authorities = {
		FactorGrantedAuthority.PASSWORD_AUTHORITY,
		FactorGrantedAuthority.WEBAUTHN_AUTHORITY
	},
	when = MultiFactorCondition.WEBAUTHN_REGISTERED
)
public class WebAuthnConditionConfiguration {

	@Bean
	public PublicKeyCredentialUserEntityRepository userEntityRepository() {
		return new MapPublicKeyCredentialUserEntityRepository();
	}

	@Bean
	public UserCredentialRepository userCredentialRepository() {
		return new MapUserCredentialRepository();
	}

}
@EnableMultiFactorAuthentication(
    authorities = [
        FactorGrantedAuthority.PASSWORD_AUTHORITY,
        FactorGrantedAuthority.WEBAUTHN_AUTHORITY
    ],
    `when` = [MultiFactorCondition.WEBAUTHN_REGISTERED]
)
internal class WebAuthnConditionConfiguration {

    @Bean
    fun userEntityRepository(): PublicKeyCredentialUserEntityRepository {
        return MapPublicKeyCredentialUserEntityRepository()
    }

    @Bean
    fun userCredentialRepository(): UserCredentialRepository {
        return MapUserCredentialRepository()
    }

}

この構成では、ユーザーがパスキーを登録している場合にのみ、FACTOR_WEBAUTHN と FACTOR_PASSWORD が必要となります。これは、条件に基づいて withWhen メソッドを更新する Customizer<AdditionalRequiredFactorsBuilder<Object>> を発行することで機能します。

この条件を満たすには、ユーザーが WebAuthn 認証情報を登録したかどうかを判断するために、PublicKeyCredentialUserEntityRepository (Javadoc) Bean と UserCredentialRepository (Javadoc) Bean の両方が公開されている必要があります。

カスタム MFA 条件

@EnableMultiFactorAuthentication によって作成されたファクトリをカスタマイズするために、1 つ以上の Customizer<AdditionalRequiredFactorsBuilder<Object>> Bean を公開することもできます。例: 特定のユーザーに対して条件付きで MFA を適用できます。

  • Java

  • Kotlin

@Bean
Customizer<AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder<Object>> additionalRequiredFactorsCustomizer() {
	return (builder) -> builder.when((auth) -> "admin".equals(auth.getName()));
}
@Bean
fun additionalRequiredFactorsCustomizer(): Customizer<AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder<Any>> {
    return Customizer { builder -> builder.`when` { auth -> "admin" == auth.name } }
}

AuthorizationManagerFactory

@EnableMultiFactorAuthentication authorities プロパティは、AuthorizationManagerFactory (Javadoc) Bean を公開するためのショートカットです。AuthorizationManagerFactory Bean が利用可能な場合、Spring Security はこれを使用して、AuthorizationManagerFactory Bean インターフェースで定義された hasAnyRole(String) などの認可ルールを作成します。@EnableMultiFactorAuthentication によって公開される実装により、各認可は指定された要素を持つという要件と組み合わされます。

以下の AuthorizationManagerFactory Bean は、前述の @EnableMultiFactorAuthentication の例で公開されているものです。

  • Java

  • Kotlin

@Bean
AuthorizationManagerFactory<Object> authz() {
	return AuthorizationManagerFactories.multiFactor()
		.requireFactors(
			FactorGrantedAuthority.PASSWORD_AUTHORITY,
			FactorGrantedAuthority.OTT_AUTHORITY
		)
		.build();
}
@Bean
fun authz(): AuthorizationManagerFactory<Any> {
    return AuthorizationManagerFactories.multiFactor<Any>()
        .requireFactors(
            FactorGrantedAuthority.PASSWORD_AUTHORITY,
            FactorGrantedAuthority.OTT_AUTHORITY
        )
        .build()
}

選択的に MFA を要求する

@EnableMultiFactorAuthentications authorities プロパティを使用して、アプリケーション全体で MFA を必須にする方法を説明しました。ただし、アプリケーションの一部のみに MFA を必須にしたい場合もあります。以下の要件を考慮してください。

  • /admin/ で始まる URL には、権限 FACTOR_OTTFACTOR_PASSWORDROLE_ADMIN が必要です。

  • /user/settings で始まる URL には、FACTOR_OTTFACTOR_PASSWORD の権限が必要です。

  • その他の URL では認証されたユーザーが必要です

この場合、一部の URL では MFA が必須ですが、他の URL では必須ではありません。これは、前述のグローバルアプローチが機能しないことを意味します。幸いなことに、AuthorizationManagerFactory で学んだことを活用することで、この問題を簡潔に解決できます。

まず、権限のない @EnableMultiFactorAuthentication を指定します。これにより MFA サポートが有効になりますが、AuthorizationManagerFactory (Bean)は公開されません。

  • Java

  • Kotlin

@EnableMultiFactorAuthentication(authorities = {})
@EnableMultiFactorAuthentication(authorities = [])

次に、AuthorizationManagerFactory インスタンスを作成しますが、Bean として公開しないでください。

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	(1)
	var mfa = AuthorizationManagerFactories.multiFactor()
		.requireFactors(
			FactorGrantedAuthority.PASSWORD_AUTHORITY,
			FactorGrantedAuthority.OTT_AUTHORITY
		)
		.build();
	http
		.authorizeHttpRequests((authorize) -> authorize
			(2)
			.requestMatchers("/admin/**").access(mfa.hasRole("ADMIN"))
			(3)
			.requestMatchers("/user/settings/**").access(mfa.authenticated())
			(4)
			.anyRequest().authenticated()
		)
		(5)
		.formLogin(Customizer.withDefaults())
		.oneTimeTokenLogin(Customizer.withDefaults());
	return http.build();
}
@Bean
@Throws(Exception::class)
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    (1)
    val mfa = AuthorizationManagerFactories.multiFactor<Any>()
        .requireFactors(
            FactorGrantedAuthority.PASSWORD_AUTHORITY,
            FactorGrantedAuthority.OTT_AUTHORITY
        )
        .build()
    http {
        authorizeHttpRequests {
            (2)
            authorize("/admin/**", mfa.hasRole("ADMIN"))
            (3)
            authorize("/user/settings/**", mfa.authenticated())
            (4)
            authorize(anyRequest, authenticated)
        }
        (5)
        formLogin { }
        oneTimeTokenLogin {  }
    }
    return http.build()
}
1 以前と同様に DefaultAuthorizationManagerFactory を作成しますが、Bean としては公開しません。Bean として公開しないことで、すべての認可ルールで AuthorizationManagerFactory を使用するのではなく、選択的に AuthorizationManagerFactory を使用できるようになります。
2/admin/** で始まる URL には FACTOR_OTTFACTOR_PASSWORDROLE_ADMIN が必要となるように、明示的に AuthorizationManagerFactory を使用します。
3AuthorizationManagerFactory を明示的に使用して、/user/settings で始まる URL には FACTOR_OTT と FACTOR_PASSWORD が必要となるようにします。
4 それ以外の場合、リクエストは認証される必要があります。AuthorizationManagerFactory は使用されないため、MFA は必要ありません。
5 必要な要素を提供できる認証メカニズムを設定します。

有効な期間の指定

場合によっては、最近認証したかどうかに基づいて認可ルールを定義したいことがあります。たとえば、アプリケーションでは、/user/settings エンドポイントへのアクセスを認可するために、ユーザーが過去 1 時間以内に認証されていることを要求する場合があります。

認証時に、FactorGrantedAuthority が Authentication に追加されることを覚えておいてください。FactorGrantedAuthority は issuedAt であった時期を指定しますが、有効期間は指定しません。これは意図的なものであり、単一の FactorGrantedAuthority を複数の validDuration と組み合わせて使用できるようにするためのものです。

次の要件を満たす方法を示す例を見てみましょう。

  • /admin/ で始まる URL では、過去 30 分以内にパスワードを入力する必要があります。

  • /user/settings の URL では、過去 1 時間以内にパスワードを入力する必要があります。

  • それ以外の場合は認証が必要ですが、それがパスワードであるかどうかや、認証がいつ行われたかは関係ありません。

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	(1)
	var passwordIn30m = AuthorizationManagerFactories.multiFactor()
		.requireFactor( (factor) -> factor
			.passwordAuthority()
			.validDuration(Duration.ofMinutes(30))
		)
		.build();
	(2)
	var passwordInHour = AuthorizationManagerFactories.multiFactor()
		.requireFactor( (factor) -> factor
			.passwordAuthority()
			.validDuration(Duration.ofHours(1))
		)
		.build();
	http
		.authorizeHttpRequests((authorize) -> authorize
			(3)
			.requestMatchers("/admin/**").access(passwordIn30m.hasRole("ADMIN"))
			(4)
			.requestMatchers("/user/settings/**").access(passwordInHour.authenticated())
			(5)
			.anyRequest().authenticated()
		)
		(6)
		.formLogin(Customizer.withDefaults());
	return http.build();
}
@Bean
@Throws(Exception::class)
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    (1)
    val passwordIn30m = AuthorizationManagerFactories.multiFactor<Any>()
        .requireFactor( { factor -> factor
            .passwordAuthority()
            .validDuration(Duration.ofMinutes(30))
        })
        .build()
    (2)
    val passwordInHour = AuthorizationManagerFactories.multiFactor<Any>()
        .requireFactor( { factor -> factor
            .passwordAuthority()
            .validDuration(Duration.ofHours(1))
        })
        .build()
    http {
        authorizeHttpRequests {
            (3)
            authorize("/admin/**", passwordIn30m.hasRole("ADMIN"))
            (4)
            authorize("/user/settings/**", passwordInHour.authenticated())
            (5)
            authorize(anyRequest, authenticated)
        }
        (6)
        formLogin { }
    }
    return http.build()
}
1 まず、passwordIn30m を 30 分以内のパスワードの要件として定義します。
2 次に、passwordInHour を 1 時間以内にパスワードを入力するための要件として定義します。
3passwordIn30m を使用して、/admin/ で始まる URL では、過去 30 分以内にパスワードが提供され、ユーザーが ROLE_ADMIN 権限を持っていることを要求するようにします。
4passwordInHour を使用して、/user/settings で始まる URL では、過去 1 時間以内にパスワードが提供されていることを要求するようにします。
5 それ以外の場合は認証が必要ですが、それがパスワードであるかどうかや、認証がいつ行われたかは関係ありません。
6 必要な要素を提供できる認証メカニズムを設定します。

AllRequiredFactorsAuthorizationManager.anyOf

前述の例では、アクセスにはユーザーがすべての認証要素を満たしていることが必要でした。しかし、アプリケーションによっては、ユーザーが複数の認証要素の組み合わせのうちいずれか 1 つを満たせばアクセスを許可したい場合があります。AllRequiredFactorsAuthorizationManager.anyOf (Javadoc) は、指定された認証要素の組み合わせのうち少なくとも 1 つが満たされればアクセスを許可します。

ユーザーが WebAuthn のみで認証できる場合、またはパスワードとワンタイムトークンの両方で認証できる場合を考えてみましょう。

  • Java

  • Kotlin

@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
	(1)
	AllRequiredFactorsAuthorizationManager<Object> webauthn = AllRequiredFactorsAuthorizationManager
			.<Object>builder()
			.requireFactor((factor) -> factor.webauthnAuthority())
			.build();
	(2)
	AllRequiredFactorsAuthorizationManager<Object> passwordAndOtt = AllRequiredFactorsAuthorizationManager
			.<Object>builder()
			.requireFactor((factor) -> factor.passwordAuthority())
			.requireFactor((factor) -> factor.ottAuthority())
			.build();
	(3)
	DefaultAuthorizationManagerFactory<Object> mfa = new DefaultAuthorizationManagerFactory<>();
	mfa.setAdditionalAuthorization(AllRequiredFactorsAuthorizationManager.anyOf(webauthn, passwordAndOtt));
	http
		.authorizeHttpRequests((authorize) -> authorize
			(4)
			.requestMatchers("/protected/**").access(mfa.authenticated())
			(5)
			.anyRequest().authenticated()
		)
		(6)
		.formLogin(Customizer.withDefaults())
		.oneTimeTokenLogin(Customizer.withDefaults())
		.webAuthn((webAuthn) -> webAuthn
			.rpName("Spring Security")
			.rpId("example.com")
			.allowedOrigins("https://example.com")
		);
	return http.build();
}
@Bean
@Throws(Exception::class)
fun springSecurity(http: HttpSecurity): SecurityFilterChain? {
	(1)
	val webauthn = AllRequiredFactorsAuthorizationManager.builder<Any>()
		.requireFactor { factor -> factor.webauthnAuthority() }
		.build()
	(2)
	val passwordAndOtt = AllRequiredFactorsAuthorizationManager.builder<Any>()
		.requireFactor { factor -> factor.passwordAuthority() }
		.requireFactor { factor -> factor.ottAuthority() }
		.build()
	(3)
	val mfa = DefaultAuthorizationManagerFactory<Any>()
	mfa.setAdditionalAuthorization(AllRequiredFactorsAuthorizationManager.anyOf(webauthn, passwordAndOtt))
	http {
		authorizeHttpRequests {
			(4)
			authorize("/protected/**", mfa.authenticated())
			(5)
			authorize(anyRequest, authenticated)
		}
		(6)
		formLogin { }
		oneTimeTokenLogin { }
		webAuthn {
			rpName = "Spring Security"
			rpId = "example.com"
			allowedOrigins = setOf("https://example.com")
		}
	}
	return http.build()
}
1WebAuthn が必要です
2 パスワードとワンタイムトークンの両方が必要です
3 要因の組み合わせを anyOf と組み合わせ、いずれかの条件が満たされた場合にアクセスを許可します。
4/protected/** で始まる URL では、ユーザーは以下のいずれかの条件を満たす必要があります。
5 その他のリクエストには認証のみが必要です
6 必要な要素を提供できる認証メカニズムを設定する

プログラムによる MFA

これまでの例では、MFA はリクエストごとに静的に決定されていました。しかし、一部のユーザーには MFA を必須とし、他のユーザーには必須としない場合もあります。ユーザーごとに MFA を有効にするかどうかは、Authentication に基づいて条件付きで要素をリクエストする AuthorizationManagerFactories.multiFactor().when を使用することで実現できます。これは ConditionalAuthorizationManager を使用して実装されます。

条件付き MFA ルールをグローバルに有効にするには、AuthorizationManagerFactory Bean を公開します。

  • Java

  • Kotlin

@Bean
AuthorizationManagerFactory<Object> authorizationManagerFactory() {
	(3)
	return AuthorizationManagerFactories.multiFactor()
		(1)
		.requireFactors(FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY)
		(2)
		.when((auth) -> "admin".equals(auth.getName()))
		.build();
}
@Bean
fun authorizationManagerFactory(): AuthorizationManagerFactory<Any> {
    (3)
    return AuthorizationManagerFactories.multiFactor<Any>()
        (1)
        .requireFactors(FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY)
        (2)
        .`when` { auth -> "admin" == auth.name }
        .build()
}
1FACTOR_OTT と FACTOR_PASSWORD が必要です
2 ユーザー名が admin の場合のみ、この要件が適用されます。それ以外の場合は、MFA は不要です。
3.build() を使用して AuthorizationManagerFactory を返します。Bean として公開されているため、グローバルに使用されます。

これは、AuthorizationManagerFactory の前の例では非常によく似ているはずです。違いは、前の例では AuthorizationManagerFactories が常に同じ権限を必要とする AuthorizationManager を作成する点です。

これで、AuthorizationManagerFactory と組み合わせた認可ルールを定義できるようになりました。

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests((authorize) -> authorize
			(1)
			.requestMatchers("/admin/**").hasRole("ADMIN")
			(2)
			.anyRequest().authenticated()
		)
		.formLogin(Customizer.withDefaults())
		.oneTimeTokenLogin(Customizer.withDefaults());
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            (1)
            authorize("/admin/**", hasRole("ADMIN"))
            (2)
            authorize(anyRequest, authenticated)
        }
        formLogin { }
        oneTimeTokenLogin { }
    }
    return http.build()
}
1/admin/** で始まる URL には ROLE_ADMIN が必要です。ユーザー名が admin の場合は、FACTOR_OTT と FACTOR_PASSWORD も必要です。
2 それ以外の場合、リクエストは認証される必要があります。ユーザー名が admin の場合、FACTOR_OTT と FACTOR_PASSWORD も必要です。
MFA はロールではなくユーザー名で有効化されます。これは、RequiredAuthoritiesAuthorizationManagerConfiguration の実装方法がロールに基づいているためです。必要に応じて、ユーザー名ではなくロールに基づいて MFA を有効化するようにロジックを変更することも可能です。

RequiredAuthoritiesAuthorizationManager

AuthorizationManagerFactories.multiFactor().when を使用してプログラムによる MFA で特定のユーザーの権限を動的に決定する方法を実証しました。しかし、これは非常に一般的なシナリオであるため、Spring Security では RequiredAuthoritiesAuthorizationManager (Javadoc) RequiredAuthoritiesRepository (Javadoc) を使用して組み込みのサポートを提供しています。

組み込みサポートを使用して、プログラムによる MFA で行ったのと同じ要件を実装してみましょう。

まず、使用する RequiredAuthoritiesAuthorizationManager Bean を作成します。

  • Java

  • Kotlin

@Bean
RequiredAuthoritiesAuthorizationManager<Object> adminAuthorization() {
	(1)
	MapRequiredAuthoritiesRepository authorities = new MapRequiredAuthoritiesRepository();
	authorities.saveRequiredAuthorities("admin", List.of(
		FactorGrantedAuthority.PASSWORD_AUTHORITY,
		FactorGrantedAuthority.OTT_AUTHORITY)
	);
	(2)
	return new RequiredAuthoritiesAuthorizationManager<>(authorities);
}
@Bean
fun adminAuthorization(): RequiredAuthoritiesAuthorizationManager<Any> {
    (1)
    val authorities = MapRequiredAuthoritiesRepository()
    authorities.saveRequiredAuthorities("admin", listOf(
        FactorGrantedAuthority.PASSWORD_AUTHORITY,
        FactorGrantedAuthority.OTT_AUTHORITY)
    )
    (2)
    return RequiredAuthoritiesAuthorizationManager(authorities)
}
1 ユーザー名が admin のユーザーをマッピングして MFA を要求する MapRequiredAuthoritiesRepository (Javadoc) を作成します。
2MapRequiredAuthoritiesRepository が注入された RequiredAuthoritiesAuthorizationManager を返します。

次に、RequiredAuthoritiesAuthorizationManager を使用する AuthorizationManagerFactory を定義します。

  • Java

  • Kotlin

@Bean
AuthorizationManagerFactory<Object> authorizationManagerFactory(
		RequiredAuthoritiesAuthorizationManager admins) {
	DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>();
	(1)
	defaults.setAdditionalAuthorization(admins);
	(2)
	return defaults;
}
@Bean
fun authorizationManagerFactory(admins: RequiredAuthoritiesAuthorizationManager<Any>): AuthorizationManagerFactory<Any> {
    val defaults = DefaultAuthorizationManagerFactory<Any>()
    (1)
    defaults.setAdditionalAuthorization(admins)
    (2)
    return defaults
}
1RequiredAuthoritiesAuthorizationManager を DefaultAuthorization.additionalAuthorization (Javadoc) として挿入します。これにより、DefaultAuthorizationManagerFactory は、すべての認可ルールにおいて、アプリケーションで定義された認可要件(例: hasRole("ADMIN"))に加えて、RequiredAuthoritiesAuthorizationManager を適用するように指示されます。
2DefaultAuthorizationManagerFactory を Bean として公開し、世界中で使用されるようにする

これで、RequiredAuthoritiesAuthorizationManager と組み合わせた認可ルールを定義できるようになりました。

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests((authorize) -> authorize
			.requestMatchers("/admin/**").hasRole("ADMIN") (1)
			.anyRequest().authenticated() (2)
		)
		.formLogin(Customizer.withDefaults())
		.oneTimeTokenLogin(Customizer.withDefaults());
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            authorize("/admin/**", hasRole("ADMIN")) (1)
            authorize(anyRequest, authenticated) (2)
        }
        formLogin { }
        oneTimeTokenLogin { }
    }
    return http.build()
}
1/admin/** で始まる URL には ROLE_ADMIN が必要です。ユーザー名が admin の場合は、FACTOR_OTT と FACTOR_PASSWORD も必要です。
2 それ以外の場合、リクエストは認証される必要があります。ユーザー名が admin の場合、FACTOR_OTT と FACTOR_PASSWORD も必要です。

この例では、ユーザー名と追加の必要な権限をメモリ内でマッピングしています。ユーザー名によって判断できるより動的なユースケースについては、RequiredAuthoritiesRepository (Javadoc) のカスタム実装を作成できます。考えられる例としては、ユーザーが明示的な設定で MFA を有効にしているかどうかを確認したり、ユーザーがパスキーを登録しているかどうかを判断したりすることが挙げられます。

Authentication に基づいて MFA を決定する必要があるケースでは、プログラムによる MFA で示されているように AuthorizationManagerFactories.multiFactor().when を使用できます。

hasAllAuthorities の使用

MFA をサポートするための追加インフラストラクチャを多数紹介しました。ただし、シンプルな MFA のユースケースでは、hasAllAuthorities を使用して複数の要素を要求するのが効果的です。

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests((authorize) -> authorize
			(1)
			.anyRequest().hasAllAuthorities(
				FactorGrantedAuthority.PASSWORD_AUTHORITY,
				FactorGrantedAuthority.OTT_AUTHORITY
			)
		)
		(2)
		.formLogin(Customizer.withDefaults())
		.oneTimeTokenLogin(Customizer.withDefaults());
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            (1)
            authorize(anyRequest, hasAllAuthorities(
                FactorGrantedAuthority.PASSWORD_AUTHORITY,
                FactorGrantedAuthority.OTT_AUTHORITY
            ))
        }
        (2)
        formLogin { }
        oneTimeTokenLogin {  }
    }
    return http.build()
}
1 すべてのリクエストに FACTOR_PASSWORD と FACTOR_OTT をリクエストする
2 必要な要素を提供できる認証メカニズムを設定します。

上記の設定は、最もシンプルなユースケースでのみ適切に機能します。エンドポイントが多数ある場合は、すべての認可ルールで MFA の要件を繰り返すのは避けた方が良いでしょう。

例: 次の構成を検討してください。

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests((authorize) -> authorize
			(1)
			.requestMatchers("/admin/**").hasAllAuthorities(
				"ROLE_ADMIN",
				FactorGrantedAuthority.PASSWORD_AUTHORITY,
				FactorGrantedAuthority.OTT_AUTHORITY
			)
			(2)
			.anyRequest().hasAllAuthorities(
				"ROLE_USER",
				FactorGrantedAuthority.PASSWORD_AUTHORITY,
				FactorGrantedAuthority.OTT_AUTHORITY
			)
		)
		(3)
		.formLogin(Customizer.withDefaults())
		.oneTimeTokenLogin(Customizer.withDefaults());
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            (1)
            authorize("/admin/**", hasAllAuthorities(
                "ROLE_ADMIN",
                FactorGrantedAuthority.PASSWORD_AUTHORITY,
                FactorGrantedAuthority.OTT_AUTHORITY
            ))
            (2)
            authorize(anyRequest, hasAllAuthorities(
                "ROLE_USER",
                FactorGrantedAuthority.PASSWORD_AUTHORITY,
                FactorGrantedAuthority.OTT_AUTHORITY
            ))
        }
        (3)
        formLogin { }
        oneTimeTokenLogin {  }
    }
    return http.build()
}
1/admin/** で始まる URL の場合、次の権限が必要です FACTOR_OTTFACTOR_PASSWORDROLE_ADMIN
2 その他のすべての URL については、次の権限が必要です FACTOR_OTTFACTOR_PASSWORDROLE_USER
3 必要な要素を提供できる認証メカニズムを設定します。

この設定では 2 つの認可ルールしか指定されていませんが、重複が望ましくないことは十分に理解できます。このようなルールを何百も宣言するとどうなるか想像できますか?

さらに、より複雑な認可ルールを表現することが難しくなります。例: 2 つの要素と ROLE_ADMIN または ROLE_USER のいずれかを要求するにはどうすればよいでしょうか。

これらの質問への答えは、すでに見たように、@EnableMultiFactorAuthentication を使用することです。

再認証

最も一般的なのは再認証です。次のように構成されたアプリケーションを想像してみてください。

  • Java

  • Kotlin

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
		.formLogin(Customizer.withDefaults())
		.oneTimeTokenLogin(Customizer.withDefaults());
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            authorize(anyRequest, authenticated)
        }
        formLogin { }
        oneTimeTokenLogin { }
    }
    return http.build()
}

デフォルトでは、このアプリケーションには 2 つの認証メカニズムが許可されており、ユーザーはどちらかを使用して完全に認証できます。

特定の要素を必要とするエンドポイントのセットがある場合は、次のように authorizeHttpRequests で指定できます。

  • Java

  • Kotlin

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests((authorize) -> authorize
			.requestMatchers("/profile/**").hasAuthority(FactorGrantedAuthority.OTT_AUTHORITY) (1)
			.anyRequest().authenticated()
		)
		.formLogin(Customizer.withDefaults())
		.oneTimeTokenLogin(Customizer.withDefaults());
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            authorize("/profile/**", hasAuthority(FactorGrantedAuthority.OTT_AUTHORITY)) (1)
            authorize(anyRequest, authenticated)
        }
        formLogin { }
        oneTimeTokenLogin { }
    }
    return http.build()
}
1 すべての /profile/** エンドポイントは、承認のためにワンタイムトークンログインを必要とすることを規定しています。

上記の設定により、ユーザーはサポートされている任意のメカニズムでログインできます。また、プロフィールページにアクセスしたい場合、Spring Security はワンタイムトークンのログインページにリダイレクトし、トークンを取得します。

このように、ユーザーに付与される権限は、付与された証明の量に正比例します。この適応型アプローチにより、ユーザーは意図した操作を実行するために必要な証明のみを付与できます。

追加のスコープの承認

例外処理を構成して、不足しているスコープを取得する方法を Spring Security に指示することもできます。

特定のエンドポイントに対して特定の OAuth 2.0 スコープを必要とするアプリケーションを考えてみましょう。

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests((authorize) -> authorize
			.requestMatchers("/profile/**").hasAuthority("SCOPE_profile:read")
			.anyRequest().authenticated()
		)
		.x509(Customizer.withDefaults())
		.oauth2Login(Customizer.withDefaults());
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            authorize("/profile/**", hasAuthority("SCOPE_profile:read"))
            authorize(anyRequest, authenticated)
        }
        x509 { }
        oauth2Login { }
    }
    return http.build()
}

これも次のように AuthorizationManagerFactory Bean で構成されている場合:

  • Java

  • Kotlin

@Bean
AuthorizationManagerFactory<Object> authz() {
	return AuthorizationManagerFactories.multiFactor()
			.requireFactors(FactorGrantedAuthority.X509_AUTHORITY, FactorGrantedAuthority.AUTHORIZATION_CODE_AUTHORITY)
			.build();
}
@Bean
fun authz(): AuthorizationManagerFactory<Any> {
    return AuthorizationManagerFactories.multiFactor<Any>()
            .requireFactors(
                FactorGrantedAuthority.X509_AUTHORITY,
                FactorGrantedAuthority.AUTHORIZATION_CODE_AUTHORITY
            )
            .build()
}

次に、アプリケーションには、X.509 証明書と、OAuth 2.0 認証サーバーからの認証が必要になります。

ユーザーが profile:read に同意しない場合、このアプリケーションは現状では 403 を発行します。ただし、アプリケーションが再度同意を求める方法がある場合は、次のように AuthenticationEntryPoint でこれを実装できます。

  • Java

  • Kotlin

@Component
class ScopeRetrievingAuthenticationEntryPoint implements AuthenticationEntryPoint {
	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
			throws IOException, ServletException {
		response.sendRedirect("https://authz.example.org/authorize?scope=profile:read");
	}
}
@Component
internal class ScopeRetrievingAuthenticationEntryPoint : AuthenticationEntryPoint {
    override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) {
        response.sendRedirect("https://authz.example.org/authorize?scope=profile:read")
    }
}

次に、フィルターチェーン宣言で、このエントリポイントを次のように特定のオーソリティにバインドできます。

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, ScopeRetrievingAuthenticationEntryPoint oauth2) throws Exception {
	http
		.authorizeHttpRequests((authorize) -> authorize
			.requestMatchers("/profile/**").hasAuthority("SCOPE_profile:read")
			.anyRequest().authenticated()
		)
		.x509(Customizer.withDefaults())
		.oauth2Login(Customizer.withDefaults())
		.exceptionHandling((exceptions) -> exceptions
			.defaultDeniedHandlerForMissingAuthority(oauth2, "SCOPE_profile:read")
		);
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity, oauth2: ScopeRetrievingAuthenticationEntryPoint): DefaultSecurityFilterChain? {
    http {
        authorizeHttpRequests {
            authorize("/profile/**", hasAuthority("SCOPE_profile:read"))
            authorize(anyRequest, authenticated)
        }
        x509 { }
        oauth2Login { }
    }

    http.exceptionHandling { e: ExceptionHandlingConfigurer<HttpSecurity> -> e
        .defaultDeniedHandlerForMissingAuthority(oauth2, "SCOPE_profile:read")
    }
    return http.build()
}