|
| 1 | +[[servlet-saml2login-sp-initiated-factory]] |
| 2 | += Producing ``<saml2:AuthnRequest>``s |
| 3 | + |
| 4 | +As stated earlier, Spring Security's SAML 2.0 support produces a `<saml2:AuthnRequest>` to commence authentication with the asserting party. |
| 5 | + |
| 6 | +Spring Security achieves this in part by registering the `Saml2WebSsoAuthenticationRequestFilter` in the filter chain. |
| 7 | +This filter by default responds to endpoint `+/saml2/authenticate/{registrationId}+`. |
| 8 | + |
| 9 | +For example, if you were deployed to `https://rp.example.com` and you gave your registration an ID of `okta`, you could navigate to: |
| 10 | + |
| 11 | +`https://rp.example.org/saml2/authenticate/ping` |
| 12 | + |
| 13 | +and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded `<saml2:AuthnRequest>`. |
| 14 | + |
| 15 | +[[servlet-saml2login-store-authn-request]] |
| 16 | +== Changing How the `<saml2:AuthnRequest>` Gets Stored |
| 17 | + |
| 18 | +`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. |
| 19 | + |
| 20 | +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>`]. |
| 21 | + |
| 22 | +By default, Spring Security uses an `HttpSessionSaml2AuthenticationRequestRepository`, which stores the `AbstractSaml2AuthenticationRequest` in the `HttpSession`. |
| 23 | + |
| 24 | +If you have a custom implementation of `Saml2AuthenticationRequestRepository`, you may configure it by exposing it as a `@Bean` as shown in the following example: |
| 25 | + |
| 26 | +==== |
| 27 | +.Java |
| 28 | +[source,java,role="primary"] |
| 29 | +---- |
| 30 | +@Bean |
| 31 | +Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository() { |
| 32 | + return new CustomSaml2AuthenticationRequestRepository(); |
| 33 | +} |
| 34 | +---- |
| 35 | +
|
| 36 | +.Kotlin |
| 37 | +[source,kotlin,role="secondary"] |
| 38 | +---- |
| 39 | +@Bean |
| 40 | +open fun authenticationRequestRepository(): Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> { |
| 41 | + return CustomSaml2AuthenticationRequestRepository() |
| 42 | +} |
| 43 | +---- |
| 44 | +==== |
| 45 | + |
| 46 | +[[servlet-saml2login-sp-initiated-factory-signing]] |
| 47 | +== Changing How the `<saml2:AuthnRequest>` Gets Sent |
| 48 | + |
| 49 | +By default, Spring Security signs each `<saml2:AuthnRequest>` and send it as a GET to the asserting party. |
| 50 | + |
| 51 | +Many asserting parties don't require a signed `<saml2:AuthnRequest>`. |
| 52 | +This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so: |
| 53 | + |
| 54 | + |
| 55 | +.Not Requiring Signed AuthnRequests |
| 56 | +==== |
| 57 | +.Boot |
| 58 | +[source,yaml,role="primary"] |
| 59 | +---- |
| 60 | +spring: |
| 61 | + security: |
| 62 | + saml2: |
| 63 | + relyingparty: |
| 64 | + okta: |
| 65 | + identityprovider: |
| 66 | + entity-id: ... |
| 67 | + singlesignon.sign-request: false |
| 68 | +---- |
| 69 | +
|
| 70 | +.Java |
| 71 | +[source,java,role="secondary"] |
| 72 | +---- |
| 73 | +RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta") |
| 74 | + // ... |
| 75 | + .assertingPartyDetails(party -> party |
| 76 | + // ... |
| 77 | + .wantAuthnRequestsSigned(false) |
| 78 | + ) |
| 79 | + .build(); |
| 80 | +---- |
| 81 | +
|
| 82 | +.Kotlin |
| 83 | +[source,java,role="secondary"] |
| 84 | +---- |
| 85 | +var relyingPartyRegistration: RelyingPartyRegistration = |
| 86 | + RelyingPartyRegistration.withRegistrationId("okta") |
| 87 | + // ... |
| 88 | + .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party |
| 89 | + // ... |
| 90 | + .wantAuthnRequestsSigned(false) |
| 91 | + } |
| 92 | + .build(); |
| 93 | +---- |
| 94 | +==== |
| 95 | + |
| 96 | +Otherwise, you will need to specify a private key to `RelyingPartyRegistration#signingX509Credentials` so that Spring Security can sign the `<saml2:AuthnRequest>` before sending. |
| 97 | + |
| 98 | +[[servlet-saml2login-sp-initiated-factory-algorithm]] |
| 99 | +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. |
| 100 | + |
| 101 | +You can configure the algorithm based on the asserting party's xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistrationrepository[metadata using `RelyingPartyRegistrations`]. |
| 102 | + |
| 103 | +Or, you can provide it manually: |
| 104 | + |
| 105 | +==== |
| 106 | +.Java |
| 107 | +[source,java,role="primary"] |
| 108 | +---- |
| 109 | +String metadataLocation = "classpath:asserting-party-metadata.xml"; |
| 110 | +RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation) |
| 111 | + // ... |
| 112 | + .assertingPartyDetails((party) -> party |
| 113 | + // ... |
| 114 | + .signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512)) |
| 115 | + ) |
| 116 | + .build(); |
| 117 | +---- |
| 118 | +
|
| 119 | +.Kotlin |
| 120 | +[source,kotlin,role="secondary"] |
| 121 | +---- |
| 122 | +var metadataLocation = "classpath:asserting-party-metadata.xml" |
| 123 | +var relyingPartyRegistration: RelyingPartyRegistration = |
| 124 | + RelyingPartyRegistrations.fromMetadataLocation(metadataLocation) |
| 125 | + // ... |
| 126 | + .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party |
| 127 | + // ... |
| 128 | + .signingAlgorithms { sign: MutableList<String?> -> |
| 129 | + sign.add( |
| 130 | + SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512 |
| 131 | + ) |
| 132 | + } |
| 133 | + } |
| 134 | + .build(); |
| 135 | +---- |
| 136 | +==== |
| 137 | + |
| 138 | +NOTE: The snippet above uses the OpenSAML `SignatureConstants` class to supply the algorithm name. |
| 139 | +But, that's just for convenience. |
| 140 | +Since the datatype is `String`, you can supply the name of the algorithm directly. |
| 141 | + |
| 142 | +[[servlet-saml2login-sp-initiated-factory-binding]] |
| 143 | +Some asserting parties require that the `<saml2:AuthnRequest>` be POSTed. |
| 144 | +This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so: |
| 145 | + |
| 146 | +==== |
| 147 | +.Java |
| 148 | +[source,java,role="primary"] |
| 149 | +---- |
| 150 | +RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta") |
| 151 | + // ... |
| 152 | + .assertingPartyDetails(party -> party |
| 153 | + // ... |
| 154 | + .singleSignOnServiceBinding(Saml2MessageBinding.POST) |
| 155 | + ) |
| 156 | + .build(); |
| 157 | +---- |
| 158 | +
|
| 159 | +.Kotlin |
| 160 | +[source,kotlin,role="secondary"] |
| 161 | +---- |
| 162 | +var relyingPartyRegistration: RelyingPartyRegistration? = |
| 163 | + RelyingPartyRegistration.withRegistrationId("okta") |
| 164 | + // ... |
| 165 | + .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party |
| 166 | + // ... |
| 167 | + .singleSignOnServiceBinding(Saml2MessageBinding.POST) |
| 168 | + } |
| 169 | + .build() |
| 170 | +---- |
| 171 | +==== |
| 172 | + |
| 173 | +[[servlet-saml2login-sp-initiated-factory-custom-authnrequest]] |
| 174 | +== Customizing OpenSAML's `AuthnRequest` Instance |
| 175 | + |
| 176 | +There are a number of reasons that you may want to adjust an `AuthnRequest`. |
| 177 | +For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default. |
| 178 | + |
| 179 | +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]. |
| 180 | +This will give you access to post-process the `AuthnRequest` instance before it's serialized. |
| 181 | + |
| 182 | +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: |
| 183 | + |
| 184 | +==== |
| 185 | +.Java |
| 186 | +[source,java,role="primary"] |
| 187 | +---- |
| 188 | +@Component |
| 189 | +public class AuthnRequestConverter implements |
| 190 | + Converter<MySaml2AuthenticationRequestContext, AuthnRequest> { |
| 191 | +
|
| 192 | + private final AuthnRequestBuilder authnRequestBuilder; |
| 193 | + private final IssuerBuilder issuerBuilder; |
| 194 | +
|
| 195 | + // ... constructor |
| 196 | +
|
| 197 | + public AuthnRequest convert(Saml2AuthenticationRequestContext context) { |
| 198 | + MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context; |
| 199 | + Issuer issuer = issuerBuilder.buildObject(); |
| 200 | + issuer.setValue(myContext.getIssuer()); |
| 201 | +
|
| 202 | + AuthnRequest authnRequest = authnRequestBuilder.buildObject(); |
| 203 | + authnRequest.setIssuer(issuer); |
| 204 | + authnRequest.setDestination(myContext.getDestination()); |
| 205 | + authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl()); |
| 206 | +
|
| 207 | + // ... additional settings |
| 208 | +
|
| 209 | + authRequest.setForceAuthn(myContext.getForceAuthn()); |
| 210 | + return authnRequest; |
| 211 | + } |
| 212 | +} |
| 213 | +---- |
| 214 | +
|
| 215 | +.Kotlin |
| 216 | +[source,kotlin,role="secondary"] |
| 217 | +---- |
| 218 | +@Component |
| 219 | +class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> { |
| 220 | + private val authnRequestBuilder: AuthnRequestBuilder? = null |
| 221 | + private val issuerBuilder: IssuerBuilder? = null |
| 222 | +
|
| 223 | + // ... constructor |
| 224 | + override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest { |
| 225 | + val myContext: MySaml2AuthenticationRequestContext = context |
| 226 | + val issuer: Issuer = issuerBuilder.buildObject() |
| 227 | + issuer.value = myContext.getIssuer() |
| 228 | + val authnRequest: AuthnRequest = authnRequestBuilder.buildObject() |
| 229 | + authnRequest.issuer = issuer |
| 230 | + authnRequest.destination = myContext.getDestination() |
| 231 | + authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl() |
| 232 | +
|
| 233 | + // ... additional settings |
| 234 | + authRequest.setForceAuthn(myContext.getForceAuthn()) |
| 235 | + return authnRequest |
| 236 | + } |
| 237 | +} |
| 238 | +---- |
| 239 | +==== |
| 240 | + |
| 241 | +Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory` and publish them as ``@Bean``s: |
| 242 | + |
| 243 | +==== |
| 244 | +.Java |
| 245 | +[source,java,role="primary"] |
| 246 | +---- |
| 247 | +@Bean |
| 248 | +Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() { |
| 249 | + Saml2AuthenticationRequestContextResolver resolver = |
| 250 | + new DefaultSaml2AuthenticationRequestContextResolver(); |
| 251 | + return request -> { |
| 252 | + Saml2AuthenticationRequestContext context = resolver.resolve(request); |
| 253 | + return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null); |
| 254 | + }; |
| 255 | +} |
| 256 | +
|
| 257 | +@Bean |
| 258 | +Saml2AuthenticationRequestFactory authenticationRequestFactory( |
| 259 | + AuthnRequestConverter authnRequestConverter) { |
| 260 | +
|
| 261 | + OpenSaml4AuthenticationRequestFactory authenticationRequestFactory = |
| 262 | + new OpenSaml4AuthenticationRequestFactory(); |
| 263 | + authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter); |
| 264 | + return authenticationRequestFactory; |
| 265 | +} |
| 266 | +---- |
| 267 | +
|
| 268 | +.Kotlin |
| 269 | +[source,kotlin,role="secondary"] |
| 270 | +---- |
| 271 | +@Bean |
| 272 | +open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver { |
| 273 | + val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver() |
| 274 | + return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest -> |
| 275 | + val context = resolver.resolve(request) |
| 276 | + MySaml2AuthenticationRequestContext( |
| 277 | + context, |
| 278 | + request.getParameter("force") != null |
| 279 | + ) |
| 280 | + } |
| 281 | +} |
| 282 | +
|
| 283 | +@Bean |
| 284 | +open fun authenticationRequestFactory( |
| 285 | + authnRequestConverter: AuthnRequestConverter? |
| 286 | +): Saml2AuthenticationRequestFactory? { |
| 287 | + val authenticationRequestFactory = OpenSaml4AuthenticationRequestFactory() |
| 288 | + authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter) |
| 289 | + return authenticationRequestFactory |
| 290 | +} |
| 291 | +---- |
| 292 | +==== |
| 293 | + |
0 commit comments