I've read in Spring Security Reference that AuthorizationFilter supersedes FilterSecurityInterceptor. So I'm trying to migrate my application to this newer method.
I have something like
http.authorizeRequests()
.mvcMatchers("/")
.hasIpAddress("127.0.0.1")
According to the linked page I should be able to write something like
http.authorizeHttpRequests()
.mvcMatchers("/")
.access("hasIpAddress('127.0.0.1')")
but there's no access(String) method. I even tried to paste verbatim code from the documentation:
#Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
// ...
.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/resources/**", "/signup", "/about").permitAll()
.mvcMatchers("/admin/**").hasRole("ADMIN")
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().denyAll()
);
return http.build();
}
which does not compile for the same reason.
Here's compilation error:
Application.java:103:55
java: incompatible types: java.lang.String cannot be converted to org.springframework.security.authorization.AuthorizationManager<org.springframework.security.web.access.intercept.RequestAuthorizationContext>
How do I use authorizeHttpRequests with IP addresses or string expression? Is it issue with documentation?
I'm using Spring Boot 2.7.0 and Spring Security 5.7.1
This does appear to be an issue with the docs. There is not currently a built-in implementation providing the hasIpAddress(String) access check, but you can use the IpAddressMatcher class to implement an AuthorizationManager capable of performing it.
Here's an example configuration:
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorizeRequests) -> authorizeRequests
.mvcMatchers("/").access(hasIpAddress("127.0.0.1"))
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
private static AuthorizationManager<RequestAuthorizationContext> hasIpAddress(String ipAddress) {
IpAddressMatcher ipAddressMatcher = new IpAddressMatcher(ipAddress);
return (authentication, context) -> {
HttpServletRequest request = context.getRequest();
return new AuthorizationDecision(ipAddressMatcher.matches(request));
};
}
}
Related
I am building a microservice using Spring Cloud Gateway and OAuth2 Resource Server. The app aims at redirecting to other microservices after doing the security part. I am trying to setup a filter before AnonymousAuthenticationFilter and handle my custom exception from there but however the custom exception filter is never being invoked. Following the security config I have in the app:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().cors().disable()
.httpBasic().disable()
.formLogin().disable()
.addFilterBefore(customExceptionHandler, AnonymousAuthenticationFilter.class)
.authorizeRequests( auth -> auth.antMatchers(AUTH_WHITELIST).permitAll()
.antMatchers("/**").authenticated())
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.jwt())
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
}
In my customExceptionHandler, I have the following code:
public class CustomExceptionHandler extends OncePerRequestFilter {
#Autowired
#Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("Spring Security Filter Chain Exception:", e);
resolver.resolveException(request, response, null, e);
}
}
}
Also following is my build.gradle:
// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Spring Cloud
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
I also have an #ExceptionAdvice class that handles all the exceptions. However, if I pass in an expired JWT or any other error scenario to the service, I always get handled by the following error message in my WWW-Authenticate header:
Bearer error="invalid_token", error_description="Jwt expired at 2022-06-16T19:58:09Z", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
How do I throw a custom POJO instead of this message?
This error is coming from BearerTokenAuthenticationEntryPoint, so to override the behavior you can just easily provide a custom entryPoint
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.jwt().and().authenticationEntryPoint(myCustomEntryPoint))
I have an application with multiple authentication types (i.e. Basic and a special Preauthorized login). I am attempting to add a SAML2 RelyingParty registration in my security configuration, where I am attempting to change the default path from:
/login/saml2/sso/{registrationId}
to
/auth/saml2/{registrationId}
So, I have the following setup:
public RelyingPartyRegistration provder1RelyingPartyRegistration() {
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation("classpath:provider1/metadata.xml")
.registrationId("provider1")
.assertionConsumerServiceLocation("{baseUrl}/auth/saml2/{registrationId}")
.build();
return registration;
}
// #Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
Collection<RelyingPartyRegistration> registrations = Collections.unmodifiableList(Arrays.asList(provider1RelyingPartyRegistration()));
InMemoryRelyingPartyRegistrationRepository repository = new InMemoryRelyingPartyRegistrationRepository(registrations);
return repository;
}
// fluff
#Override
protected void configure(HttpSecurity http) throws Exception {
final RequestMatcher filterRequestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher("/auth/basic"),
new AntPathRequestMatcher("/auth/preauth")
);
ApplicationAuthenticationProcessingFilter filter = new ApplicationAuthenticationProcessingFilter(filterRequestMatcher, authenticationManagerBean());
filter.setAuthenticationSuccessHandler(successHandler());
filter.setAuthenticationFailureHandler(failureHandler());
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.and()
.addFilterAfter(filter, LogoutFilter.class)
// fluff
.and()
.saml2Login()
.relyingPartyRegistrationRepository(relyingPartyRegistrationRepository())
.loginProcessingUrl("/auth/saml2/{registrationId}")
;
}
Unfortunately, I get this:
14 Dec 10:55:34 WARN [https-openssl-nio-127.0.0.1-444-exec-2] (DispatcherServlet.java:1278) - No mapping for POST /svc/auth/saml2/provider1
Can anyone tell me what I'm doing wrong trying to change that path? My application does NOT use Spring Boot, so I'm stuck with manual configuration.
EDIT
Some debugging has led to this hitting this line in the Saml2LoginConfigurer:
Map<String, String> providerUrlMap = getIdentityProviderUrlMap(
this.authenticationRequestEndpoint.filterProcessingUrl, this.relyingPartyRegistrationRepository);
Somehow, there's a default authenticationRequestEndpoint (since I didn't define one) setting the filterProcessingUrl to a value of /saml2/authenticate/{registrationId}. So, how do I override this?
The loginProcessingUrl is called by the asserting party after the authentication succeeds, which contains in the request the SAMLResponse parameter.
What you are trying to change is the URL to process an authentication request (create the SAMLRequest and send to the asserting party), this is done by the Saml2WebSsoAuthenticationRequestFilter class. To change the redirectMatcher you have to provide an ObjectPostProcessor, see this issue.
ObjectPostProcessor<Saml2WebSsoAuthenticationRequestFilter> processor = new ObjectPostProcessor<>() {
#Override
public <O extends Saml2WebSsoAuthenticationRequestFilter> O postProcess(O filter) {
filter.setRedirectMatcher(new AntPathRequestMatcher("/my/custom/url"));
return filter;
}
};
http.saml2Login().addObjectPostProcessor(processor);
Take a look at SAML 2.0 Login Overview for more detail about the flow.
I have an application that connects to a SAML idP that only supports the POST Binding. After configuring my application which uses spring-security-saml2-service-provider to manually create a POST Authentication request, I looked at the XML that got generated and saw that it included the Signature information (which is expected) but not the Key Info. Then in the logs, I noticed it said:
No KeyInfoGenerator was supplied in parameters or resolveable for credential type org.opensaml.security.x509.X509Credential, No KeyInfo will be generated for Signature
This is what my code looks like to manually generate the POST Authentication request:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.disable()
.csrf()
.disable();
}
#Bean
public RelyingPartyRegistration nnanetRelyingPartyRegistration() {
SAMLMetadataSignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
return RelyingPartyRegistrations
.fromMetadataLocation("https://example.com/metadata.xml")
.entityId("example")
.registrationId("nnanet")
.assertingPartyDetails(party -> {
party.wantAuthnRequestsSigned(true)
.singleSignOnServiceLocation("https://example.com/login")
.entityId("https://example.com/login");
.verificationX509Credentials(saml2X509Credentials -> {
saml2X509Credentials.add(getVerificationCertificate());
});
})
.signingX509Credentials(saml2X509Credentials -> {
saml2X509Credentials.add(getSigningCredential());
})
.decryptionX509Credentials(saml2X509Credentials -> {
saml2X509Credentials.add(getSigningCredential());
})
.build();
}
#Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration());
}
#Bean
public Saml2PostAuthenticationRequest saml2PostAuthenticationRequest() {
Saml2AuthenticationRequestContext.Builder contextBuilder = Saml2AuthenticationRequestContext.builder();
contextBuilder.assertionConsumerServiceUrl("http://localhost:8080/saml/SSO");
contextBuilder.relyingPartyRegistration(relyingPartyRegistration());
contextBuilder.issuer("issuer");
OpenSamlAuthenticationRequestFactory factory = new OpenSamlAuthenticationRequestFactory();
return factory.createPostAuthenticationRequest(contextBuilder.build());
}
I just call the saml2PostAuthenticationRequest() method from my Controller and generate a form to submit automatically due to some reasons that are outside the scope of this question. After looking further into the OpenSamlAuthenticationRequestFactory, it looks like this is creating the SignatureSigningParameters specifically without including the KeyInfoGenerator as it's only being created in the private methods. Does anyone have an idea on how to get around this, or perhaps point out if I'm doing something wrong?
Thanks!
This will be addressed in a future version of Spring Security SAML 2
Please review the PR - https://github.com/spring-projects/spring-security/pull/9746
I am attempting to replace CAS with Azure Active Directory SAML authentication (SSO) in a Spring Boot API. My version of Spring Security is 5.3.2. Spring boot is 2.3.0.
Documentation has been hard to find. I think this is explained by 8685. I found 8010 and attempted the workaround mentioned there, but my breakpoints there are not getting hit.
Given the current state of the transition from SAML Extension to Spring Security, should I be using the old SAML Extension? I can reach my "success" endpoint with a JSESSIONID and SAMLReponse, but it's encrypted. Is this something I need to do myself? (If so, how?) The SecurityContext / user details are not getting set. I see AccessDenied stack traces in my logs, but I think that's a symptom of the Anonymous user context.
Relevant code is below. I have application.yml and application.properties files, but all config is annotations-based. If you see anything way off base, please let me know! Any guidance would be appreciated.
Here is my SecurityConfig:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
RelyingPartyRegistration getSaml2AuthenticationConfiguration() throws Exception {
// remote IDP entity ID
String idpEntityId = "https://sts.windows.net/xxxxxxxxxxxx/";
// remote WebSSO Endpoint - Where to Send AuthNRequests to
String webSsoEndpoint = "https://login.microsoftonline.com/xxxxxxxxxxxx/saml2";
// local registration ID
String registrationId = "xxxxxxxxxxxx";
// local entity ID - autogenerated based on URL
String localEntityIdTemplate = "xxxxxxxxxxxx.local";
// local signing (and decryption key)
Saml2X509Credential signingCredential = getSigningCredential(); //private method not included
// IDP certificate for verification of incoming messages
Saml2X509Credential idpVerificationCertificate = getVerificationCertificate(); //private method not included
String acsUrlTemplate = "https://xxxxxxxxxxxx.local/success"; //REST endpoint, see below
return RelyingPartyRegistration.withRegistrationId(registrationId)
.providerDetails(config -> config.entityId(idpEntityId))
.providerDetails(config -> config.webSsoUrl(webSsoEndpoint)).credentials(c -> c.add(signingCredential))
.credentials(c -> c.add(idpVerificationCertificate)).localEntityIdTemplate(localEntityIdTemplate)
.assertionConsumerServiceUrlTemplate(acsUrlTemplate).build();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// Just a test
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
http
.headers()
.frameOptions()
.sameOrigin()
.httpStrictTransportSecurity()
.disable()
.and()
.authorizeRequests()
//... more antMatchers and permitAlls
.antMatchers("/success").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/logout").permitAll()
.antMatchers("/error").permitAll().anyRequest().authenticated().and()
.csrf().disable()
.saml2Login(
saml2 -> {
try {
saml2
.authenticationManager(a -> {
// This code is never reached
Authentication result = provider.authenticate(a);
Saml2Authentication saml2Authentication = (Saml2Authentication) result;
return result;
}).relyingPartyRegistrationRepository(
new InMemoryRelyingPartyRegistrationRepository(getSaml2AuthenticationConfiguration())
)
.loginProcessingUrl("/login/{registrationId}");
} catch (Exception e) {
// It made me put this try/catch here... this isn't getting reached either
e.printStackTrace();
}
});
}
}
And my REST endpoint:
#RestController
public class HelloController {
#RequestMapping(value = "/success", method=RequestMethod.POST)
public String saml2Post(HttpServletRequest request) throws IOException {
String jSessionId = request.getHeader("cookie");
System.out.println(jSessionId);
String samlResponse = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
System.out.println(samlResponse);
return "login success";
}
}
And my gradle dependencies (Gradle 6.5):
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
compile 'org.springframework.security:spring-security-config'
compile 'org.springframework.security:spring-security-saml2-service-provider'
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
compile 'org.springframework.boot:spring-boot-starter-web'
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
implementation 'org.springframework.boot:spring-boot-starter-freemarker'
implementation 'org.springframework.boot:spring-boot-starter-integration'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-mail'
compile 'org.springframework.security:spring-security-oauth2-client'
compile 'org.springframework.security:spring-security-oauth2-jose'
implementation 'joda-time:joda-time:2.10.6'
implementation 'com.google.guava:guava:29.0-jre'
implementation 'com.opencsv:opencsv:5.2'
implementation 'org.apache.commons:commons-lang3:3.10'
implementation 'net.minidev:json-smart:2.3'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc'
runtimeOnly 'org.hsqldb:hsqldb'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testImplementation 'org.springframework.integration:spring-integration-test'
testImplementation 'org.springframework.security:spring-security-test'
}
I created a local LDAP server and added the user "djiao" with password "123456
Trying to implement authentication with Spring Security with Spring Boot. My webconfig class is as follows:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin();
}
#Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider("", "ldap://localhost:10389");
provider.setConvertSubErrorCodesToExceptions(true);
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
return provider;
}
#Bean
public LoggerListener loggerListener() {
return new LoggerListener();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
However I can't seem to login from the login page.
If I use djiao (cn) or djiao1 (uid), I will get 500.
[LDAP: error code 34 - Incorrect DN given : djiao1 (0x64 0x6A 0x69 0x61 0x6F 0x31 ) is invalid]; nested exception is javax.naming.InvalidNameException: [LDAP: error code 34 - Incorrect DN given : djiao1 (0x64 0x6A 0x69 0x61 0x6F 0x31 ) is invalid]
If I use dn "cn=djiao,ou=Users,dc=example,dc=com" as the username I will get "Bad credentials" error. And the password is simply 123456.
What should the username for login? Or am I missing something in websecurityconfig class?
Since from your code I could identify that you're using Spring-Boot.
This is what was working for us connecting to LDAP
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder authBuilder) throws Exception {
authBuilder
.ldapAuthentication()
.userSearchFilter("(sAMAccountName={0})")
.userSearchBase("dc=some,dc=domain,dc=com")
.groupSearchBase("ou=groups,dc=some,dc=domain,dc=com")
.groupSearchFilter("member={0}")
.contextSource()
.url("ldaps://<ldap-server>")
.port(639)
.managerDn("cn=binduser,ou=users,dc=some,dc=domain,dc=com")
.managerPassword("some pass")
;
}
}
So in essence going for the userSearchFilter you'd have to define different values. If you use any LDAP besides AD your filter should by "(uid={0})" or if you wan't people to be able to use the email you could also go for "(mail={0})" or a combination like "(|(uid={0})(mail={0}))" which woul allow to use both.
If you go for ActiveDirectory – which I assume you do not based on what you have written above – it should be the sAMAccountName as stated above to allow people to just enter their ID in the domain like MYDOMAIN\myusername so the login would just be myusername.
If you need to connect to multiple LDAP-Server who share the same information for HA purposes you can do this through the .contextSource().url() call. If they carry different ones, e.g. 'EMEA', 'US', 'AP' you can combine these calls using:
#Autowired
public void configureGlobal(AuthenticationManagerBuilder authBuilder) throws Exception {
authBuilder
.ldapAuthentication()
.userSearchFilter("(sAMAccountName={0})")
.userSearchBase("dc=emea,dc=domain,dc=com")
.groupSearchBase("ou=groups,dc=emea,dc=domain,dc=com")
.groupSearchFilter("member={0}")
.contextSource()
.url("ldaps://<emea-ldap-server>")
.port(639)
.managerDn("cn=binduser,ou=users,dc=emea,dc=domain,dc=com")
.managerPassword("some pass")
.and()
.and()
.ldapAuthentication()
.userSearchFilter("(sAMAccountName={0})")
.userSearchBase("dc=ap,dc=domain,dc=com")
.groupSearchBase("ou=groups,dc=ap,dc=domain,dc=com")
.groupSearchFilter("member={0}")
.contextSource()
.url("ldaps://<ap-ldap-server>")
.port(639)
.managerDn("cn=binduser,ou=users,dc=ap,dc=domain,dc=com")
.managerPassword("some pass")
;
}
BTW: this also allows you to combine different authentication mechanisms like InMemory (Default-Admin-Backdoor) with LDAP and/or JDBC.