最新の安定バージョンについては、Spring Security 6.4.5 を使用してください! |
SAML 2.0 ログインの概要
SAML 2.0 証明書利用者認証が Spring Security 内でどのように機能するかを見てみましょう。まず、OAuth 2.0 ログインと同様に、Spring Security はユーザーをサードパーティに誘導して認証を実行することがわかります。これは、一連のリダイレクトを通じて行われます。
上の図は、SecurityFilterChain
および AbstractAuthenticationProcessingFilter
図を基にしています。
最初に、ユーザーが認可されていないリソース /private
に対して認証されていないリクエストを行います。
Spring Security の FilterSecurityInterceptor
は、認証されていないリクエストが AccessDeniedException
をスローすることによって拒否されたことを示します。
ユーザーに認可がないため、ExceptionTranslationFilter
は認証開始を開始します。構成された AuthenticationEntryPoint
は、<saml2:AuthnRequest>
生成エンドポイントSaml2WebSsoAuthenticationRequestFilter
にリダイレクトする LoginUrlAuthenticationEntryPoint
(Javadoc) のインスタンスです。または、複数のアサートパーティを設定した場合は、最初にピッカーページにリダイレクトされます。
次に、Saml2WebSsoAuthenticationRequestFilter
は、構成された Saml2AuthenticationRequestFactory
を使用して <saml2:AuthnRequest>
を作成、署名、シリアライズ、エンコードします。
次に、ブラウザーはこの <saml2:AuthnRequest>
を受け取り、アサーティングパーティに提示します。アサーティングパーティは、ユーザーの認証を試みます。成功すると、ブラウザーに <saml2:Response>
が返されます。
次に、ブラウザーは <saml2:Response>
をアサーションコンシューマーサービスエンドポイントに POST します。
<saml2:Response>
の認証 この図は、SecurityFilterChain
ダイアグラムから構築されています。
ブラウザーが <saml2:Response>
をアプリケーションに送信すると、ブラウザーは Saml2WebSsoAuthenticationFilter
に委譲します。このフィルターは、構成された AuthenticationConverter
を呼び出して、HttpServletRequest
からレスポンスを抽出することによって Saml2AuthenticationToken
を作成します。このコンバーターはさらに RelyingPartyRegistration
を解決し、Saml2AuthenticationToken
に供給します。
次に、フィルターはトークンを構成済みの AuthenticationManager
に渡します。デフォルトでは、OpenSAML authentication provider
を使用します。
認証に失敗した場合、Failure
SecurityContextHolder
はクリアされます。AuthenticationEntryPoint
が呼び出され、認証プロセスが再開されます。
認証が成功した場合は、Success .
Authentication
はSecurityContextHolder
に設定されます。Saml2WebSsoAuthenticationFilter
はFilterChain#doFilter(request,response)
を呼び出して、残りのアプリケーションロジックを続行します。
最小限の依存関係
SAML 2.0 サービスプロバイダーのサポートは spring-security-saml2-service-provider
にあります。これは OpenSAML ライブラリから構築されているため、ビルド構成に Shibboleth Maven リポジトリも含める必要があります。別のリポジトリが必要な理由の詳細については、このリンク (英語) を確認してください。
Maven
<repositories>
<!-- ... -->
<repository>
<id>shibboleth-releases</id>
<url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
</repository>
</repositories>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
repositories {
// ...
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
}
dependencies {
// ...
implementation 'org.springframework.security:spring-security-saml2-service-provider'
}
最小構成
Spring Boot を使用する場合、アプリケーションをサービスプロバイダーとして設定するには、2 つの基本的な手順があります。最初に、必要な依存関係を含め、次に必要なアサーティングパーティのメタデータを示します。
また、これは、すでにあなたの主張する当事者に依拠当事者を登録していることを前提としています。 |
ID プロバイダーのメタデータの指定
Spring Boot アプリケーションで ID プロバイダーのメタデータを指定するには、次のようにします。
spring:
security:
saml2:
relyingparty:
registration:
adfs:
identityprovider:
entity-id: https://idp.example.com/issuer
verification.credentials:
- certificate-location: "classpath:idp.crt"
singlesignon.url: https://idp.example.com/issuer/sso
singlesignon.sign-request: false
where
idp.example.com/issuer (英語)
は、ID プロバイダーが発行する SAML レスポンスのIssuer
属性に含まれる値ですclasspath:idp.crt
は、SAML レスポンスを検証するための ID プロバイダーの証明書のクラスパス上の場所です。idp.example.com/issuer/sso (英語)
は、ID プロバイダーがAuthnRequest
を予期しているエンドポイントです。adfs
は、選択した任意の識別子です。
以上です!
ID プロバイダーとアサーティングパーティはシノニムであり、サービスプロバイダと依存パーティも同義です。これらは、それぞれ AP および RP と略されることがよくあります。 |
ランタイムの期待
上で設定したように、アプリケーションは SAMLResponse
パラメーターを含む POST /login/saml2/sso/{registrationId}
リクエストを処理します。
POST /login/saml2/sso/adfs HTTP/1.1
SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
アサーティングパーティが SAMLResponse
を生成するように誘導するには、2 つの方法があります。
まず、アサーティングパーティに移動できます。おそらく、
SAMLResponse
を送信するためにクリックできる、登録済みの証明書利用者ごとに何らかのリンクまたはボタンがあります。次に、アプリ内の保護されたページ(
localhost:8080
など)に移動できます。次に、アプリは構成されたアサーティングパーティーにリダイレクトし、アサーティングパーティーがSAMLResponse
を送信します。
ここから、次へのジャンプを検討してください。
SAML 2.0 ログインが OpenSAML と統合する方法
Spring Security の SAML 2.0 サポートには、いくつかの設計ゴールがあります。
まず、SAML 2.0 操作とドメインオブジェクトのライブラリに依存します。これを達成するために、Spring Security は OpenSAML を使用します。
次に、Spring Security の SAML サポートを使用するときにこのライブラリが不要であることを確認します。これを実現するために、Spring Security が契約で OpenSAML を使用するすべてのインターフェースまたはクラスは、カプセル化されたままです。これにより、OpenSAML を他のいくつかのライブラリ、サポートされていないバージョンの OpenSAML に切り替えることができます。
上記の 2 つのゴールの当然の結果として、Spring Security の SAML API は他のモジュールに比べてかなり小さいです。代わりに、OpenSaml4AuthenticationRequestFactory
や OpenSaml4AuthenticationProvider
などのクラスは、認証プロセスのさまざまなステップをカスタマイズする Converter
を公開します。
例: アプリケーションが SAMLResponse
を受け取り、Saml2WebSsoAuthenticationFilter
に委譲すると、フィルターは OpenSaml4AuthenticationProvider
に委譲します。
下位互換性のために、Spring Security はデフォルトで最新の OpenSAML3 を使用します。OpenSAML 3 はサポートが終了したため、OpenSAML 4.x に更新することをお勧めします。そのため、Spring Security は OpenSAML3.x および 4.x の両方をサポートしています。OpenSAML の 4.x への依存関係を管理する場合、Spring Security は OpenSAML4.x 実装を選択します。 |
Response
の認証 この図は、Saml2WebSsoAuthenticationFilter
図に基づいています。
Saml2WebSsoAuthenticationFilter
は Saml2AuthenticationToken
を作成し、AuthenticationManager
を呼び出します。
AuthenticationManager
は、OpenSAML 認証プロバイダーを呼び出します。
認証プロバイダーは、レスポンスを OpenSAML Response
に逆直列化し、その署名をチェックします。署名が無効な場合、認証は失敗します。
次に、プロバイダーは EncryptedAssertion
要素を復号化します。復号化が失敗すると、認証は失敗します。
次に、プロバイダーはレスポンスの Issuer
および Destination
値を検証します。それらが RelyingPartyRegistration
の内容と一致しない場合、認証は失敗します。
その後、プロバイダーは各 Assertion
の署名を検証します。いずれかの署名が無効な場合、認証は失敗します。また、レスポンスにもアサーションにも署名がない場合、認証は失敗します。レスポンスまたはすべてのアサーションに署名が必要です。
次に、プロバイダーは、EncryptedID
または EncryptedAttribute
要素を復号化します。]。復号化が失敗すると、認証は失敗します。
次に、プロバイダは各アサーションの ExpiresAt
および NotBefore
タイムスタンプ、<Subject>
およびすべての <AudienceRestriction>
条件を検証します。いずれかの検証が失敗すると、認証は失敗します。
その後、プロバイダーは最初のアサーションの AttributeStatement
を取得し、それを Map<String, List<Object>>
にマップします。また、ROLE_USER
付与権限も付与します。
そして最後に、最初のアサーションから NameID
、属性の Map
、GrantedAuthority
を取得し、Saml2AuthenticatedPrincipal
を構築します。次に、そのプリンシパルと権限を Saml2Authentication
に配置します。
結果の Authentication#getPrincipal
は Spring Security Saml2AuthenticatedPrincipal
オブジェクトであり、Authentication#getName
は最初のアサーションの NameID
要素にマップされます。Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId
は、関連付けられた RelyingPartyRegistration
の識別子を保持します。
OpenSAML 構成のカスタマイズ
次のように、Spring Security と OpenSAML の両方を使用するクラスは、クラスの先頭で OpenSamlInitializationService
を静的に初期化する必要があります。
Java
Kotlin
static {
OpenSamlInitializationService.initialize();
}
companion object {
init {
OpenSamlInitializationService.initialize()
}
}
これは OpenSAML の InitializationService#initialize
に代わるものです。
OpenSAML が SAML オブジェクトを構築、マーシャリング、アンマーシャリングする方法をカスタマイズすることが役立つ場合があります。このような状況では、代わりに OpenSAML の XMLObjectProviderFactory
へのアクセスを提供する OpenSamlInitializationService#requireInitialize(Consumer)
を呼び出すことができます。
例: 署名されていない AuthNRequest を送信する場合、再認証を強制することができます。その場合、次のように独自の AuthnRequestMarshaller
を登録できます。
Java
Kotlin
static {
OpenSamlInitializationService.requireInitialize(factory -> {
AuthnRequestMarshaller marshaller = new AuthnRequestMarshaller() {
@Override
public Element marshall(XMLObject object, Element element) throws MarshallingException {
configureAuthnRequest((AuthnRequest) object);
return super.marshall(object, element);
}
public Element marshall(XMLObject object, Document document) throws MarshallingException {
configureAuthnRequest((AuthnRequest) object);
return super.marshall(object, document);
}
private void configureAuthnRequest(AuthnRequest authnRequest) {
authnRequest.setForceAuthn(true);
}
}
factory.getMarshallerFactory().registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller);
});
}
companion object {
init {
OpenSamlInitializationService.requireInitialize {
val marshaller = object : AuthnRequestMarshaller() {
override fun marshall(xmlObject: XMLObject, element: Element): Element {
configureAuthnRequest(xmlObject as AuthnRequest)
return super.marshall(xmlObject, element)
}
override fun marshall(xmlObject: XMLObject, document: Document): Element {
configureAuthnRequest(xmlObject as AuthnRequest)
return super.marshall(xmlObject, document)
}
private fun configureAuthnRequest(authnRequest: AuthnRequest) {
authnRequest.isForceAuthn = true
}
}
it.marshallerFactory.registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller)
}
}
}
requireInitialize
メソッドは、アプリケーションインスタンスごとに 1 回だけ呼び出すことができます。
Boot 自動構成のオーバーライドまたは置換
Spring Boot が依存パーティ用に生成する 2 つの @Bean
があります。
1 つ目は、アプリを証明書利用者として構成する SecurityFilterChain
です。spring-security-saml2-service-provider
を含めると、SecurityFilterChain
は次のようになります。
Java
Kotlin
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.saml2Login(withDefaults());
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
saml2Login { }
}
return http.build()
}
アプリケーションが SecurityFilterChain
Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。
これを置き換えるには、アプリケーション内で Bean を公開します。
Java
Kotlin
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/messages/**").hasAuthority("ROLE_USER")
.anyRequest().authenticated()
)
.saml2Login(withDefaults());
return http.build();
}
}
@EnableWebSecurity
class MyCustomSecurityConfiguration {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize("/messages/**", hasAuthority("ROLE_USER"))
authorize(anyRequest, authenticated)
}
saml2Login {
}
}
return http.build()
}
}
上記では、/messages/
で始まる URL の USER
のロールが必要です。
2 番目の @Bean
Spring Boot が作成する RelyingPartyRegistrationRepository
(Javadoc) は、アサーティングパーティと依存パーティのメタデータを表します。これには、アサーティングパーティに認証をリクエストするときに依存パーティが使用する必要がある SSO エンドポイントの場所などが含まれます。
独自の RelyingPartyRegistrationRepository
Bean を公開することにより、デフォルトを上書きできます。例: 次のようにメタデータエンドポイントにアクセスすることにより、アサーティングパーティの構成を検索できます。
Java
Kotlin
@Value("${metadata.location}")
String assertingPartyMetadataLocation;
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation(assertingPartyMetadataLocation)
.registrationId("example")
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Value("\${metadata.location}")
var assertingPartyMetadataLocation: String? = null
@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
val registration = RelyingPartyRegistrations
.fromMetadataLocation(assertingPartyMetadataLocation)
.registrationId("example")
.build()
return InMemoryRelyingPartyRegistrationRepository(registration)
}
registrationId は、登録を区別するために選択する任意の値です。 |
または、以下に示すように、各詳細を手動で提供できます。
Java
Kotlin
@Value("${verification.key}")
File verificationKey;
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
X509Certificate certificate = X509Support.decodeCertificate(this.verificationKey);
Saml2X509Credential credential = Saml2X509Credential.verification(certificate);
RelyingPartyRegistration registration = RelyingPartyRegistration
.withRegistrationId("example")
.assertingPartyDetails(party -> party
.entityId("https://idp.example.com/issuer")
.singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
.wantAuthnRequestsSigned(false)
.verificationX509Credentials(c -> c.add(credential))
)
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Value("\${verification.key}")
var verificationKey: File? = null
@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository {
val certificate: X509Certificate? = X509Support.decodeCertificate(verificationKey!!)
val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate)
val registration = RelyingPartyRegistration
.withRegistrationId("example")
.assertingPartyDetails { party: AssertingPartyDetails.Builder ->
party
.entityId("https://idp.example.com/issuer")
.singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
.wantAuthnRequestsSigned(false)
.verificationX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
c.add(
credential
)
}
}
.build()
return InMemoryRelyingPartyRegistrationRepository(registration)
}
X509Support は OpenSAML クラスであり、簡潔にするためにスニペットで使用されていることに注意してください |
または、DSL を使用してリポジトリを直接接続することもできます。これにより、自動構成された SecurityFilterChain
もオーバーライドされます。
Java
Kotlin
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/messages/**").hasAuthority("ROLE_USER")
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.relyingPartyRegistrationRepository(relyingPartyRegistrations())
);
return http.build();
}
}
@EnableWebSecurity
class MyCustomSecurityConfiguration {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize("/messages/**", hasAuthority("ROLE_USER"))
authorize(anyRequest, authenticated)
}
saml2Login {
relyingPartyRegistrationRepository = relyingPartyRegistrations()
}
}
return http.build()
}
}
RelyingPartyRegistrationRepository に複数の証明書利用者を登録することにより、証明書利用者をマルチテナントにすることができます。 |
RelyingPartyRegistration
RelyingPartyRegistration
(Javadoc) インスタンスは、依存パーティとアサーティングパーティのメタデータ間のリンクを表します。
RelyingPartyRegistration
では、Issuer
値のような依存パーティメタデータを提供できます。このメタデータは、SAML レスポンスが送信されることを期待し、ペイロードに署名または復号化するために所有する資格情報を提供します。
また、AuthnRequests の送信先となる Issuer
値などのアサーティングパーティのメタデータと、依存パーティがペイロードを検証または暗号化する目的で所有するパブリック資格情報を提供できます。
次の RelyingPartyRegistration
は、ほとんどのセットアップに最低限必要なものです。
Java
Kotlin
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
.fromMetadataLocation("https://ap.example.org/metadata")
.registrationId("my-id")
.build();
val relyingPartyRegistration = RelyingPartyRegistrations
.fromMetadataLocation("https://ap.example.org/metadata")
.registrationId("my-id")
.build()
任意の InputStream
ソースから RelyingPartyRegistration
を作成することもできることに注意してください。そのような例の 1 つは、メタデータがデータベースに保存されている場合です。
String xml = fromDatabase();
try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
.fromMetadata(source)
.registrationId("my-id")
.build();
}
次のように、より高度な設定も可能です。
Java
Kotlin
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
.entityId("{baseUrl}/{registrationId}")
.decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
.assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
.assertingPartyDetails(party -> party
.entityId("https://ap.example.org")
.verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
.singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
)
.build();
val relyingPartyRegistration =
RelyingPartyRegistration.withRegistrationId("my-id")
.entityId("{baseUrl}/{registrationId}")
.decryptionX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
c.add(relyingPartyDecryptingCredential())
}
.assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
.assertingPartyDetails { party -> party
.entityId("https://ap.example.org")
.verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
.singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
}
.build()
最上位のメタデータメソッドは、証明書利用者に関する詳細です。assertingPartyDetails 内のメソッドは、アサーティングパーティに関する詳細です。 |
依存パーティが SAML レスポンスを期待している場所は、アサーションコンシューマーサービスの場所です。 |
証明書利用者の entityId
のデフォルトは {baseUrl}/saml2/service-provider-metadata/{registrationId}
です。これは、アサーティングパーティが依存パーティについて知るように設定するときに必要なこの値です。
assertionConsumerServiceLocation
のデフォルトは /login/saml2/sso/{registrationId}
です。デフォルトでは、フィルターチェーンの Saml2WebSsoAuthenticationFilter
にマッピングされます。
URI パターン
上記の例で、{baseUrl}
および {registrationId}
プレースホルダーに気付いたと思います。
これらは、URI の生成に役立ちます。そのため、依存パーティの entityId
および assertionConsumerServiceLocation
は、次のプレースホルダーをサポートしています。
baseUrl
- デプロイされたアプリケーションのスキーム、ホスト、ポートregistrationId
- この依存パーティの登録 IDbaseScheme
- デプロイされたアプリケーションのスキームbaseHost
- デプロイされたアプリケーションのホストbasePort
- デプロイされたアプリケーションのポート
例: 上記で定義された assertionConsumerServiceLocation
は次のとおりです。
/my-login-endpoint/{registrationId}
デプロイされたアプリケーションではこれに変換されます
/my-login-endpoint/adfs
上記の entityId
は次のように定義されました。
{baseUrl}/{registrationId}
デプロイされたアプリケーションではこれに変換されます
https://rp.example.com/adfs
一般的な URI パターンは次のとおりです。
/saml2/authenticate/{registrationId}
- そのRelyingPartyRegistration
の構成に基づいて<saml2:AuthnRequest>
を生成し、それをアサーティングパーティに送信するエンドポイント/saml2/login/sso/{registrationId}
-RelyingPartyRegistration
の構成に基づいてアサーティングパーティの<saml2:Response>
を認証するエンドポイント/saml2/logout/sso
-<saml2:LogoutRequest>
および<saml2:LogoutResponse>
ペイロードを処理するエンドポイント。RelyingPartyRegistration
は、以前に認証された状態から検索されます/saml2/saml2-service-provider/metadata/{registrationId}
- そのRelyingPartyRegistration
の証明書利用者メタデータ
registrationId
は RelyingPartyRegistration
の主要な識別子であるため、認証されていないシナリオの URL で必要になります。何らかの理由で URL から registrationId
を削除したい場合は、RelyingPartyRegistrationResolver
を指定して Spring Security に registrationId
を検索する方法を伝えることができます。
資格情報
また、使用された認証情報にも気づいたでしょう。
多くの場合、証明書利用者は、同じキーを使用してペイロードに署名し、ペイロードを復号化します。または、同じキーを使用してペイロードを検証し、暗号化します。
このため、Spring Security には Saml2X509Credential
が同梱されており、これは SAML 固有の認証情報であり、さまざまなユースケースで同じキーを簡単に構成できます。
少なくとも、アサーティングパーティの署名されたレスポンスを検証できるように、アサーティングパーティからの証明書が必要です。
アサーティングパーティからのアサーションを検証するために使用する Saml2X509Credential
を構築するには、ファイルをロードして次のように CertificateFactory
を使用できます。
Java
Kotlin
Resource resource = new ClassPathResource("ap.crt");
try (InputStream is = resource.getInputStream()) {
X509Certificate certificate = (X509Certificate)
CertificateFactory.getInstance("X.509").generateCertificate(is);
return Saml2X509Credential.verification(certificate);
}
val resource = ClassPathResource("ap.crt")
resource.inputStream.use {
return Saml2X509Credential.verification(
CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
)
}
アサーティングパーティがアサーションも暗号化するとします。その場合、依存パーティは、暗号化された値を復号化できるようにするための秘密鍵を必要とします。
その場合、RSAPrivateKey
とそれに対応する X509Certificate
が必要になります。1 つ目は Spring Security の RsaKeyConverters
ユーティリティクラスを使用して、2 つ目は以前と同様にロードできます。
Java
Kotlin
X509Certificate certificate = relyingPartyDecryptionCertificate();
Resource resource = new ClassPathResource("rp.crt");
try (InputStream is = resource.getInputStream()) {
RSAPrivateKey rsa = RsaKeyConverters.pkcs8().convert(is);
return Saml2X509Credential.decryption(rsa, certificate);
}
val certificate: X509Certificate = relyingPartyDecryptionCertificate()
val resource = ClassPathResource("rp.crt")
resource.inputStream.use {
val rsa: RSAPrivateKey = RsaKeyConverters.pkcs8().convert(it)
return Saml2X509Credential.decryption(rsa, certificate)
}
これらのファイルの場所を適切な Spring Boot プロパティとして指定すると、Spring Boot がこれらの変換を実行します。 |
証明書利用者構成の複製
アプリケーションが複数のアサーティングパーティを使用する場合、いくつかの構成が RelyingPartyRegistration
インスタンス間で複製されます。
依存パーティの
entityId
その
assertionConsumerServiceLocation
、およびその署名、たとえば署名または復号化の証明書
この設定の良いところは、一部の ID プロバイダーと他の ID プロバイダーで資格情報を簡単にローテーションできることです。
重複はいくつかの方法で軽減できます。
まず、YAML では、次のように参照でこれを軽減できます。
spring:
security:
saml2:
relyingparty:
okta:
signing.credentials: &relying-party-credentials
- private-key-location: classpath:rp.key
certificate-location: classpath:rp.crt
identityprovider:
entity-id: ...
azure:
signing.credentials: *relying-party-credentials
identityprovider:
entity-id: ...
次に、データベースでは、RelyingPartyRegistration
のモデルを複製する必要はありません。
3 番目に、Java では、次のようにカスタム構成メソッドを作成できます。
Java
Kotlin
private RelyingPartyRegistration.Builder
addRelyingPartyDetails(RelyingPartyRegistration.Builder builder) {
Saml2X509Credential signingCredential = ...
builder.signingX509Credentials(c -> c.addAll(signingCredential));
// ... other relying party configurations
}
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
RelyingPartyRegistration okta = addRelyingPartyDetails(
RelyingPartyRegistrations
.fromMetadataLocation(oktaMetadataUrl)
.registrationId("okta")).build();
RelyingPartyRegistration azure = addRelyingPartyDetails(
RelyingPartyRegistrations
.fromMetadataLocation(oktaMetadataUrl)
.registrationId("azure")).build();
return new InMemoryRelyingPartyRegistrationRepository(okta, azure);
}
private fun addRelyingPartyDetails(builder: RelyingPartyRegistration.Builder): RelyingPartyRegistration.Builder {
val signingCredential: Saml2X509Credential = ...
builder.signingX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
c.add(
signingCredential
)
}
// ... other relying party configurations
}
@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
val okta = addRelyingPartyDetails(
RelyingPartyRegistrations
.fromMetadataLocation(oktaMetadataUrl)
.registrationId("okta")
).build()
val azure = addRelyingPartyDetails(
RelyingPartyRegistrations
.fromMetadataLocation(oktaMetadataUrl)
.registrationId("azure")
).build()
return InMemoryRelyingPartyRegistrationRepository(okta, azure)
}
リクエストからの RelyingPartyRegistration
の解決
これまで見てきたように、Spring Security は URI パスで登録 ID を探すことによって RelyingPartyRegistration
を解決します。
カスタマイズする理由はいくつかあります。その中で:
多くのアサーティングパーティをフェデレーションしている可能性があります
RelyingPartyRegistration
の解決方法をカスタマイズするために、カスタム RelyingPartyRegistrationResolver
を構成できます。デフォルトでは、URI の最後のパス要素から登録 ID を検索し、RelyingPartyRegistrationRepository
で検索します。
RelyingPartyRegistration にプレースホルダーがある場合は、リゾルバー実装が解決する必要があることに注意してください。 |
単一の一貫した RelyingPartyRegistration
への解決
たとえば、常に同じ RelyingPartyRegistration
を返すリゾルバーを提供できます。
Java
Kotlin
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
private final RelyingPartyRegistrationResolver delegate;
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
}
@Override
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
return this.delegate.resolve(request, "single");
}
}
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
return this.delegate.resolve(request, "single")
}
}
次に、このリゾルバーを使用して <saml2:SPSSODescriptor> メタデータの生成をカスタマイズする方法を見てみましょう。 |
<saml2:Response#Issuer>
に基づく解決
複数のアサーティングパーティからのアサーションを受け入れることができる 1 つの依存パーティがある場合、アサーティングパーティと同数の RelyingPartyRegistration
が存在し、依存パーティ情報が各インスタンス間で複製されます。
これは、アサーションコンシューマーサービスエンドポイントがアサーティングパーティごとに異なることを意味しますが、これは望ましくない場合があります。
代わりに、Issuer
を介して registrationId
を解決できます。これを行う RelyingPartyRegistrationResolver
のカスタム実装は次のようになります。
Java
Kotlin
public class SamlResponseIssuerRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
private final InMemoryRelyingPartyRegistrationRepository registrations;
// ... constructor
@Override
RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
if (registrationId != null) {
return this.registrations.findByRegistrationId(registrationId);
}
String entityId = resolveEntityIdFromSamlResponse(request);
for (RelyingPartyRegistration registration : this.registrations) {
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
return registration;
}
}
return null;
}
private String resolveEntityIdFromSamlResponse(HttpServletRequest request) {
// ...
}
}
class SamlResponseIssuerRelyingPartyRegistrationResolver(val registrations: InMemoryRelyingPartyRegistrationRepository):
RelyingPartyRegistrationResolver {
@Override
fun resolve(val request: HttpServletRequest, val registrationId: String): RelyingPartyRegistration {
if (registrationId != null) {
return this.registrations.findByRegistrationId(registrationId)
}
String entityId = resolveEntityIdFromSamlResponse(request)
for (val registration : this.registrations) {
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
return registration
}
}
return null
}
private resolveEntityIdFromSamlResponse(val request: HttpServletRequest): String {
// ...
}
}
次に、このリゾルバーを使用して <saml2:Response> 認証をカスタマイズする方法を見てみましょう。 |
連携ログイン
SAML 2.0 の一般的な取り決めの 1 つは、複数のアサーティングパーティを持つ ID プロバイダーです。この場合、ID プロバイダーのメタデータエンドポイントは複数の <md:IDPSSODescriptor>
要素を返します。
これらの複数のアサーティングパーティは、次のように RelyingPartyRegistrations
への 1 回の呼び出しでアクセスできます。
Java
Kotlin
Collection<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
.stream().map((builder) -> builder
.registrationId(UUID.randomUUID().toString())
.entityId("https://example.org/saml2/sp")
.build()
)
.collect(Collectors.toList()));
var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrations
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
.stream().map { builder : RelyingPartyRegistration.Builder -> builder
.registrationId(UUID.randomUUID().toString())
.entityId("https://example.org/saml2/sp")
.build()
}
.collect(Collectors.toList()));
登録 ID がランダムな値に設定されているため、これにより特定の SAML 2.0 エンドポイントが予測不能になることに注意してください。これに対処するにはいくつかの方法があります。フェデレーションの特定のユースケースに適した方法に焦点を当てましょう。
多くのフェデレーションの場合、すべてのアサーティングパーティがサービスプロバイダーの設定を共有します。Spring Security がデフォルトで、その SAML 2.0 URI のすべてに registrationId
を含めることを考えると、次のステップは、多くの場合、これらの URI を変更して registrationId
を除外することです。
これらの行に沿って変更したい主な URI が 2 つあります。
必要に応じて、認証リクエストの場所を変更することもできますが、これはアプリ内部の URI であり、アサーティングパーティには公開されないため、多くの場合、メリットは最小限です。 |
この完全な例は、saml-extension-federation
サンプル [GitHub] (英語) で確認できます。
Spring Security SAML 拡張 URI の使用
Spring Security SAML 拡張機能から移行する場合、SAML 拡張 URI のデフォルトを使用するようにアプリケーションを構成すると、利点が得られる場合があります。
詳細については、custom-urls
サンプル [GitHub] (英語) および saml-extension-federation
サンプル [GitHub] (英語) を参照してください。