6 changed files with 713 additions and 709 deletions
@ -0,0 +1,293 @@
@@ -0,0 +1,293 @@
|
||||
[[servlet-saml2login-sp-initiated-factory]] |
||||
= Producing ``<saml2:AuthnRequest>``s |
||||
|
||||
As stated earlier, Spring Security's SAML 2.0 support produces a `<saml2:AuthnRequest>` to commence authentication with the asserting party. |
||||
|
||||
Spring Security achieves this in part by registering the `Saml2WebSsoAuthenticationRequestFilter` in the filter chain. |
||||
This filter by default responds to endpoint `+/saml2/authenticate/{registrationId}+`. |
||||
|
||||
For example, if you were deployed to `https://rp.example.com` and you gave your registration an ID of `okta`, you could navigate to: |
||||
|
||||
`https://rp.example.org/saml2/authenticate/ping` |
||||
|
||||
and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded `<saml2:AuthnRequest>`. |
||||
|
||||
[[servlet-saml2login-store-authn-request]] |
||||
== Changing How the `<saml2:AuthnRequest>` Gets Stored |
||||
|
||||
`Saml2WebSsoAuthenticationRequestFilter` uses an `Saml2AuthenticationRequestRepository` to persist an `AbstractSaml2AuthenticationRequest` instance before xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-sp-initiated-factory[sending the `<saml2:AuthnRequest>`] to the asserting party. |
||||
|
||||
Additionally, `Saml2WebSsoAuthenticationFilter` and `Saml2AuthenticationTokenConverter` use an `Saml2AuthenticationRequestRepository` to load any `AbstractSaml2AuthenticationRequest` as part of xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticating the `<saml2:Response>`]. |
||||
|
||||
By default, Spring Security uses an `HttpSessionSaml2AuthenticationRequestRepository`, which stores the `AbstractSaml2AuthenticationRequest` in the `HttpSession`. |
||||
|
||||
If you have a custom implementation of `Saml2AuthenticationRequestRepository`, you may configure it by exposing it as a `@Bean` as shown in the following example: |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
@Bean |
||||
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository() { |
||||
return new CustomSaml2AuthenticationRequestRepository(); |
||||
} |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
@Bean |
||||
open fun authenticationRequestRepository(): Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> { |
||||
return CustomSaml2AuthenticationRequestRepository() |
||||
} |
||||
---- |
||||
==== |
||||
|
||||
[[servlet-saml2login-sp-initiated-factory-signing]] |
||||
== Changing How the `<saml2:AuthnRequest>` Gets Sent |
||||
|
||||
By default, Spring Security signs each `<saml2:AuthnRequest>` and send it as a GET to the asserting party. |
||||
|
||||
Many asserting parties don't require a signed `<saml2:AuthnRequest>`. |
||||
This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so: |
||||
|
||||
|
||||
.Not Requiring Signed AuthnRequests |
||||
==== |
||||
.Boot |
||||
[source,yaml,role="primary"] |
||||
---- |
||||
spring: |
||||
security: |
||||
saml2: |
||||
relyingparty: |
||||
okta: |
||||
identityprovider: |
||||
entity-id: ... |
||||
singlesignon.sign-request: false |
||||
---- |
||||
|
||||
.Java |
||||
[source,java,role="secondary"] |
||||
---- |
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta") |
||||
// ... |
||||
.assertingPartyDetails(party -> party |
||||
// ... |
||||
.wantAuthnRequestsSigned(false) |
||||
) |
||||
.build(); |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,java,role="secondary"] |
||||
---- |
||||
var relyingPartyRegistration: RelyingPartyRegistration = |
||||
RelyingPartyRegistration.withRegistrationId("okta") |
||||
// ... |
||||
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party |
||||
// ... |
||||
.wantAuthnRequestsSigned(false) |
||||
} |
||||
.build(); |
||||
---- |
||||
==== |
||||
|
||||
Otherwise, you will need to specify a private key to `RelyingPartyRegistration#signingX509Credentials` so that Spring Security can sign the `<saml2:AuthnRequest>` before sending. |
||||
|
||||
[[servlet-saml2login-sp-initiated-factory-algorithm]] |
||||
By default, Spring Security will sign the `<saml2:AuthnRequest>` using `rsa-sha256`, though some asserting parties will require a different algorithm, as indicated in their metadata. |
||||
|
||||
You can configure the algorithm based on the asserting party's xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistrationrepository[metadata using `RelyingPartyRegistrations`]. |
||||
|
||||
Or, you can provide it manually: |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
String metadataLocation = "classpath:asserting-party-metadata.xml"; |
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation) |
||||
// ... |
||||
.assertingPartyDetails((party) -> party |
||||
// ... |
||||
.signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512)) |
||||
) |
||||
.build(); |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
var metadataLocation = "classpath:asserting-party-metadata.xml" |
||||
var relyingPartyRegistration: RelyingPartyRegistration = |
||||
RelyingPartyRegistrations.fromMetadataLocation(metadataLocation) |
||||
// ... |
||||
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party |
||||
// ... |
||||
.signingAlgorithms { sign: MutableList<String?> -> |
||||
sign.add( |
||||
SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512 |
||||
) |
||||
} |
||||
} |
||||
.build(); |
||||
---- |
||||
==== |
||||
|
||||
NOTE: The snippet above uses the OpenSAML `SignatureConstants` class to supply the algorithm name. |
||||
But, that's just for convenience. |
||||
Since the datatype is `String`, you can supply the name of the algorithm directly. |
||||
|
||||
[[servlet-saml2login-sp-initiated-factory-binding]] |
||||
Some asserting parties require that the `<saml2:AuthnRequest>` be POSTed. |
||||
This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so: |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta") |
||||
// ... |
||||
.assertingPartyDetails(party -> party |
||||
// ... |
||||
.singleSignOnServiceBinding(Saml2MessageBinding.POST) |
||||
) |
||||
.build(); |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
var relyingPartyRegistration: RelyingPartyRegistration? = |
||||
RelyingPartyRegistration.withRegistrationId("okta") |
||||
// ... |
||||
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party |
||||
// ... |
||||
.singleSignOnServiceBinding(Saml2MessageBinding.POST) |
||||
} |
||||
.build() |
||||
---- |
||||
==== |
||||
|
||||
[[servlet-saml2login-sp-initiated-factory-custom-authnrequest]] |
||||
== Customizing OpenSAML's `AuthnRequest` Instance |
||||
|
||||
There are a number of reasons that you may want to adjust an `AuthnRequest`. |
||||
For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default. |
||||
|
||||
If you don't need information from the `HttpServletRequest` to make your decision, then the easiest way is to xref:servlet/saml2/login/overview.adoc#servlet-saml2login-opensaml-customization[register a custom `AuthnRequestMarshaller` with OpenSAML]. |
||||
This will give you access to post-process the `AuthnRequest` instance before it's serialized. |
||||
|
||||
But, if you do need something from the request, then you can use create a custom `Saml2AuthenticationRequestContext` implementation and then a `Converter<Saml2AuthenticationRequestContext, AuthnRequest>` to build an `AuthnRequest` yourself, like so: |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
@Component |
||||
public class AuthnRequestConverter implements |
||||
Converter<MySaml2AuthenticationRequestContext, AuthnRequest> { |
||||
|
||||
private final AuthnRequestBuilder authnRequestBuilder; |
||||
private final IssuerBuilder issuerBuilder; |
||||
|
||||
// ... constructor |
||||
|
||||
public AuthnRequest convert(Saml2AuthenticationRequestContext context) { |
||||
MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context; |
||||
Issuer issuer = issuerBuilder.buildObject(); |
||||
issuer.setValue(myContext.getIssuer()); |
||||
|
||||
AuthnRequest authnRequest = authnRequestBuilder.buildObject(); |
||||
authnRequest.setIssuer(issuer); |
||||
authnRequest.setDestination(myContext.getDestination()); |
||||
authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl()); |
||||
|
||||
// ... additional settings |
||||
|
||||
authRequest.setForceAuthn(myContext.getForceAuthn()); |
||||
return authnRequest; |
||||
} |
||||
} |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
@Component |
||||
class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> { |
||||
private val authnRequestBuilder: AuthnRequestBuilder? = null |
||||
private val issuerBuilder: IssuerBuilder? = null |
||||
|
||||
// ... constructor |
||||
override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest { |
||||
val myContext: MySaml2AuthenticationRequestContext = context |
||||
val issuer: Issuer = issuerBuilder.buildObject() |
||||
issuer.value = myContext.getIssuer() |
||||
val authnRequest: AuthnRequest = authnRequestBuilder.buildObject() |
||||
authnRequest.issuer = issuer |
||||
authnRequest.destination = myContext.getDestination() |
||||
authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl() |
||||
|
||||
// ... additional settings |
||||
authRequest.setForceAuthn(myContext.getForceAuthn()) |
||||
return authnRequest |
||||
} |
||||
} |
||||
---- |
||||
==== |
||||
|
||||
Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory` and publish them as ``@Bean``s: |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
@Bean |
||||
Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() { |
||||
Saml2AuthenticationRequestContextResolver resolver = |
||||
new DefaultSaml2AuthenticationRequestContextResolver(); |
||||
return request -> { |
||||
Saml2AuthenticationRequestContext context = resolver.resolve(request); |
||||
return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null); |
||||
}; |
||||
} |
||||
|
||||
@Bean |
||||
Saml2AuthenticationRequestFactory authenticationRequestFactory( |
||||
AuthnRequestConverter authnRequestConverter) { |
||||
|
||||
OpenSaml4AuthenticationRequestFactory authenticationRequestFactory = |
||||
new OpenSaml4AuthenticationRequestFactory(); |
||||
authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter); |
||||
return authenticationRequestFactory; |
||||
} |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
@Bean |
||||
open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver { |
||||
val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver() |
||||
return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest -> |
||||
val context = resolver.resolve(request) |
||||
MySaml2AuthenticationRequestContext( |
||||
context, |
||||
request.getParameter("force") != null |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Bean |
||||
open fun authenticationRequestFactory( |
||||
authnRequestConverter: AuthnRequestConverter? |
||||
): Saml2AuthenticationRequestFactory? { |
||||
val authenticationRequestFactory = OpenSaml4AuthenticationRequestFactory() |
||||
authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter) |
||||
return authenticationRequestFactory |
||||
} |
||||
---- |
||||
==== |
||||
|
||||
@ -0,0 +1,384 @@
@@ -0,0 +1,384 @@
|
||||
[[servlet-saml2login-authenticate-responses]] |
||||
= Authenticating ``<saml2:Response>``s |
||||
|
||||
To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] by default. |
||||
|
||||
You can configure this in a number of ways including: |
||||
|
||||
1. Setting a clock skew to timestamp validation |
||||
2. Mapping the response to a list of `GrantedAuthority` instances |
||||
3. Customizing the strategy for validating assertions |
||||
4. Customizing the strategy for decrypting response and assertion elements |
||||
|
||||
To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL. |
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-clockskew]] |
||||
== Setting a Clock Skew |
||||
|
||||
It's not uncommon for the asserting and relying parties to have system clocks that aren't perfectly synchronized. |
||||
For that reason, you can configure `OpenSaml4AuthenticationProvider` 's default assertion validator with some tolerance: |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
@EnableWebSecurity |
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter { |
||||
|
||||
@Override |
||||
protected void configure(HttpSecurity http) throws Exception { |
||||
OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider(); |
||||
authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider |
||||
.createDefaultAssertionValidator(assertionToken -> { |
||||
Map<String, Object> params = new HashMap<>(); |
||||
params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis()); |
||||
// ... other validation parameters |
||||
return new ValidationContext(params); |
||||
}) |
||||
); |
||||
|
||||
http |
||||
.authorizeRequests(authz -> authz |
||||
.anyRequest().authenticated() |
||||
) |
||||
.saml2Login(saml2 -> saml2 |
||||
.authenticationManager(new ProviderManager(authenticationProvider)) |
||||
); |
||||
} |
||||
} |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
@EnableWebSecurity |
||||
open class SecurityConfig : WebSecurityConfigurerAdapter() { |
||||
override fun configure(http: HttpSecurity) { |
||||
val authenticationProvider = OpenSaml4AuthenticationProvider() |
||||
authenticationProvider.setAssertionValidator( |
||||
OpenSaml4AuthenticationProvider |
||||
.createDefaultAssertionValidator(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> { |
||||
val params: MutableMap<String, Any> = HashMap() |
||||
params[CLOCK_SKEW] = |
||||
Duration.ofMinutes(10).toMillis() |
||||
ValidationContext(params) |
||||
}) |
||||
) |
||||
http { |
||||
authorizeRequests { |
||||
authorize(anyRequest, authenticated) |
||||
} |
||||
saml2Login { |
||||
authenticationManager = ProviderManager(authenticationProvider) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
---- |
||||
==== |
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]] |
||||
== Coordinating with a `UserDetailsService` |
||||
|
||||
Or, perhaps you would like to include user details from a legacy `UserDetailsService`. |
||||
In that case, the response authentication converter can come in handy, as can be seen below: |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
@EnableWebSecurity |
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter { |
||||
@Autowired |
||||
UserDetailsService userDetailsService; |
||||
|
||||
@Override |
||||
protected void configure(HttpSecurity http) throws Exception { |
||||
OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider(); |
||||
authenticationProvider.setResponseAuthenticationConverter(responseToken -> { |
||||
Saml2Authentication authentication = OpenSaml4AuthenticationProvider |
||||
.createDefaultResponseAuthenticationConverter() <1> |
||||
.convert(responseToken); |
||||
Assertion assertion = responseToken.getResponse().getAssertions().get(0); |
||||
String username = assertion.getSubject().getNameID().getValue(); |
||||
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); <2> |
||||
return MySaml2Authentication(userDetails, authentication); <3> |
||||
}); |
||||
|
||||
http |
||||
.authorizeRequests(authz -> authz |
||||
.anyRequest().authenticated() |
||||
) |
||||
.saml2Login(saml2 -> saml2 |
||||
.authenticationManager(new ProviderManager(authenticationProvider)) |
||||
); |
||||
} |
||||
} |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
@EnableWebSecurity |
||||
open class SecurityConfig : WebSecurityConfigurerAdapter() { |
||||
@Autowired |
||||
var userDetailsService: UserDetailsService? = null |
||||
|
||||
override fun configure(http: HttpSecurity) { |
||||
val authenticationProvider = OpenSaml4AuthenticationProvider() |
||||
authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken -> |
||||
val authentication = OpenSaml4AuthenticationProvider |
||||
.createDefaultResponseAuthenticationConverter() <1> |
||||
.convert(responseToken) |
||||
val assertion: Assertion = responseToken.response.assertions[0] |
||||
val username: String = assertion.subject.nameID.value |
||||
val userDetails = userDetailsService!!.loadUserByUsername(username) <2> |
||||
MySaml2Authentication(userDetails, authentication) <3> |
||||
} |
||||
http { |
||||
authorizeRequests { |
||||
authorize(anyRequest, authenticated) |
||||
} |
||||
saml2Login { |
||||
authenticationManager = ProviderManager(authenticationProvider) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
---- |
||||
==== |
||||
<1> First, call the default converter, which extracts attributes and authorities from the response |
||||
<2> Second, call the xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[`UserDetailsService`] using the relevant information |
||||
<3> Third, return a custom authentication that includes the user details |
||||
|
||||
[NOTE] |
||||
It's not required to call `OpenSaml4AuthenticationProvider` 's default authentication converter. |
||||
It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from ``AttributeStatement``s as well as the single `ROLE_USER` authority. |
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]] |
||||
== Performing Additional Response Validation |
||||
|
||||
`OpenSaml4AuthenticationProvider` validates the `Issuer` and `Destination` values right after decrypting the `Response`. |
||||
You can customize the validation by extending the default validator concatenating with your own response validator, or you can replace it entirely with yours. |
||||
|
||||
For example, you can throw a custom exception with any additional information available in the `Response` object, like so: |
||||
[source,java] |
||||
---- |
||||
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); |
||||
provider.setResponseValidator((responseToken) -> { |
||||
Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider |
||||
.createDefaultResponseValidator() |
||||
.convert(responseToken) |
||||
.concat(myCustomValidator.convert(responseToken)); |
||||
if (!result.getErrors().isEmpty()) { |
||||
String inResponseTo = responseToken.getInResponseTo(); |
||||
throw new CustomSaml2AuthenticationException(result, inResponseTo); |
||||
} |
||||
return result; |
||||
}); |
||||
---- |
||||
|
||||
== Performing Additional Assertion Validation |
||||
`OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions. |
||||
After verifying the signature, it will: |
||||
|
||||
1. Validate `<AudienceRestriction>` and `<DelegationRestriction>` conditions |
||||
2. Validate ``<SubjectConfirmation>``s, expect for any IP address information |
||||
|
||||
To perform additional validation, you can configure your own assertion validator that delegates to `OpenSaml4AuthenticationProvider` 's default and then performs its own. |
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]] |
||||
For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `<OneTimeUse>` condition, like so: |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); |
||||
OneTimeUseConditionValidator validator = ...; |
||||
provider.setAssertionValidator(assertionToken -> { |
||||
Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider |
||||
.createDefaultAssertionValidator() |
||||
.convert(assertionToken); |
||||
Assertion assertion = assertionToken.getAssertion(); |
||||
OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse(); |
||||
ValidationContext context = new ValidationContext(); |
||||
try { |
||||
if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) { |
||||
return result; |
||||
} |
||||
} catch (Exception e) { |
||||
return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage())); |
||||
} |
||||
return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage())); |
||||
}); |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
var provider = OpenSaml4AuthenticationProvider() |
||||
var validator: OneTimeUseConditionValidator = ... |
||||
provider.setAssertionValidator { assertionToken -> |
||||
val result = OpenSaml4AuthenticationProvider |
||||
.createDefaultAssertionValidator() |
||||
.convert(assertionToken) |
||||
val assertion: Assertion = assertionToken.assertion |
||||
val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse |
||||
val context = ValidationContext() |
||||
try { |
||||
if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) { |
||||
return@setAssertionValidator result |
||||
} |
||||
} catch (e: Exception) { |
||||
return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message)) |
||||
} |
||||
result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage)) |
||||
} |
||||
---- |
||||
==== |
||||
|
||||
[NOTE] |
||||
While recommended, it's not necessary to call `OpenSaml4AuthenticationProvider` 's default assertion validator. |
||||
A circumstance where you would skip it would be if you don't need it to check the `<AudienceRestriction>` or the `<SubjectConfirmation>` since you are doing those yourself. |
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-decryption]] |
||||
== Customizing Decryption |
||||
|
||||
Spring Security decrypts `<saml2:EncryptedAssertion>`, `<saml2:EncryptedAttribute>`, and `<saml2:EncryptedID>` elements automatically by using the decryption xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-credentials[`Saml2X509Credential` instances] registered in the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`]. |
||||
|
||||
`OpenSaml4AuthenticationProvider` exposes xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[two decryption strategies]. |
||||
The response decrypter is for decrypting encrypted elements of the `<saml2:Response>`, like `<saml2:EncryptedAssertion>`. |
||||
The assertion decrypter is for decrypting encrypted elements of the `<saml2:Assertion>`, like `<saml2:EncryptedAttribute>` and `<saml2:EncryptedID>`. |
||||
|
||||
You can replace `OpenSaml4AuthenticationProvider`'s default decryption strategy with your own. |
||||
For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>`, you can use it instead like so: |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
MyDecryptionService decryptionService = ...; |
||||
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); |
||||
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse())); |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
val decryptionService: MyDecryptionService = ... |
||||
val provider = OpenSaml4AuthenticationProvider() |
||||
provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) } |
||||
---- |
||||
==== |
||||
|
||||
If you are also decrypting individual elements in a `<saml2:Assertion>`, you can customize the assertion decrypter, too: |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion())); |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) } |
||||
---- |
||||
==== |
||||
|
||||
NOTE: There are two separate decrypters since assertions can be signed separately from responses. |
||||
Trying to decrypt a signed assertion's elements before signature verification may invalidate the signature. |
||||
If your asserting party signs the response only, then it's safe to decrypt all elements using only the response decrypter. |
||||
|
||||
[[servlet-saml2login-authenticationmanager-custom]] |
||||
== Using a Custom Authentication Manager |
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-authenticationmanager]] |
||||
Of course, the `authenticationManager` DSL method can be also used to perform a completely custom SAML 2.0 authentication. |
||||
This authentication manager should expect a `Saml2AuthenticationToken` object containing the SAML 2.0 Response XML data. |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
@EnableWebSecurity |
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter { |
||||
|
||||
@Override |
||||
protected void configure(HttpSecurity http) throws Exception { |
||||
AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...); |
||||
http |
||||
.authorizeRequests(authorize -> authorize |
||||
.anyRequest().authenticated() |
||||
) |
||||
.saml2Login(saml2 -> saml2 |
||||
.authenticationManager(authenticationManager) |
||||
) |
||||
; |
||||
} |
||||
} |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
@EnableWebSecurity |
||||
open class SecurityConfig : WebSecurityConfigurerAdapter() { |
||||
override fun configure(http: HttpSecurity) { |
||||
val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...) |
||||
http { |
||||
authorizeRequests { |
||||
authorize(anyRequest, authenticated) |
||||
} |
||||
saml2Login { |
||||
authenticationManager = customAuthenticationManager |
||||
} |
||||
} |
||||
} |
||||
} |
||||
---- |
||||
==== |
||||
|
||||
[[servlet-saml2login-authenticatedprincipal]] |
||||
== Using `Saml2AuthenticatedPrincipal` |
||||
|
||||
With the relying party correctly configured for a given asserting party, it's ready to accept assertions. |
||||
Once the relying party validates an assertion, the result is a `Saml2Authentication` with a `Saml2AuthenticatedPrincipal`. |
||||
|
||||
This means that you can access the principal in your controller like so: |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
@Controller |
||||
public class MainController { |
||||
@GetMapping("/") |
||||
public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) { |
||||
String email = principal.getFirstAttribute("email"); |
||||
model.setAttribute("email", email); |
||||
return "index"; |
||||
} |
||||
} |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
@Controller |
||||
class MainController { |
||||
@GetMapping("/") |
||||
fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String { |
||||
val email = principal.getFirstAttribute<String>("email") |
||||
model.setAttribute("email", email) |
||||
return "index" |
||||
} |
||||
} |
||||
---- |
||||
==== |
||||
|
||||
[TIP] |
||||
Because the SAML 2.0 specification allows for each attribute to have multiple values, you can either call `getAttribute` to get the list of attributes or `getFirstAttribute` to get the first in the list. |
||||
`getFirstAttribute` is quite handy when you know that there is only one value. |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
[[servlet-saml2login]] |
||||
= SAML 2.0 Login |
||||
:page-section-summary-toc: 1 |
||||
|
||||
The SAML 2.0 Login feature provides an application with the capability to act as a SAML 2.0 Relying Party, having users https://wiki.shibboleth.net/confluence/display/CONCEPT/FlowsAndConfig[log in] to the application by using their existing account at a SAML 2.0 Asserting Party (Okta, ADFS, etc). |
||||
|
||||
NOTE: SAML 2.0 Login is implemented by using the *Web Browser SSO Profile*, as specified in |
||||
https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf#page=15[SAML 2 Profiles]. |
||||
|
||||
[[servlet-saml2login-spring-security-history]] |
||||
Since 2009, support for relying parties has existed as an https://github.com/spring-projects/spring-security-saml/tree/1e013b07a7772defd6a26fcfae187c9bf661ee8f#spring-saml[extension project]. |
||||
In 2019, the process began to port that into https://github.com/spring-projects/spring-security[Spring Security] proper. |
||||
This process is similar to the one started in 2017 for xref:servlet/oauth2/index.adoc[Spring Security's OAuth 2.0 support]. |
||||
|
||||
[NOTE] |
||||
==== |
||||
A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2-login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository]. |
||||
==== |
||||
Loading…
Reference in new issue