├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── github │ └── choonchernlim │ └── security │ └── adfs │ └── saml2 │ ├── CsrfHeaderFilter.java │ ├── CustomAuthnContext.java │ ├── DefaultSAMLBootstrap.java │ ├── JndiBackedKeystoreService.java │ ├── KeystoreBean.java │ ├── MockFilterSecurityInterceptor.java │ ├── SAMLConfigBean.java │ ├── SAMLWebSecurityConfigurerAdapter.java │ └── SpringSecurityAdfsSaml2Exception.java ├── site └── site.xml └── test ├── groovy └── com │ └── github │ └── choonchernlim │ └── security │ └── adfs │ └── saml2 │ ├── CsrfHeaderFilterSpec.groovy │ ├── DefaultSAMLBootstrapSpec.groovy │ ├── JndiBackedKeystoreServiceSpec.groovy │ ├── KeystoreBeanSpec.groovy │ ├── MockFilterSecurityInterceptorSpec.groovy │ ├── SAMLConfigBeanSpec.groovy │ ├── SAMLWebSecurityConfigurerAdapterSpec.groovy │ └── SpringSecurityAdfsSaml2ExceptionSpec.groovy ├── java └── com │ └── github │ └── choonchernlim │ └── security │ └── adfs │ └── saml2 │ └── DummyTest.java └── resources ├── README.md └── test.jks /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | target/ 12 | .idea/ 13 | *.iml 14 | 15 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 16 | hs_err_pid* 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.9.0 - 2019-01-09 4 | 5 | * FEATURE - Added `SAMLConfigBean.useJdkCacertsForSslVerification` flag to allow SSL verifications to be performed by using JDK's cacerts instead of app's keystore file. 6 | 7 | ## 0.8.0 - 2018-07-11 8 | 9 | * Moved from Java 7 to Java 8. 10 | * Dependencies update... organized POM. 11 | 12 | ## 0.7.1 - 2018-05-04 13 | 14 | * BUG - `CsrfHeaderFilter` creates multiple cookies with same name but different path due to possible empty context path, which then uses current request's path. This may cause client side to read the wrong cookie when retrieving the CSRF token. 15 | 16 | ## 0.7.0 - 2017-11-28 17 | 18 | * Dropped autowired `Environment` from `SAMLWebSecurityConfigurerAdapter` and replaced with `ApplicationContext` to allow concrete class to access any Spring beans instead of just `Environment` to configure the security. This will also prevent any lifecycle or circular dependency problems when trying to autowire beans in concrete class. 19 | * Replaced `@PostContruct` with `@Bean` for `SAMLWebSecurityConfigurerAdapter.socketFactoryInitialization()`. 20 | 21 | ## 0.6.0 - 2016-07-18 22 | 23 | * Helper class `JndiBackedKeystoreService` to retrieve keystore info from JNDI value with following format: `jks-path,alias,storepass,keypass` 24 | 25 | ## 0.5.0 - 2016-07-13 26 | 27 | * If `samlConfigBean.storeCsrfTokenInCookie` is `true`, then store CSRF token in cookie. 28 | * Decoupled `WebSSOProfileOptions` from `SAMLEntryPoint` to allow user to override `SAMLEntryPoint` easily. 29 | * Dependency updates. 30 | 31 | ``` 32 | [INFO] cglib:cglib-nodep ..................................... 3.2.2 -> 3.2.4 33 | [INFO] org.codehaus.groovy:groovy-all ........................ 2.4.6 -> 2.4.7 34 | [INFO] org.spockframework:spock-core ... 35 | [INFO] 1.0-groovy-2.4 -> 1.1-groovy-2.4-rc-1 36 | [INFO] org.springframework:spring-test ....... 4.2.6.RELEASE -> 4.3.1.RELEASE 37 | [INFO] org.springframework.security:spring-security-config ... 38 | [INFO] 4.1.0.RELEASE -> 4.1.1.RELEASE 39 | [INFO] org.springframework.security:spring-security-core ... 40 | [INFO] 4.1.0.RELEASE -> 4.1.1.RELEASE 41 | [INFO] org.springframework.security:spring-security-web ... 42 | [INFO] 4.1.0.RELEASE -> 4.1.1.RELEASE 43 | ``` 44 | 45 | ## 0.4.0 - 2016-06-05 46 | 47 | * If `samlConfigBean.samlUserDetailsService` is provided, then set `samlAuthenticationProvider.forcePrincipalAsString` to `false` so that `principal` represents the `userDetails` object. 48 | * Ability to mock security to bypass authentication against ADFS during rapid app development. To use this, `samlConfigBean.samlUserDetailsService` must be set. 49 | * Dependency, parent and plugins updates. 50 | 51 | ``` 52 | com.github.choonchernlim:build-reports ................ 0.2.4 -> 0.3.2 53 | com.google.guava:guava-testlib .......................... 18.0 -> 19.0 54 | junit:junit ............................................. 4.11 -> 4.12 55 | org.codehaus.groovy:groovy-all .............. 2.4.3 -> 2.4.6 56 | org.springframework.security:spring-security-config ... 57 | 4.0.3.RELEASE -> 4.1.0.RELEASE 58 | org.springframework.security:spring-security-core ... 59 | 4.0.3.RELEASE -> 4.1.0.RELEASE 60 | org.springframework.security:spring-security-web ... 61 | 4.0.3.RELEASE -> 4.1.0.RELEASE 62 | org.springframework.security.extensions:spring-security-saml2-core ... 63 | 1.0.1.RELEASE -> 1.0.2.RELEASE 64 | maven-compiler-plugin ................................... 3.3 -> 3.5.1 65 | ``` 66 | 67 | ## 0.3.3 - 2016-04-13 68 | * Inject Spring environment to get access to project properties file. ([#1](https://github.com/choonchernlim/spring-security-adfs-saml2/pull/1)) 69 | 70 | ## 0.3.2 - 2016-03-14 71 | 72 | * Used `SAMLContextProviderLB` instead of `SAMLContextProviderImpl` to handle servers doing SSL termination. 73 | * Dropped `SAMLConfigBean.spMetadataBaseUrl`. 74 | * Renamed `SAMLConfigBean.adfsHostName` to `SAMLConfigBean.idpHostName`. 75 | * Added `SAMLConfigBean.spServerName`. 76 | * Added `SAMLConfigBean.spHttpsPort`. 77 | * Added `SAMLConfigBean.spContextPath`. 78 | 79 | ## 0.3.1 - 2016-03-10 80 | 81 | * Added `SAMLConfigBean.spMetadataBaseUrl` to manually specify the Sp's metadata base URL to handle situations where servers do SSL termination (HTTPS -> HTTP). 82 | * Configured metadata generator to use user defined Sp's metadata base URL when generating SAML endpoints URLs. 83 | 84 | ## 0.2.2 - 2016-03-08 85 | 86 | * Fixed casing typo from `SAMLConfigBean.keyStoreResource` to `SAMLConfigBean.keystoreResource`. 87 | 88 | ## 0.2.1 - 2016-03-07 89 | 90 | * Added `SAMLConfigBean.keystorePrivateKeyPassword` to add password for private key. 91 | * Kept storepass and keypass separate. 92 | * Excluded `xml-apis` from dependency because it's known to cause problems in WAS. 93 | 94 | ## 0.2.0 - 2016-03-02 95 | 96 | * Options to allow different authentication method. Default is user/password using IdP's form login page. 97 | * `CustomAuthnContext.WINDOWS_INTEGRATED_AUTHN_CTX` to allow Windows Integrated Authentication. 98 | 99 | ## 0.1.0 - 2016-02-28 100 | 101 | * Initial. 102 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Choon-Chern Lim 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-security-adfs-saml2 [![Build Status](https://travis-ci.org/choonchernlim/spring-security-adfs-saml2.svg?branch=master)](https://travis-ci.org/choonchernlim/spring-security-adfs-saml2) 2 | 3 | Spring Security module for service provider (Sp) to authenticate against identity provider's (IdP) ADFS using SAML2 protocol. 4 | 5 | How this module is configured:- 6 | 7 | * `HTTP-Redirect` binding for sending SAML messages to IdP. 8 | * Handles Sp servers doing SSL termination. 9 | * Default authentication method is user/password using IdP's form login page. 10 | * Default signature algorithm is `SHA256withRSA`. 11 | * Default digest algorithm is `SHA-256`. 12 | 13 | Tested against Sp's environments:- 14 | 15 | * Local Tomcat server without SSL termination. 16 | * Azure Tomcat server with SSL termination. 17 | 18 | Tested against IdP's environments:- 19 | 20 | * ADFS 2.0 - Windows Server 2008 R2. 21 | * ADFS 2.1 - Windows Server 2012. 22 | 23 | ## Maven Dependency 24 | 25 | ```xml 26 | 27 | com.github.choonchernlim 28 | spring-security-adfs-saml2 29 | 0.9.0 30 | 31 | ``` 32 | 33 | ## Prerequisites 34 | 35 | * Java 8. 36 | * Both Sp and IdP must use HTTPS protocol. 37 | * Java’s default keysize is limited to 128-bit key due to US export laws and a few countries’ import laws. So, Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files must be installed to allow larger key size, such as 256-bit key. 38 | * Keystore contains the following:- 39 | * (REQUIRED) Sp's public/private keys - to generate digital signature before sending SAML messages to IdP. 40 | * (OPTIONAL) IdP's public certificate - to verify IdP's SAML messages to prevent man-in-the-middle attack. This certificate can also be stored under JDK's cacerts. 41 | * To generate Sp's public/private keys:- 42 | 43 | ``` 44 | keytool -genkeypair \ 45 | -v \ 46 | -keystore /path/to/keystore.jks \ 47 | -storepass mystorepass \ 48 | -alias myapp \ 49 | -dname 'CN=[COMMON-NAME], OU=[ORGANIZATION-UNIT], O=[ORGANIZATION-NAME], L=[CITY-NAME], ST=[STATE-NAME], C=[COUNTRY]' \ 50 | -keypass mykeypass \ 51 | -keyalg RSA \ 52 | -keysize 2048 \ 53 | -sigalg SHA256withRSA 54 | ``` 55 | 56 | * To import IdP's public certificate into keystore:- 57 | 58 | ``` 59 | keytool -importcert \ 60 | -file idp-adfs-server.crt \ 61 | -keystore /path/to/keystore.jks \ 62 | -alias idp-adfs-server \ 63 | -storepass mystorepass 64 | ``` 65 | 66 | ## Usage 67 | 68 | ### Simplest Configuration 69 | 70 | If you are configuring for one IDP server, the easiest approach is to hardcode all the SAML config in the `@Configuration` file. 71 | 72 | ```java 73 | // Create a Java-based Spring configuration that extends SAMLWebSecurityConfigurerAdapter. 74 | @Configuration 75 | @EnableWebSecurity 76 | class AppSecurityConfig extends SAMLWebSecurityConfigurerAdapter { 77 | 78 | // See `SAMLConfigBean Properties` section below for more info. 79 | @Override 80 | protected SAMLConfigBean samlConfigBean() { 81 | return new SAMLConfigBeanBuilder() 82 | .withIdpServerName("idp-server") 83 | .withSpServerName("sp-server") 84 | .withSpContextPath("/app") 85 | .withKeystoreResource(new DefaultResourceLoader().getResource("classpath:keystore.jks")) 86 | .withKeystorePassword("storepass") 87 | .withKeystoreAlias("alias") 88 | .withKeystorePrivateKeyPassword("keypass") 89 | .withSuccessLoginDefaultUrl("/") 90 | .withSuccessLogoutUrl("/goodbye") 91 | .withStoreCsrfTokenInCookie(true) 92 | .build(); 93 | } 94 | 95 | // This configuration is not needed if your signature algorithm is SHA256withRSA and 96 | // digest algorithm is SHA-256. However, if you are using different algorithm(s), then 97 | // add this bean with the correct algorithms. 98 | @Bean 99 | public static SAMLBootstrap samlBootstrap() { 100 | return new DefaultSAMLBootstrap("RSA", 101 | SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512, 102 | SignatureConstants.ALGO_ID_DIGEST_SHA512); 103 | } 104 | 105 | // call `samlizedConfig(http)` first to decorate `http` with SAML configuration 106 | // before configuring app specific HTTP security 107 | @Override 108 | protected void configure(final HttpSecurity http) throws Exception { 109 | samlizedConfig(http) 110 | .authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN") 111 | .anyRequest().authenticated(); 112 | } 113 | 114 | // call `samlizedConfig(web)` first to decorate `web` with SAML configuration 115 | // before configuring app specific web security 116 | @Override 117 | public void configure(final WebSecurity web) throws Exception { 118 | samlizedConfig(web).ignoring().antMatchers("/resources/**"); 119 | } 120 | } 121 | ``` 122 | 123 | ### Customizing SSL Verification 124 | 125 | By default, the keystore file serves 2 purposes:- 126 | 127 | * Acts as a keystore, containing app's public/private key. 128 | * Acts as a truststore, containing IdP's certificate with public key. 129 | 130 | If the keystore does not contain IdP's certificate, the SSL verification will fail with the following error when attempting to retrieve IdP's metadata:- 131 | 132 | ``` 133 | PKIX path construction failed for untrusted credential: [subjectName='CN=idp.server.com,OU=IDP,C=US']: unable to find valid certification path to requested target 134 | I/O exception (javax.net.ssl.SSLPeerUnverifiedException) caught when processing request: SSL peer failed hostname validation for name: null 135 | Error retrieving metadata from https://idp.server.com/federationmetadata/2007-06/federationmetadata.xml 136 | ``` 137 | 138 | If you store the IdP's certificate under JDK's truststore (ie: cacerts) and you want the SSL verification to rely on that file, do this:- 139 | 140 | ```java 141 | @Configuration 142 | @EnableWebSecurity 143 | class AppSecurityConfig extends SAMLWebSecurityConfigurerAdapter { 144 | 145 | @Override 146 | protected SAMLConfigBean samlConfigBean() { 147 | return new SAMLConfigBeanBuilder() 148 | // ... other configurations 149 | .withUseJdkCacertsForSslVerification(true) 150 | .build(); 151 | } 152 | 153 | ... 154 | } 155 | ``` 156 | 157 | ### Environment Properties Driven Configuration 158 | 159 | If you don't want to use `@Profile` to configure environment-specific security, you may pass the configuration values through environment properties. 160 | 161 | To prevent lifecycle loading or circular dependency issues, instead of autowiring `Environment` into the concrete class, use the given autowired `applicationContext` to get hold of the Spring bean. 162 | 163 | ```java 164 | @Configuration 165 | @EnableWebSecurity 166 | class AppSecurityConfig extends SAMLWebSecurityConfigurerAdapter { 167 | 168 | @Override 169 | protected SAMLConfigBean samlConfigBean() { 170 | final Environment env = applicationContext.getBean(Environment.class); 171 | 172 | return new SAMLConfigBeanBuilder() 173 | .withIdpServerName(env.getProperty("idpServerName")) 174 | .withSpServerName(env.getProperty("spServerName")) 175 | .withSpContextPath(env.getProperty("spContextPath")) 176 | .withKeystoreResource(new DefaultResourceLoader().getResource(env.getProperty("keystoreResource"))) 177 | .withKeystorePassword(env.getProperty("keystorePassword")) 178 | .withKeystoreAlias(env.getProperty("keystoreAlias")) 179 | .withKeystorePrivateKeyPassword(env.getProperty("keystorePrivateKeyPassword")) 180 | .withSuccessLoginDefaultUrl(env.getProperty("successLoginDefaultUrl")) 181 | .withSuccessLogoutUrl(env.getProperty("successLogoutUrl")) 182 | .withStoreCsrfTokenInCookie(env.getProperty("storeCsrfTokenInCookie")) 183 | .build(); 184 | } 185 | 186 | ... 187 | } 188 | ``` 189 | 190 | ### Database Driven Configuration 191 | 192 | You may also configure `SAMLConfigBean` by retrieving the configuration values from database. 193 | 194 | Let's assume you have the following Spring JPA repository:- 195 | 196 | ```java 197 | public interface SecurityConfigRepository extends JpaRepository { 198 | SecurityConfigEntity findByEnvironment(String environment); 199 | } 200 | ``` 201 | 202 | To prevent lifecycle loading or circular dependency issues, instead of autowiring `SecurityConfigRepository` into the concrete class, use the given autowired `applicationContext` to get hold of the Spring repository bean. 203 | 204 | ```java 205 | @Configuration 206 | @EnableWebSecurity 207 | class AppSecurityConfig extends SAMLWebSecurityConfigurerAdapter { 208 | 209 | @Override 210 | protected SAMLConfigBean samlConfigBean() { 211 | final SecurityConfigRepository repository = applicationContext.getBean(SecurityConfigRepository.class); 212 | final SecurityConfigEntity entity = repository.findByEnvironment("dev"); 213 | 214 | return new SAMLConfigBeanBuilder() 215 | .withIdpServerName(entity.getIdpServerName()) 216 | .withSpServerName(entity.getSpServerName()) 217 | .withSpContextPath(entity.getSpContextPath()) 218 | .withKeystoreResource(new DefaultResourceLoader().getResource(entity.getKeystoreResource())) 219 | .withKeystorePassword(entity.getKeystorePassword()) 220 | .withKeystoreAlias(entity.getKeystoreAlias()) 221 | .withKeystorePrivateKeyPassword(entity.getKeystorePrivateKeyPassword()) 222 | .withSuccessLoginDefaultUrl(entity.getSuccessLoginDefaultUrl()) 223 | .withSuccessLogoutUrl(entity.getSuccessLogoutUrl()) 224 | .withStoreCsrfTokenInCookie(entity.getStoreCsrfTokenInCookie()) 225 | .build(); 226 | } 227 | 228 | ... 229 | } 230 | ``` 231 | 232 | ### Mocking Security by Hardcoding a Given User for Rapid App Development 233 | 234 | ```java 235 | @Override 236 | protected void configure(final HttpSecurity http) throws Exception { 237 | // `CurrentUser` must extend `User` 238 | final CurrentUser currentUser = new CurrentUser("First name", "Last Name", "ROLE_ADMIN"); 239 | 240 | mockSecurity(http, currentUser) 241 | .authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN") 242 | .anyRequest().authenticated(); 243 | } 244 | ``` 245 | 246 | ## SAMLConfigBean Properties 247 | 248 | `SAMLConfigBean` stores app-specific security configuration. 249 | 250 | |Property |Required? |Description | 251 | |--------------------------------|----------|----------------------------------------------------------------------------------------------------------| 252 | |idpServerName |Yes |IdP server name. Used for retrieving IdP metadata using HTTPS. If IdP link is `https://idp-server/adfs/ls`, value should be `idp-server`. | 253 | |spServerName |Yes |Sp server name. Used for generating correct SAML endpoints in Sp metadata to handle servers doing SSL termination. If Sp link is `https://sp-server:8443/myapp`, value should be `sp-server`. | 254 | |spHttpsPort |No |Sp HTTPS port. Used for generating correct SAML endpoints in Sp metadata to handle servers doing SSL termination. If Sp link is `https://sp-server:8443/myapp`, value should be `8443`.

Default is `443`. | 255 | |spContextPath |No |Sp context path. Used for generating correct SAML endpoints in Sp metadata to handle servers doing SSL termination. If Sp link is `https://sp-server:8443/myapp`, value should be `/myapp`.

Default is `''`. | 256 | |keystoreResource |Yes |App's keystore containing its public/private key and ADFS' certificate with public key. | 257 | |keystorePassword |Yes |Password to access app's keystore. | 258 | |keystoreAlias |Yes |Alias of app's public/private key pair. | 259 | |keystorePrivateKeyPassword |Yes |Password to access app's private key. | 260 | |successLoginDefaultUrl |Yes |Where to redirect user on successful login if no saved request is found in the session. | 261 | |successLogoutUrl |Yes |Where to redirect user on successful logout. | 262 | |failedLoginDefaultUrl |No |Where to redirect user on failed login. This value is set to null, which returns 401 error code on failed login. But, in theory, this will never be used because IdP will handled the failed login on IdP login page.

Default is `''`, which return 401 error code.| 263 | |storeCsrfTokenInCookie |No |Whether to store CSRF token in cookie named `XSRF-TOKEN` and expecting CSRF token to be set using header named `X-XSRF-TOKEN` to cater single-page app using frameworks like React and AngularJS.

Default is `false`. | 264 | |samlUserDetailsService |No |For configuring user details and authorities. When set, `userDetails` will be set as `principal`.

Default is `null`. | 265 | |authnContexts |No |Determine what authentication methods to use. To use the order of authentication methods defined by IdP, set as empty set. To enable Windows Integrated Auth (WIA), use `CustomAuthnContext.WINDOWS_INTEGRATED_AUTHN_CTX`.

Default is `AuthnContext.PASSWORD_AUTHN_CTX` where IdP login page is displayed to obtain user/password.| 266 | |useJdkCacertsForSslVerification |No |When performing IdP's SSL verification, find IdP's certs under JDK's cacerts instead of app's keystore file.

Default is `false`.| 267 | 268 | 269 | ## Important SAML Endpoints 270 | 271 | There are several SAML processing endpoints, but these are the ones you probably care:- 272 | 273 | |Endpoint |Description | 274 | |----------------------|----------------------------------------------------------------------------------------------------------------------------------------| 275 | |`/saml/login` |Initiates login process between Sp and IdP. Upon successful login, user will be redirected to `SAMLConfigBean.successLoginDefaultUrl`. | 276 | |`/saml/logout` |Initiates logout process between Sp and IdP. Upon successful logout, user will be redirected to `SAMLConfigBean.successLogoutUrl`. | 277 | |`/saml/metadata` |Returns Sp metadata. IdP may need this link to register Sp on ADFS. | 278 | 279 | ## Relevant Links 280 | 281 | * [Travis CI Reports](https://travis-ci.org/choonchernlim/spring-security-adfs-saml2) 282 | * [Maven Site](https://choonchernlim.github.io/spring-security-adfs-saml2/project-info.html) 283 | 284 | Learn about my pains and lessons learned while building this module. 285 | 286 | * [Replacing SHA-1 with SHA-256 on Signature and Digest Algorithms](http://myshittycode.com/2016/02/23/spring-security-saml-replacing-sha-1-with-sha-256-on-signature-and-digest-algorithms/) 287 | * [Handling IdP’s Public Certificate When Loading Metadata Over HTTPS](http://myshittycode.com/2016/02/19/spring-security-saml-handling-idps-public-certificate-when-loading-metadata-over-https/) 288 | * [Configuring Binding for Sending SAML Messages to IdP](http://myshittycode.com/2016/02/18/spring-security-saml-configuring-binding-for-sending-saml-messages-to-idp/) 289 | * [Java + SAML: Illegal Key Size](http://myshittycode.com/2016/02/18/java-saml-illegal-key-size/) 290 | 291 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.github.choonchernlim 8 | spring-boot-ci 9 | 0.4.0 10 | 11 | 12 | 13 | spring-security-adfs-saml2 14 | 0.9.0 15 | jar 16 | 17 | Spring Security ADFS SAML2 18 | Spring Security ADFS module using SAML2 protocol 19 | https://github.com/choonchernlim/spring-security-adfs-saml2 20 | 21 | 22 | 23 | MIT License 24 | http://www.opensource.org/licenses/mit-license.php 25 | 26 | 27 | 28 | 29 | 30 | choonchernlim 31 | Choon-Chern Lim 32 | choonchernlim@gmail.com 33 | https://github.com/choonchernlim 34 | 35 | 36 | 37 | 38 | scm:git:git@github.com:choonchernlim/spring-security-adfs-saml2.git 39 | scm:git:git@github.com:choonchernlim/spring-security-adfs-saml2.git 40 | git@github.com:choonchernlim/spring-security-adfs-saml2.git 41 | 42 | 43 | 44 | UTF-8 45 | UTF-8 46 | 1.8 47 | 48 | 0.1.1 49 | 2.6.4 50 | 8.0 51 | 4.2.2 52 | 3.0.1 53 | 1.0.3.RELEASE 54 | 55 | 56 | 57 | 58 | 59 | javax 60 | javaee-api 61 | ${javaee-api.version} 62 | 63 | 64 | net.karneim 65 | pojobuilder 66 | ${pojobuilder.version} 67 | 68 | 69 | org.springframework.security.extensions 70 | spring-security-saml2-core 71 | ${spring-security-saml2.version} 72 | 73 | 74 | xml-apis 75 | xml-apis 76 | 77 | 78 | 79 | 80 | org.opensaml 81 | opensaml 82 | ${opensaml.version} 83 | 84 | 85 | com.github.choonchernlim 86 | better-preconditions 87 | ${better-preconditions.version} 88 | 89 | 90 | org.objenesis 91 | objenesis 92 | ${objenesis.version} 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | javax 101 | javaee-api 102 | provided 103 | 104 | 105 | net.karneim 106 | pojobuilder 107 | provided 108 | 109 | 110 | 111 | 112 | org.springframework.security.extensions 113 | spring-security-saml2-core 114 | 115 | 116 | org.opensaml 117 | opensaml 118 | 119 | 120 | com.github.choonchernlim 121 | better-preconditions 122 | 123 | 124 | 125 | 126 | javax.servlet 127 | javax.servlet-api 128 | test 129 | 130 | 131 | org.springframework 132 | spring-test 133 | test 134 | 135 | 136 | org.spockframework 137 | spock-spring 138 | test 139 | 140 | 141 | cglib 142 | cglib-nodep 143 | test 144 | 145 | 146 | org.objenesis 147 | objenesis 148 | test 149 | 150 | 151 | 152 | 153 | 154 | 161 | 162 | org.apache.maven.plugins 163 | maven-antrun-plugin 164 | 165 | 166 | initialize 167 | 168 | run 169 | 170 | 171 | 172 | 173 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | org.springframework.boot 184 | spring-boot-maven-plugin 185 | 186 | true 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /src/main/java/com/github/choonchernlim/security/adfs/saml2/CsrfHeaderFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.choonchernlim.security.adfs.saml2; 2 | 3 | import com.google.common.base.MoreObjects; 4 | import com.google.common.base.Strings; 5 | import org.springframework.security.web.csrf.CsrfToken; 6 | import org.springframework.web.filter.OncePerRequestFilter; 7 | import org.springframework.web.util.WebUtils; 8 | 9 | import javax.servlet.FilterChain; 10 | import javax.servlet.ServletException; 11 | import javax.servlet.http.Cookie; 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.io.IOException; 15 | 16 | /** 17 | * Filter to pass CSRF token to single-page app through cookie. 18 | *

19 | * This approach is approved by Rob Winch, the project lead of Spring Security. 20 | */ 21 | final class CsrfHeaderFilter extends OncePerRequestFilter { 22 | /** 23 | * Header name to match AngularJS's spec to ensure this filter is more compatible with major 24 | * client-side MV* frameworks. 25 | */ 26 | static final String HEADER_NAME = "X-XSRF-TOKEN"; 27 | 28 | /** 29 | * Cookie name to match AngularJS's spec to ensure this filter is more compatible with major 30 | * client-side MV* frameworks. 31 | */ 32 | static final String COOKIE_NAME = "XSRF-TOKEN"; 33 | 34 | @Override 35 | protected void doFilterInternal(final HttpServletRequest request, 36 | final HttpServletResponse response, 37 | final FilterChain filterChain) 38 | throws ServletException, IOException { 39 | 40 | final CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); 41 | 42 | if (csrf != null) { 43 | final String token = csrf.getToken(); 44 | final Cookie existingCookie = WebUtils.getCookie(request, COOKIE_NAME); 45 | 46 | // If there's no existing cookie or the token value doesn't match, create/update it. 47 | // 48 | // `domain` = Don't need to set this so that the current request's domain will be used. 49 | // `httpOnly` = Cannot set this value because we need JS to be able to read this cookie to get the token. 50 | // `path` = Match app's root context so that only the app can access this cooke. 51 | // If path is empty, set it as '/' to prevent using the resource path currently requested. 52 | // This prevents creating too many cookies with the same name but different paths 53 | // (due to bookmark-able links) because client side will have difficulties grabbing 54 | // the right CSRF token value from the right cookie. 55 | // Regarding path not set, see See https://en.wikipedia.org/wiki/HTTP_cookie#Domain_and_path 56 | // 57 | // `secure` = Cookie to only be transmitted over secure protocol as https 58 | // 59 | // `maxAge` = Expire the cookie after 8 hours. Cannot use HTTP session timeout value because this 60 | // cookie will only get created/updated if the token value is different instead of 61 | // every time user refreshes the session by interacting with server side. 62 | if (existingCookie == null || !token.equals(existingCookie.getValue())) { 63 | final Cookie cookie = new Cookie(COOKIE_NAME, token); 64 | final String path = MoreObjects.firstNonNull( 65 | Strings.emptyToNull(request.getServletContext().getContextPath()), "/"); 66 | 67 | cookie.setPath(path); 68 | cookie.setSecure(true); 69 | cookie.setMaxAge(60 * 60 * 8); 70 | response.addCookie(cookie); 71 | } 72 | } 73 | 74 | filterChain.doFilter(request, response); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/github/choonchernlim/security/adfs/saml2/CustomAuthnContext.java: -------------------------------------------------------------------------------- 1 | package com.github.choonchernlim.security.adfs.saml2; 2 | 3 | /** 4 | * ADFS-specific authentication method. 5 | */ 6 | public final class CustomAuthnContext { 7 | /** 8 | * Windows integration authentication. 9 | */ 10 | public static final String WINDOWS_INTEGRATED_AUTHN_CTX = "urn:federation:authentication:windows"; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/choonchernlim/security/adfs/saml2/DefaultSAMLBootstrap.java: -------------------------------------------------------------------------------- 1 | package com.github.choonchernlim.security.adfs.saml2; 2 | 3 | import static com.github.choonchernlim.betterPreconditions.preconditions.PreconditionFactory.expect; 4 | import org.opensaml.Configuration; 5 | import org.opensaml.xml.security.BasicSecurityConfiguration; 6 | import org.opensaml.xml.signature.SignatureConstants; 7 | import org.springframework.beans.BeansException; 8 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 9 | import org.springframework.security.saml.SAMLBootstrap; 10 | 11 | /** 12 | * By default, Spring Security SAML uses SHA1withRSA for signature algorithm and SHA-1 for digest algorithm. 13 | *

14 | * This class allows app to use stronger encryption such as SHA-256. 15 | *

16 | * See: http://stackoverflow.com/questions/23681362/how-to-change-the-signature-algorithm-of-saml-request-in-spring-security 17 | * See: http://stackoverflow.com/questions/25982093/setting-the-extendedmetadata-signingalgorithm-field/26004147 18 | */ 19 | public final class DefaultSAMLBootstrap extends SAMLBootstrap { 20 | 21 | private final String signatureAlgorithmName; 22 | private final String signatureAlgorithmURI; 23 | private final String digestAlgorithmURI; 24 | 25 | /** 26 | * Default signature algorithm is SHA256withRSA and default digest algorithm is SHA-256. 27 | */ 28 | public DefaultSAMLBootstrap() { 29 | this("RSA", SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256, SignatureConstants.ALGO_ID_DIGEST_SHA256); 30 | } 31 | 32 | /** 33 | * Allows user to specify different algorithm URIs. 34 | * 35 | * @param signatureAlgorithmName Signature algorithm name 36 | * @param signatureAlgorithmURI Signature algorithm URI 37 | * @param digestAlgorithmURI Digest algorithm URI 38 | */ 39 | public DefaultSAMLBootstrap(final String signatureAlgorithmName, 40 | final String signatureAlgorithmURI, 41 | final String digestAlgorithmURI) { 42 | //@formatter:off 43 | this.signatureAlgorithmName = expect(signatureAlgorithmName, "Signature algorithm name").not().toBeBlank().check(); 44 | this.signatureAlgorithmURI = expect(signatureAlgorithmURI, "Signature algorithm URI").not().toBeBlank().check(); 45 | this.digestAlgorithmURI = expect(digestAlgorithmURI, "Digest algorithm URI").not().toBeBlank().check(); 46 | //@formatter:on 47 | } 48 | 49 | @Override 50 | public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { 51 | super.postProcessBeanFactory(beanFactory); 52 | BasicSecurityConfiguration config = (BasicSecurityConfiguration) Configuration.getGlobalSecurityConfiguration(); 53 | config.registerSignatureAlgorithmURI(signatureAlgorithmName, signatureAlgorithmURI); 54 | config.setSignatureReferenceDigestMethod(digestAlgorithmURI); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/github/choonchernlim/security/adfs/saml2/JndiBackedKeystoreService.java: -------------------------------------------------------------------------------- 1 | package com.github.choonchernlim.security.adfs.saml2; 2 | 3 | import static com.github.choonchernlim.betterPreconditions.preconditions.PreconditionFactory.expect; 4 | import com.google.common.base.Splitter; 5 | import org.springframework.core.io.DefaultResourceLoader; 6 | import org.springframework.core.io.Resource; 7 | import org.springframework.core.io.ResourceLoader; 8 | import org.springframework.jndi.JndiTemplate; 9 | 10 | import java.io.InputStream; 11 | import java.security.KeyStore; 12 | import java.util.Iterator; 13 | import java.util.List; 14 | 15 | /** 16 | * Helper class that retrieves JNDI value and returns a keystore bean. 17 | * The JNDI value has the following format: "jks-path,alias,storepass,keypass" 18 | */ 19 | public class JndiBackedKeystoreService { 20 | private final ResourceLoader resourceLoader = new DefaultResourceLoader(); 21 | 22 | /** 23 | * JNDI name 24 | */ 25 | private final String jndiName; 26 | 27 | private JndiTemplate jndiTemplate = new JndiTemplate(); 28 | 29 | public JndiBackedKeystoreService(final String jndiName) { 30 | this.jndiName = jndiName; 31 | } 32 | 33 | /** 34 | * For mocking out instance during testing. 35 | * 36 | * @param jndiTemplate Jndi template 37 | */ 38 | public void setJndiTemplate(final JndiTemplate jndiTemplate) { 39 | this.jndiTemplate = jndiTemplate; 40 | } 41 | 42 | /** 43 | * Retrieves keystore info from JNDI. 44 | * 45 | * @return Keystore bean 46 | */ 47 | public KeystoreBean get() { 48 | final Iterator ite = getJndiValues(); 49 | return getKeystoreBean(ite.next(), ite.next(), ite.next(), ite.next()); 50 | } 51 | 52 | /** 53 | * Returns transformed JNDI value from comma separated value to collection. 54 | * 55 | * @return JNDI values 56 | */ 57 | private Iterator getJndiValues() { 58 | final String jndiValue; 59 | try { 60 | jndiValue = jndiTemplate.lookup(jndiName, String.class); 61 | } 62 | catch (Exception e) { 63 | throw new SpringSecurityAdfsSaml2Exception(String.format("Unable to get value from JNDI: %s", jndiName), e); 64 | } 65 | 66 | final List jndiValues = Splitter.on(",").trimResults().splitToList(jndiValue); 67 | 68 | expect(jndiValues.size(), "jndiValues size").toBeEqual(4).check(); 69 | 70 | return jndiValues.iterator(); 71 | } 72 | 73 | /** 74 | * Ensures the input values are all valid before returning keystore bean. 75 | * 76 | * @param jksPath JKS path 77 | * @param keystoreAlias Keystore alias 78 | * @param keystorePassword Keystore password 79 | * @param keystorePrivateKeyPassword Keystore private key password 80 | * @return Keystore bean 81 | */ 82 | private KeystoreBean getKeystoreBean(final String jksPath, 83 | final String keystoreAlias, 84 | final String keystorePassword, 85 | final String keystorePrivateKeyPassword) { 86 | final Resource keystoreResource = resourceLoader.getResource(jksPath); 87 | 88 | final InputStream keystoreInputStream; 89 | try { 90 | keystoreInputStream = keystoreResource.getInputStream(); 91 | } 92 | catch (Exception e) { 93 | throw new SpringSecurityAdfsSaml2Exception("Invalid keystore path", e); 94 | } 95 | 96 | final KeyStore keyStore; 97 | try { 98 | keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 99 | } 100 | catch (Exception e) { 101 | throw new SpringSecurityAdfsSaml2Exception("Unable to initialize keystore", e); 102 | } 103 | 104 | try { 105 | keyStore.load(keystoreInputStream, keystorePassword.toCharArray()); 106 | } 107 | catch (Exception e) { 108 | throw new SpringSecurityAdfsSaml2Exception("Invalid keystore password", e); 109 | } 110 | 111 | try { 112 | if (!keyStore.isKeyEntry(keystoreAlias)) { 113 | throw new IllegalArgumentException("Provided alias not found"); 114 | } 115 | } 116 | catch (Exception e) { 117 | throw new SpringSecurityAdfsSaml2Exception("Invalid keystore alias", e); 118 | } 119 | 120 | try { 121 | keyStore.getKey(keystoreAlias, keystorePrivateKeyPassword.toCharArray()); 122 | } 123 | catch (Exception e) { 124 | throw new SpringSecurityAdfsSaml2Exception("Invalid keystore private key password", e); 125 | } 126 | 127 | return new KeystoreBeanBuilder() 128 | .withJksPath(jksPath) 129 | .withKeystoreAlias(keystoreAlias) 130 | .withKeystorePassword(keystorePassword) 131 | .withKeystorePrivateKeyPassword(keystorePrivateKeyPassword) 132 | .withKeystoreResource(keystoreResource) 133 | .build(); 134 | } 135 | 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/com/github/choonchernlim/security/adfs/saml2/KeystoreBean.java: -------------------------------------------------------------------------------- 1 | package com.github.choonchernlim.security.adfs.saml2; 2 | 3 | import net.karneim.pojobuilder.GeneratePojoBuilder; 4 | import org.springframework.core.io.Resource; 5 | 6 | /** 7 | * Keystore related info. 8 | */ 9 | public final class KeystoreBean { 10 | private final String jksPath; 11 | private final String keystoreAlias; 12 | private final String keystorePassword; 13 | private final String keystorePrivateKeyPassword; 14 | private final Resource keystoreResource; 15 | 16 | @GeneratePojoBuilder 17 | KeystoreBean(final String jksPath, 18 | final String keystoreAlias, 19 | final String keystorePassword, 20 | final String keystorePrivateKeyPassword, 21 | final Resource keystoreResource) { 22 | this.jksPath = jksPath; 23 | this.keystoreAlias = keystoreAlias; 24 | this.keystorePassword = keystorePassword; 25 | this.keystorePrivateKeyPassword = keystorePrivateKeyPassword; 26 | this.keystoreResource = keystoreResource; 27 | } 28 | 29 | public String getJksPath() { 30 | return jksPath; 31 | } 32 | 33 | public String getKeystoreAlias() { 34 | return keystoreAlias; 35 | } 36 | 37 | public String getKeystorePassword() { 38 | return keystorePassword; 39 | } 40 | 41 | public String getKeystorePrivateKeyPassword() { 42 | return keystorePrivateKeyPassword; 43 | } 44 | 45 | public Resource getKeystoreResource() { 46 | return keystoreResource; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/choonchernlim/security/adfs/saml2/MockFilterSecurityInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.github.choonchernlim.security.adfs.saml2; 2 | 3 | import static com.github.choonchernlim.betterPreconditions.preconditions.PreconditionFactory.expect; 4 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 5 | import org.springframework.security.core.context.SecurityContext; 6 | import org.springframework.security.core.context.SecurityContextHolder; 7 | import org.springframework.security.core.userdetails.User; 8 | import org.springframework.security.web.context.HttpSessionSecurityContextRepository; 9 | 10 | import javax.servlet.Filter; 11 | import javax.servlet.FilterChain; 12 | import javax.servlet.FilterConfig; 13 | import javax.servlet.ServletException; 14 | import javax.servlet.ServletRequest; 15 | import javax.servlet.ServletResponse; 16 | import javax.servlet.http.HttpServletRequest; 17 | import java.io.IOException; 18 | 19 | /** 20 | * This filter class can be placed before Spring Security's FilterSecurityInterceptor to bypass any security 21 | * by setting the given user to the security context and HTTP session. This allows developer to do rapid app 22 | * development without the need to log into the app again and again. 23 | */ 24 | final class MockFilterSecurityInterceptor implements Filter { 25 | 26 | private final User user; 27 | 28 | MockFilterSecurityInterceptor(final User user) { 29 | expect(user, "user").not().toBeNull().check(); 30 | this.user = user; 31 | } 32 | 33 | @Override 34 | public void doFilter(final ServletRequest servletRequest, 35 | final ServletResponse servletResponse, 36 | final FilterChain filterChain) throws IOException, ServletException { 37 | 38 | // To be consistent with SAML configuration, the `userDetails` is set as `principal` too 39 | final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( 40 | user, 41 | null, 42 | user.getAuthorities()); 43 | 44 | // setting user details 45 | authentication.setDetails(user); 46 | 47 | // setting `authentication` to security context 48 | final SecurityContext securityContext = SecurityContextHolder.getContext(); 49 | securityContext.setAuthentication(authentication); 50 | 51 | // setting `authentication` to HTTP session 52 | ((HttpServletRequest) servletRequest) 53 | .getSession(true) 54 | .setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext); 55 | 56 | filterChain.doFilter(servletRequest, servletResponse); 57 | } 58 | 59 | @Override 60 | public void init(final FilterConfig filterConfig) throws ServletException { 61 | } 62 | 63 | @Override 64 | public void destroy() { 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/github/choonchernlim/security/adfs/saml2/SAMLConfigBean.java: -------------------------------------------------------------------------------- 1 | package com.github.choonchernlim.security.adfs.saml2; 2 | 3 | import static com.github.choonchernlim.betterPreconditions.preconditions.PreconditionFactory.expect; 4 | import com.google.common.collect.ImmutableSet; 5 | import net.karneim.pojobuilder.GeneratePojoBuilder; 6 | import org.opensaml.saml2.core.AuthnContext; 7 | import org.springframework.core.io.Resource; 8 | import org.springframework.security.saml.userdetails.SAMLUserDetailsService; 9 | 10 | import java.util.Optional; 11 | import java.util.Set; 12 | 13 | /** 14 | * This class contains all properties that can be configured by Sp using the provided builder class. 15 | */ 16 | public final class SAMLConfigBean { 17 | 18 | /** 19 | * (REQUIRED) IdP's server name. 20 | */ 21 | private final String idpServerName; 22 | 23 | /** 24 | * (REQUIRED) Sp's server name. 25 | */ 26 | private final String spServerName; 27 | 28 | /** 29 | * (OPTIONAL) Sp's HTTPS port. 30 | *

31 | * Default is 443. 32 | */ 33 | private final Integer spHttpsPort; 34 | 35 | /** 36 | * (OPTIONAL) Sp's context path. 37 | *

38 | * Default is "". 39 | */ 40 | private final String spContextPath; 41 | 42 | /** 43 | * (REQUIRED) Keystore containing app's public/private key and ADFS' certificate with public key. 44 | */ 45 | private final Resource keystoreResource; 46 | 47 | /** 48 | * (REQUIRED) Keystore alias. 49 | */ 50 | private final String keystoreAlias; 51 | 52 | /** 53 | * (REQUIRED) Keystore password. 54 | */ 55 | private final String keystorePassword; 56 | 57 | /** 58 | * (REQUIRED) Keystore private key password. 59 | */ 60 | private final String keystorePrivateKeyPassword; 61 | 62 | /** 63 | * (REQUIRED) Where to redirect user on successful login if no saved request is found in the session. 64 | */ 65 | private final String successLoginDefaultUrl; 66 | 67 | /** 68 | * (REQUIRED) Where to redirect user on successful logout. 69 | */ 70 | private final String successLogoutUrl; 71 | 72 | /** 73 | * Where to redirect user on failed login. This value is set to null, which returns 74 | * 401 error code on failed login. But, in theory, this will never be used because 75 | * IdP will handled the failed login on IdP login page. 76 | *

77 | * Default is blank. 78 | */ 79 | private final String failedLoginDefaultUrl; 80 | 81 | /** 82 | * For configuring user details and authorities. 83 | *

84 | * Default is null. 85 | */ 86 | private final SAMLUserDetailsService samlUserDetailsService; 87 | 88 | /** 89 | * Whether to store CSRF token in cookie. 90 | *

91 | * Default is false. 92 | */ 93 | private final Boolean storeCsrfTokenInCookie; 94 | 95 | /** 96 | * Determine what authentication methods to use. 97 | *

98 | * To use the order of authentication methods defined by IdP, set as empty set. 99 | *

100 | * To enable Windows Integrated Auth (WIA) cross browsers and OSes, use `CustomAuthnContext.WINDOWS_INTEGRATED_AUTHN_CTX`. 101 | *

102 | * Default is user/password authentication where IdP login page is displayed. 103 | */ 104 | private final Set authnContexts; 105 | 106 | /** 107 | * Whether to rely on JDK's cacerts for SSL verification or not. 108 | *

109 | * By default, the provided keystore contains ADFS' certificate(s) to perform SSL verification. 110 | *

111 | * Default is false. 112 | */ 113 | private final Boolean useJdkCacertsForSslVerification; 114 | 115 | @GeneratePojoBuilder 116 | SAMLConfigBean(final String idpServerName, 117 | final String spServerName, 118 | final Integer spHttpsPort, 119 | final String spContextPath, 120 | final Resource keystoreResource, 121 | final String keystoreAlias, 122 | final String keystorePassword, 123 | final String keystorePrivateKeyPassword, 124 | final String successLoginDefaultUrl, 125 | final String successLogoutUrl, 126 | final String failedLoginDefaultUrl, 127 | final Boolean storeCsrfTokenInCookie, 128 | final SAMLUserDetailsService samlUserDetailsService, 129 | final Set authnContexts, 130 | final Boolean useJdkCacertsForSslVerification) { 131 | 132 | //@formatter:off 133 | this.idpServerName = expect(idpServerName, "IdP server name").not().toBeBlank().check(); 134 | 135 | this.spServerName = expect(spServerName, "Sp server name").not().toBeBlank().check(); 136 | this.spHttpsPort = Optional.ofNullable(spHttpsPort).orElse(443); 137 | this.spContextPath = Optional.ofNullable(spContextPath).orElse(""); 138 | 139 | this.keystoreResource = (Resource) expect(keystoreResource, "Key store").not().toBeNull().check(); 140 | this.keystoreAlias = expect(keystoreAlias, "Keystore alias").not().toBeBlank().check(); 141 | this.keystorePassword = expect(keystorePassword, "Keystore password").not().toBeBlank().check(); 142 | this.keystorePrivateKeyPassword = expect(keystorePrivateKeyPassword, "Keystore private key password").not().toBeBlank().check(); 143 | 144 | this.successLoginDefaultUrl = expect(successLoginDefaultUrl, "Success login URL").not().toBeBlank().check(); 145 | this.successLogoutUrl = expect(successLogoutUrl, "Success logout URL").not().toBeBlank().check(); 146 | this.failedLoginDefaultUrl = Optional.ofNullable(failedLoginDefaultUrl).orElse(""); 147 | 148 | this.storeCsrfTokenInCookie = Optional.ofNullable(storeCsrfTokenInCookie).orElse( false); 149 | this.samlUserDetailsService = samlUserDetailsService; 150 | 151 | this.authnContexts = Optional.ofNullable(authnContexts).orElse(ImmutableSet.of(AuthnContext.PASSWORD_AUTHN_CTX)); 152 | this.useJdkCacertsForSslVerification = Optional.ofNullable(useJdkCacertsForSslVerification).orElse(false); 153 | //@formatter:on 154 | } 155 | 156 | public String getIdpServerName() { 157 | return idpServerName; 158 | } 159 | 160 | public String getSpServerName() { 161 | return spServerName; 162 | } 163 | 164 | public Integer getSpHttpsPort() { 165 | return spHttpsPort; 166 | } 167 | 168 | public String getSpContextPath() { 169 | return spContextPath; 170 | } 171 | 172 | public Resource getKeystoreResource() { 173 | return keystoreResource; 174 | } 175 | 176 | public String getKeystoreAlias() { 177 | return keystoreAlias; 178 | } 179 | 180 | public String getKeystorePassword() { 181 | return keystorePassword; 182 | } 183 | 184 | public String getKeystorePrivateKeyPassword() { 185 | return keystorePrivateKeyPassword; 186 | } 187 | 188 | public String getSuccessLoginDefaultUrl() { 189 | return successLoginDefaultUrl; 190 | } 191 | 192 | public String getSuccessLogoutUrl() { 193 | return successLogoutUrl; 194 | } 195 | 196 | public String getFailedLoginDefaultUrl() { 197 | return failedLoginDefaultUrl; 198 | } 199 | 200 | public Boolean getStoreCsrfTokenInCookie() { 201 | return storeCsrfTokenInCookie; 202 | } 203 | 204 | public SAMLUserDetailsService getSamlUserDetailsService() { 205 | return samlUserDetailsService; 206 | } 207 | 208 | public Set getAuthnContexts() { 209 | return authnContexts; 210 | } 211 | 212 | public Boolean getUseJdkCacertsForSslVerification() { 213 | return useJdkCacertsForSslVerification; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/main/java/com/github/choonchernlim/security/adfs/saml2/SAMLWebSecurityConfigurerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.choonchernlim.security.adfs.saml2; 2 | 3 | import static com.github.choonchernlim.betterPreconditions.preconditions.PreconditionFactory.expect; 4 | import com.google.common.collect.ImmutableList; 5 | import com.google.common.collect.ImmutableMap; 6 | import org.apache.commons.httpclient.HttpClient; 7 | import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; 8 | import org.apache.commons.httpclient.protocol.Protocol; 9 | import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; 10 | import org.apache.velocity.app.VelocityEngine; 11 | import org.opensaml.common.xml.SAMLConstants; 12 | import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider; 13 | import org.opensaml.saml2.metadata.provider.MetadataProvider; 14 | import org.opensaml.saml2.metadata.provider.MetadataProviderException; 15 | import org.opensaml.xml.parse.StaticBasicParserPool; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.beans.factory.config.MethodInvokingFactoryBean; 18 | import org.springframework.context.ApplicationContext; 19 | import org.springframework.context.annotation.Bean; 20 | import org.springframework.security.authentication.AuthenticationManager; 21 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 22 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 23 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 24 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 25 | import org.springframework.security.core.userdetails.User; 26 | import org.springframework.security.saml.SAMLAuthenticationProvider; 27 | import org.springframework.security.saml.SAMLBootstrap; 28 | import org.springframework.security.saml.SAMLDiscovery; 29 | import org.springframework.security.saml.SAMLEntryPoint; 30 | import org.springframework.security.saml.SAMLLogoutFilter; 31 | import org.springframework.security.saml.SAMLLogoutProcessingFilter; 32 | import org.springframework.security.saml.SAMLProcessingFilter; 33 | import org.springframework.security.saml.SAMLWebSSOHoKProcessingFilter; 34 | import org.springframework.security.saml.context.SAMLContextProviderLB; 35 | import org.springframework.security.saml.key.JKSKeyManager; 36 | import org.springframework.security.saml.key.KeyManager; 37 | import org.springframework.security.saml.log.SAMLDefaultLogger; 38 | import org.springframework.security.saml.metadata.CachingMetadataManager; 39 | import org.springframework.security.saml.metadata.ExtendedMetadataDelegate; 40 | import org.springframework.security.saml.metadata.MetadataDisplayFilter; 41 | import org.springframework.security.saml.metadata.MetadataGenerator; 42 | import org.springframework.security.saml.metadata.MetadataGeneratorFilter; 43 | import org.springframework.security.saml.parser.ParserPoolHolder; 44 | import org.springframework.security.saml.processor.HTTPArtifactBinding; 45 | import org.springframework.security.saml.processor.HTTPPAOS11Binding; 46 | import org.springframework.security.saml.processor.HTTPPostBinding; 47 | import org.springframework.security.saml.processor.HTTPRedirectDeflateBinding; 48 | import org.springframework.security.saml.processor.HTTPSOAP11Binding; 49 | import org.springframework.security.saml.processor.SAMLBinding; 50 | import org.springframework.security.saml.processor.SAMLProcessorImpl; 51 | import org.springframework.security.saml.trust.httpclient.TLSProtocolConfigurer; 52 | import org.springframework.security.saml.trust.httpclient.TLSProtocolSocketFactory; 53 | import org.springframework.security.saml.userdetails.SAMLUserDetailsService; 54 | import org.springframework.security.saml.util.VelocityFactory; 55 | import org.springframework.security.saml.websso.ArtifactResolutionProfileImpl; 56 | import org.springframework.security.saml.websso.SingleLogoutProfile; 57 | import org.springframework.security.saml.websso.SingleLogoutProfileImpl; 58 | import org.springframework.security.saml.websso.WebSSOProfile; 59 | import org.springframework.security.saml.websso.WebSSOProfileConsumer; 60 | import org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl; 61 | import org.springframework.security.saml.websso.WebSSOProfileConsumerImpl; 62 | import org.springframework.security.saml.websso.WebSSOProfileECPImpl; 63 | import org.springframework.security.saml.websso.WebSSOProfileImpl; 64 | import org.springframework.security.saml.websso.WebSSOProfileOptions; 65 | import org.springframework.security.web.DefaultSecurityFilterChain; 66 | import org.springframework.security.web.FilterChainProxy; 67 | import org.springframework.security.web.SecurityFilterChain; 68 | import org.springframework.security.web.access.channel.ChannelProcessingFilter; 69 | import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; 70 | import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 71 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; 72 | import org.springframework.security.web.authentication.logout.LogoutHandler; 73 | import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; 74 | import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; 75 | import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; 76 | import org.springframework.security.web.csrf.CsrfFilter; 77 | import org.springframework.security.web.csrf.CsrfTokenRepository; 78 | import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; 79 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 80 | 81 | import java.util.Timer; 82 | 83 | /** 84 | * Spring Security configuration to authenticate against ADFS using SAML protocol. 85 | * This class should be extended by Sp's Java-based Spring configuration for web security. 86 | */ 87 | public abstract class SAMLWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { 88 | 89 | /** 90 | * Provides an opportunity for child class to access any Spring beans, if needed. 91 | */ 92 | @Autowired 93 | protected ApplicationContext applicationContext; 94 | 95 | @Autowired 96 | private SAMLAuthenticationProvider samlAuthenticationProvider; 97 | 98 | // Initialization of OpenSAML library, must be static to prevent "ObjectPostProcessor is a required bean" exception 99 | // By default, Spring Security SAML uses SHA-1. So, use `DefaultSAMLBootstrap` to use SHA-256. 100 | @Bean 101 | public static SAMLBootstrap samlBootstrap() { 102 | return new DefaultSAMLBootstrap(); 103 | } 104 | 105 | /** 106 | * Sp's SAMLConfigBean to further customize this security configuration. 107 | * 108 | * @return SAML config bean 109 | */ 110 | @Bean 111 | protected abstract SAMLConfigBean samlConfigBean(); 112 | 113 | /** 114 | * Fluent API that pre-configures HttpSecurity with SAML specific configuration. 115 | * 116 | * @param http HttpSecurity instance 117 | * @return Same HttpSecurity instance 118 | * @throws Exception Exception 119 | */ 120 | // CSRF must be disabled when processing /saml/** to prevent "Expected CSRF token not found" exception. 121 | // See: http://stackoverflow.com/questions/26508835/spring-saml-extension-and-spring-security-csrf-protection-conflict/26560447 122 | protected final HttpSecurity samlizedConfig(final HttpSecurity http) throws Exception { 123 | http.httpBasic().authenticationEntryPoint(samlEntryPoint()) 124 | .and() 125 | .csrf().ignoringAntMatchers("/saml/**") 126 | .and() 127 | .authorizeRequests().antMatchers("/saml/**").permitAll() 128 | .and() 129 | .addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class) 130 | .addFilterAfter(filterChainProxy(), BasicAuthenticationFilter.class); 131 | 132 | // store CSRF token in cookie 133 | if (samlConfigBean().getStoreCsrfTokenInCookie()) { 134 | http.csrf() 135 | .csrfTokenRepository(csrfTokenRepository()) 136 | .and() 137 | .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class); 138 | } 139 | 140 | return http; 141 | } 142 | 143 | /** 144 | * Configure CSRF token repository to accept CSRF token from AngularJS friendly header. 145 | * 146 | * @return CsrfTokenRepository 147 | */ 148 | private CsrfTokenRepository csrfTokenRepository() { 149 | HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository(); 150 | repository.setHeaderName(CsrfHeaderFilter.HEADER_NAME); 151 | return repository; 152 | } 153 | 154 | 155 | /** 156 | * Mocks security by hardcoding a given user so that it will always appear that user is accessing the protected 157 | * resources. This is useful to allow developer to bypass any web authentication against ADFS during rapid 158 | * app development. 159 | * 160 | * @param http HttpSecurity instance 161 | * @param user User instance 162 | * @return HttpSecurity that will never authenticate against ADFS 163 | */ 164 | protected final HttpSecurity mockSecurity(final HttpSecurity http, final User user) { 165 | expect(user, "user").not().toBeNull().check(); 166 | 167 | if (samlConfigBean().getSamlUserDetailsService() == null) { 168 | throw new SpringSecurityAdfsSaml2Exception( 169 | "`samlConfigBean.samlUserDetailsService` cannot be null. " + 170 | "When mocking security, the given user details object will be set as principal. " + 171 | "Because setting `samlConfigBean.samlUserDetailsService` will set the user details object as principal, " + 172 | "this property must be configured to ensure the mock security mimics the actual security configuration." 173 | ); 174 | } 175 | 176 | return http.addFilterBefore(new MockFilterSecurityInterceptor(user), FilterSecurityInterceptor.class); 177 | } 178 | 179 | /** 180 | * Fluent API that pre-configures WebSecurity with SAML specific configuration. 181 | * 182 | * @param web WebSecurity instance 183 | * @return Same WebSecurity instance 184 | * @throws Exception Exception 185 | */ 186 | protected final WebSecurity samlizedConfig(final WebSecurity web) throws Exception { 187 | web.ignoring().antMatchers(samlConfigBean().getSuccessLogoutUrl()); 188 | return web; 189 | } 190 | 191 | // IDP metadata URL 192 | private String getMetdataUrl() { 193 | return String.format("https://%s/federationmetadata/2007-06/federationmetadata.xml", 194 | samlConfigBean().getIdpServerName()); 195 | } 196 | 197 | // Entry point to initialize authentication 198 | @Bean 199 | public SAMLEntryPoint samlEntryPoint() { 200 | SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint(); 201 | samlEntryPoint.setDefaultProfileOptions(webSSOProfileOptions()); 202 | return samlEntryPoint; 203 | } 204 | 205 | /** 206 | * Customizing SAML request message to be sent to the IDP. 207 | * 208 | * @return WebSSOProfileOptions 209 | */ 210 | @Bean 211 | public WebSSOProfileOptions webSSOProfileOptions() { 212 | WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions(); 213 | 214 | // Disable element scoping when sending requests to IdP to prevent 215 | // "Response has invalid status code urn:oasis:names:tc:SAML:2.0:status:Responder, status message is null" 216 | // exception 217 | webSSOProfileOptions.setIncludeScoping(false); 218 | 219 | // Always use HTTP-Redirect instead of HTTP-Post, although both works with ADFS 220 | webSSOProfileOptions.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); 221 | 222 | // Force IdP to re-authenticate user if issued token is too old to prevent 223 | // "Authentication statement is too old to be used with value" exception 224 | // See: http://stackoverflow.com/questions/30528636/saml-login-errors 225 | webSSOProfileOptions.setForceAuthN(true); 226 | 227 | // Determine what authentication method to use (WIA, user/password, etc). 228 | // If not set, it will use authentication method order defined by IdP 229 | if (!samlConfigBean().getAuthnContexts().isEmpty()) { 230 | webSSOProfileOptions.setAuthnContexts(samlConfigBean().getAuthnContexts()); 231 | } 232 | 233 | return webSSOProfileOptions; 234 | } 235 | 236 | // Filter automatically generates default SP metadata 237 | @Bean 238 | public MetadataGeneratorFilter metadataGeneratorFilter() { 239 | // generates base URL that matches `SAMLContextProviderLB` configuration 240 | // to ensure SAML endpoints work for server doing SSL termination 241 | StringBuilder sb = new StringBuilder(); 242 | sb.append("https://").append(samlConfigBean().getSpServerName()); 243 | if (samlConfigBean().getSpHttpsPort() != 443) { 244 | sb.append(":").append(samlConfigBean().getSpHttpsPort()); 245 | } 246 | sb.append(samlConfigBean().getSpContextPath()); 247 | String entityBaseUrl = sb.toString(); 248 | 249 | MetadataGenerator metadataGenerator = new MetadataGenerator(); 250 | metadataGenerator.setKeyManager(keyManager()); 251 | metadataGenerator.setEntityBaseURL(entityBaseUrl); 252 | return new MetadataGeneratorFilter(metadataGenerator); 253 | } 254 | 255 | // HTTP client 256 | @Bean 257 | public HttpClient httpClient() { 258 | return new HttpClient(new MultiThreadedHttpConnectionManager()); 259 | } 260 | 261 | // Filters for processing of SAML messages 262 | @Bean 263 | public FilterChainProxy filterChainProxy() throws Exception { 264 | //@formatter:off 265 | return new FilterChainProxy(ImmutableList.of( 266 | new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"), samlEntryPoint()), 267 | new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"), samlLogoutFilter()), 268 | new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"), metadataDisplayFilter()), 269 | new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"), samlProcessingFilter()), 270 | new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSOHoK/**"), samlWebSSOHoKProcessingFilter()), 271 | new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"), samlLogoutProcessingFilter()), 272 | new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"), samlIDPDiscovery()) 273 | )); 274 | //@formatter:on 275 | } 276 | 277 | // Handler deciding where to redirect user after successful login 278 | @Bean 279 | public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() { 280 | SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler = new SavedRequestAwareAuthenticationSuccessHandler(); 281 | successRedirectHandler.setDefaultTargetUrl(samlConfigBean().getSuccessLoginDefaultUrl()); 282 | return successRedirectHandler; 283 | } 284 | 285 | // Handler deciding where to redirect user after failed login 286 | @Bean 287 | public SimpleUrlAuthenticationFailureHandler failureRedirectHandler() { 288 | SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); 289 | 290 | // The precondition on `setDefaultFailureUrl(..)` will cause an exception if the value is null. 291 | // So, only set this value if it is not null 292 | if (!samlConfigBean().getFailedLoginDefaultUrl().isEmpty()) { 293 | failureHandler.setDefaultFailureUrl(samlConfigBean().getFailedLoginDefaultUrl()); 294 | } 295 | 296 | return failureHandler; 297 | } 298 | 299 | // Handler for successful logout 300 | @Bean 301 | public SimpleUrlLogoutSuccessHandler successLogoutHandler() { 302 | SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler(); 303 | successLogoutHandler.setDefaultTargetUrl(samlConfigBean().getSuccessLogoutUrl()); 304 | return successLogoutHandler; 305 | } 306 | 307 | // Authentication manager 308 | @Bean 309 | @Override 310 | public AuthenticationManager authenticationManagerBean() throws Exception { 311 | return super.authenticationManagerBean(); 312 | } 313 | 314 | // Register authentication manager for SAML provider 315 | @Override 316 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 317 | auth.authenticationProvider(samlAuthenticationProvider); 318 | } 319 | 320 | // Logger for SAML messages and events 321 | @Bean 322 | public SAMLDefaultLogger samlLogger() { 323 | return new SAMLDefaultLogger(); 324 | } 325 | 326 | // Central storage of cryptographic keys 327 | @Bean 328 | public KeyManager keyManager() { 329 | return new JKSKeyManager(samlConfigBean().getKeystoreResource(), 330 | samlConfigBean().getKeystorePassword(), 331 | ImmutableMap.of(samlConfigBean().getKeystoreAlias(), 332 | samlConfigBean().getKeystorePrivateKeyPassword()), 333 | samlConfigBean().getKeystoreAlias()); 334 | } 335 | 336 | // IDP Discovery service 337 | @Bean 338 | public SAMLDiscovery samlIDPDiscovery() { 339 | return new SAMLDiscovery(); 340 | } 341 | 342 | // The filter is waiting for connections on URL suffixed with filterSuffix and presents SP metadata there 343 | @Bean 344 | public MetadataDisplayFilter metadataDisplayFilter() { 345 | return new MetadataDisplayFilter(); 346 | } 347 | 348 | // Configure HTTP Client to accept certificates from the keystore or from JDK cacerts for IDP SSL verification 349 | @Bean 350 | public TLSProtocolConfigurer tlsProtocolConfigurer() { 351 | // To perform IDP SSL verification against app keystore file, return `TLSProtocolConfigurer`. 352 | // Otherwise, return null so that it will try to find the IDP certs in JDK's cacerts. 353 | // 354 | // See https://stackoverflow.com/questions/28505824/spring-security-saml-https-to-another-page/28538799#28538799 355 | return !samlConfigBean().getUseJdkCacertsForSslVerification() ? 356 | new TLSProtocolConfigurer() : 357 | null; 358 | } 359 | 360 | // Configure TLSProtocolConfigurer 361 | @Bean 362 | public ProtocolSocketFactory protocolSocketFactory() { 363 | return new TLSProtocolSocketFactory(keyManager(), null, "default"); 364 | } 365 | 366 | // Configure TLSProtocolConfigurer 367 | @Bean 368 | public Protocol protocol() { 369 | return new Protocol("https", protocolSocketFactory(), 443); 370 | } 371 | 372 | // Configure TLSProtocolConfigurer 373 | @Bean 374 | public MethodInvokingFactoryBean socketFactoryInitialization() { 375 | // Since it is not possible to return `null`, if app needs to perform SSL verification against JDK cacerts, 376 | // just set the protocol ID to anything but "http" or "https" so that it will be ignored. 377 | String protocolId = !samlConfigBean().getUseJdkCacertsForSslVerification() ? "https" : "ignored"; 378 | 379 | MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean(); 380 | methodInvokingFactoryBean.setTargetClass(Protocol.class); 381 | methodInvokingFactoryBean.setTargetMethod("registerProtocol"); 382 | methodInvokingFactoryBean.setArguments(protocolId, protocol()); 383 | return methodInvokingFactoryBean; 384 | } 385 | 386 | // IDP Metadata configuration - paths to metadata of IDPs in circle of trust is here 387 | @Bean 388 | public CachingMetadataManager metadata() throws MetadataProviderException { 389 | HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(new Timer(true), 390 | httpClient(), 391 | getMetdataUrl()); 392 | httpMetadataProvider.setParserPool(parserPool()); 393 | 394 | ExtendedMetadataDelegate extendedMetadataDelegate = new ExtendedMetadataDelegate(httpMetadataProvider); 395 | // Disable metadata trust check to prevent "Signature trust establishment failed for metadata entry" exception 396 | extendedMetadataDelegate.setMetadataTrustCheck(false); 397 | 398 | return new CachingMetadataManager(ImmutableList.of(extendedMetadataDelegate)); 399 | } 400 | 401 | // SAML Authentication Provider responsible for validating of received SAML messages 402 | @Bean 403 | public SAMLAuthenticationProvider samlAuthenticationProvider() { 404 | SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider(); 405 | SAMLUserDetailsService samlUserDetailsService = samlConfigBean().getSamlUserDetailsService(); 406 | 407 | if (samlUserDetailsService != null) { 408 | samlAuthenticationProvider.setUserDetails(samlUserDetailsService); 409 | 410 | // By default, `principal` is always going to be `NameID` even though the `Authentication` object 411 | // contain `userDetails` object. So, if `userDetails` is provided, then don't force `principal` as 412 | // string so that `principal` represents `userDetails` object. 413 | // See: http://stackoverflow.com/questions/33786861/how-to-override-the-nameid-value-in-samlauthenticationprovider 414 | samlAuthenticationProvider.setForcePrincipalAsString(false); 415 | } 416 | 417 | return samlAuthenticationProvider; 418 | } 419 | 420 | // In order to get SAML to work for Sp servers doing SSL termination, `SAMLContextProviderLB` has 421 | // to be used instead of `SAMLContextProviderImpl` to prevent the following exception:- 422 | // 423 | // "SAML message intended destination endpoint 'https://server/app/saml/SSO' did not match the 424 | // recipient endpoint 'http://server/app/saml/SSO'" 425 | // 426 | // This configuration will work for Sp servers (not) doing SSL termination. 427 | @Bean 428 | public SAMLContextProviderLB contextProvider() { 429 | SAMLContextProviderLB contextProviderLB = new SAMLContextProviderLB(); 430 | contextProviderLB.setScheme("https"); 431 | contextProviderLB.setServerName(samlConfigBean().getSpServerName()); 432 | contextProviderLB.setServerPort(samlConfigBean().getSpHttpsPort()); 433 | contextProviderLB.setIncludeServerPortInRequestURL(samlConfigBean().getSpHttpsPort() != 443); 434 | contextProviderLB.setContextPath(samlConfigBean().getSpContextPath()); 435 | return contextProviderLB; 436 | } 437 | 438 | // Processing filter for WebSSO profile messages 439 | @Bean 440 | public SAMLProcessingFilter samlProcessingFilter() throws Exception { 441 | SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter(); 442 | samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager()); 443 | samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler()); 444 | samlWebSSOProcessingFilter.setAuthenticationFailureHandler(failureRedirectHandler()); 445 | return samlWebSSOProcessingFilter; 446 | } 447 | 448 | // Processing filter for WebSSO Holder-of-Key profile 449 | @Bean 450 | public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter() throws Exception { 451 | SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter = new SAMLWebSSOHoKProcessingFilter(); 452 | samlWebSSOHoKProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler()); 453 | samlWebSSOHoKProcessingFilter.setAuthenticationManager(authenticationManager()); 454 | samlWebSSOHoKProcessingFilter.setAuthenticationFailureHandler(failureRedirectHandler()); 455 | return samlWebSSOHoKProcessingFilter; 456 | } 457 | 458 | // Logout handler terminating local session 459 | @Bean 460 | public SecurityContextLogoutHandler logoutHandler() { 461 | SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler(); 462 | logoutHandler.setInvalidateHttpSession(true); 463 | logoutHandler.setClearAuthentication(true); 464 | return logoutHandler; 465 | } 466 | 467 | // Override default logout processing filter with the one processing SAML messages 468 | @Bean 469 | public SAMLLogoutFilter samlLogoutFilter() { 470 | return new SAMLLogoutFilter(successLogoutHandler(), 471 | new LogoutHandler[]{logoutHandler()}, 472 | new LogoutHandler[]{logoutHandler()}); 473 | } 474 | 475 | // Filter processing incoming logout messages 476 | // First argument determines URL user will be redirected to after successful global logout 477 | @Bean 478 | public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() { 479 | return new SAMLLogoutProcessingFilter(successLogoutHandler(), logoutHandler()); 480 | } 481 | 482 | // Class loading incoming SAML messages from httpRequest stream 483 | @Bean 484 | public SAMLProcessorImpl processor() { 485 | return new SAMLProcessorImpl(ImmutableList.of(redirectDeflateBinding(), 486 | postBinding(), 487 | artifactBinding(), 488 | soapBinding(), 489 | paosBinding())); 490 | } 491 | 492 | // SAML 2.0 WebSSO Assertion Consumer 493 | @Bean 494 | public WebSSOProfileConsumer webSSOprofileConsumer() { 495 | return new WebSSOProfileConsumerImpl(); 496 | } 497 | 498 | // SAML 2.0 Holder-of-Key WebSSO Assertion Consumer 499 | @Bean 500 | public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() { 501 | return new WebSSOProfileConsumerHoKImpl(); 502 | } 503 | 504 | // SAML 2.0 Web SSO profile 505 | @Bean 506 | public WebSSOProfile webSSOprofile() { 507 | return new WebSSOProfileImpl(); 508 | } 509 | 510 | // SAML 2.0 Holder-of-Key Web SSO profile 511 | @Bean 512 | public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() { 513 | return new WebSSOProfileConsumerHoKImpl(); 514 | } 515 | 516 | // SAML 2.0 ECP profile 517 | @Bean 518 | public WebSSOProfileECPImpl ecpprofile() { 519 | return new WebSSOProfileECPImpl(); 520 | } 521 | 522 | // SAML 2.0 Logout profile 523 | @Bean 524 | public SingleLogoutProfile logoutprofile() { 525 | return new SingleLogoutProfileImpl(); 526 | } 527 | 528 | // Bindings, encoders and decoders used for creating and parsing messages 529 | @Bean 530 | public HTTPPostBinding postBinding() { 531 | return new HTTPPostBinding(parserPool(), velocityEngine()); 532 | } 533 | 534 | @Bean 535 | public HTTPRedirectDeflateBinding redirectDeflateBinding() { 536 | return new HTTPRedirectDeflateBinding(parserPool()); 537 | } 538 | 539 | @Bean 540 | public HTTPArtifactBinding artifactBinding() { 541 | ArtifactResolutionProfileImpl artifactResolutionProfile = new ArtifactResolutionProfileImpl(httpClient()); 542 | artifactResolutionProfile.setProcessor(new SAMLProcessorImpl(soapBinding())); 543 | return new HTTPArtifactBinding(parserPool(), velocityEngine(), artifactResolutionProfile); 544 | } 545 | 546 | @Bean 547 | public HTTPSOAP11Binding soapBinding() { 548 | return new HTTPSOAP11Binding(parserPool()); 549 | } 550 | 551 | @Bean 552 | public HTTPPAOS11Binding paosBinding() { 553 | return new HTTPPAOS11Binding(parserPool()); 554 | } 555 | 556 | // Initialization of the velocity engine 557 | @Bean 558 | public VelocityEngine velocityEngine() { 559 | return VelocityFactory.getEngine(); 560 | } 561 | 562 | // XML parser pool needed for OpenSAML parsing 563 | @Bean(initMethod = "initialize") 564 | public StaticBasicParserPool parserPool() { 565 | return new StaticBasicParserPool(); 566 | } 567 | 568 | @Bean 569 | public ParserPoolHolder parserPoolHolder() { 570 | return new ParserPoolHolder(); 571 | } 572 | } -------------------------------------------------------------------------------- /src/main/java/com/github/choonchernlim/security/adfs/saml2/SpringSecurityAdfsSaml2Exception.java: -------------------------------------------------------------------------------- 1 | package com.github.choonchernlim.security.adfs.saml2; 2 | 3 | /** 4 | * Module specific exception. 5 | */ 6 | public final class SpringSecurityAdfsSaml2Exception extends RuntimeException { 7 | 8 | SpringSecurityAdfsSaml2Exception(final String message) { 9 | super(message); 10 | } 11 | 12 | SpringSecurityAdfsSaml2Exception(final String message, final Throwable cause) { 13 | super(message, cause); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/site/site.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | ${project.name} 7 | ${project.url} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | org.apache.maven.skins 18 | maven-fluido-skin 19 | 1.5 20 | 21 | 22 | 23 | 24 |