I'm trying to make Keycloak work. I'm able to get the login flow to work, but not the logout and more immediately, I'm trying to add a /login route that goes to whatever the login is. If permitAll() isn't valid, I would have thought it would trigger a syntax error, but somehow, it creates this 8 mile long security chain, and blocks permitAll() in a few random inches in that chain.
To add this extra /login url, I followed another person's Stackoverflow recommendation to allow an extra /login url at
How change the default Spring Boot oauth urls (/login/oauth2/code and /oauth2/authorization)?
Basically, I changed the RequestMatcher in keycloakAuthenticationProcessingFilter. I thought this was valid, but commenting it out gets rid of the error. This appears to be the offending class:
package com.mycompany.myapplication.configurations;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.filter.AdapterStateCookieRequestMatcher;
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;
import org.keycloak.adapters.springsecurity.filter.QueryParamPresenceRequestMatcher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
#Configuration
#Order(200)
public class GeneticistKeycloakAuthenticationProcessingFilter
extends KeycloakWebSecurityConfigurerAdapter {
#Bean
#Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
final RequestMatcher customRequestMatcher =
new OrRequestMatcher(
new AntPathRequestMatcher(KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI),
new AntPathRequestMatcher("/login/**"),
new RequestHeaderRequestMatcher(KeycloakAuthenticationProcessingFilter.AUTHORIZATION_HEADER),
new QueryParamPresenceRequestMatcher(OAuth2Constants.ACCESS_TOKEN),
new AdapterStateCookieRequestMatcher()
);
return new KeycloakAuthenticationProcessingFilter(authenticationManagerBean(),customRequestMatcher);
}
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return null;
}
}
Is there some easier way to get Spring to take a URL as another way to login, then follow the normal login path?
This is a snippet of the stack trace:
Caused by: java.lang.IllegalStateException: permitAll only works with HttpSecurity.authorizeRequests()
at org.springframework.util.Assert.state(Assert.java:76)
at org.springframework.security.config.annotation.web.configurers.PermitAllSupport.permitAll(PermitAllSupport.java:51)
at org.springframework.security.config.annotation.web.configurers.PermitAllSupport.permitAll(PermitAllSupport.java:41)
at org.springframework.security.config.annotation.web.configurers.LogoutConfigurer.init(LogoutConfigurer.java:277)
at org.springframework.security.config.annotation.web.configurers.LogoutConfigurer.init(LogoutConfigurer.java:69)
at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.init(AbstractConfiguredSecurityBuilder.java:338)
at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:300)
at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:38)
at org.springframework.security.config.annotation.web.builders.WebSecurity.performBuild(WebSecurity.java:302)
at org.springframework.security.config.annotation.web.builders.WebSecurity.performBuild(WebSecurity.java:90)
at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:305)
at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:38)
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration.springSecurityFilterChain(WebSecurityConfiguration.java:127)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)
... 22 common frames omitted
Prior to 5.7.0 Spring Security didn't support permitAll in authorizeHttpRequests.
If you upgrade to Spring Security >= 5.7.0 or Spring Boot >= 2.7.0 you should be able to add permitAll() when using authorizeHttpRequests. This PR solved the issue.
Related
With spring security saml2 provider version 5.7.x mandatory validation of InResponseTo was introduced if it is provided in the authentication response.
Validation logic expects to find saved Saml2AuthenticationRequest in HttpSession. However that is only possible if SameSite attribute is not set.
According security requirements of current project I'm working on it is set to Lax or Strict. This configuration is done outside of the application. This causes loss of the session and request data.
Maybe someone already have dealed with an issue and knows how to deal with it? I don't see any way to disable validation or alternative way of saving request available in the library.
While upgrading spring security from 5.x to 6, I ran into the same issue with spring session cookie. There doesn't seem to be any standard solution for this. For now, I have provided an implementation of Saml2AuthenticationRequestRepository which saves the SAML request in database based on RelayState parameter. Following is the sample implementation using Mongo. Any backend (In-memory cache, Redis, MySQL etc.) can be used -
import java.util.Optional;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.stereotype.Repository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
#Repository
#RequiredArgsConstructor
#Slf4j
public class MongoSaml2AuthenticationRequestRepository implements Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {
public static final String SAML2_REQUEST_COLLECTION = "saml2RequestsRepository";
private final #NonNull MongoTemplate mongoTemplate;
#Override
public AbstractSaml2AuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) {
String relayState = request.getParameter(Saml2ParameterNames.RELAY_STATE);
if (relayState == null) {
return null;
}
log.debug("Fetching SAML2 Authentication Request by relay state : {}", relayState.get());
Query query = Query.query(Criteria.where("relayState").is(relayState.get()));
AbstractSaml2AuthenticationRequest authenticationRequest = mongoTemplate.findOne(query, AbstractSaml2AuthenticationRequest.class, SAML2_REQUEST_COLLECTION);
if (!authenticationRequest.getRelayState().equals(relayState.get())) {
log.error("Relay State received from request '{}' is different from saved request '{}'.", relayState.get(), authenticationRequest.getRelayState());
return null;
}
log.debug("SAML2 Request retrieved : {}", authenticationRequest);
return authenticationRequest;
}
#Override
public void saveAuthenticationRequest(AbstractSaml2AuthenticationRequest authenticationRequest,
HttpServletRequest request, HttpServletResponse response) {
//As per OpenSamlAuthenticationRequestResolver, it will always have value. However, one validation can be added to check for null and regenerate.
String relayState = authenticationRequest.getRelayState();
log.debug("Relay State Received: {}", relayState);
mongoTemplate.save(authenticationRequest, SAML2_REQUEST_COLLECTION);
}
#Override
public AbstractSaml2AuthenticationRequest removeAuthenticationRequest(HttpServletRequest request,
HttpServletResponse response) {
AbstractSaml2AuthenticationRequest authenticationRequest = loadAuthenticationRequest(request);
if (authenticationRequest == null) {
return null;
}
mongoTemplate.remove(authenticationRequest, SAML2_REQUEST_COLLECTION);
return authenticationRequest;
}
}
Post adding this, all the SAML validations including InResponseTo validation are passed successfully. Since RelayState is not supposed to change between SAML requests as per specifications, this seemed like reasonable alternative (in absence of any standard solution so far). I am running this through standard security testing and will update the solution with my findings if any.
I found a very simple example for LDAP authentication, which works just fine using an embedded LDAP server: https://github.com/asbnotebook/spring-boot/tree/master/spring-security-embedded-ldap-example . It is exactly what I need - one config class added and now all users are required to log in before accessing the application.
Since our AD (local server, not the Azure AD) requires userDN and password for access, I added this to the example code, also modified url, base dn etc.
When I attempt to log in, I always get the "Bad credentials" error message. I then stepped through the code and found that the Spring LDAP code successfully retrieves some user data from AD (I found the user email address in the "userDetails" object which is known only in the AD), however the "password" field is set to null. This null value is then compared to the password entered by the user which fails and a BadCredentialsException is thrown in function org.springframework.security.authentication.dao.additionalAuthenticationChecks().
So now I have two questions:
why is the "password" attribute set to null? My understanding is that it should contain the password hash. I checked the AD response with ldapsearch but I don't see anything looking like a password hash. However the userDN does work with other applications so it is probably not a problem with the userDN AD account. Please advise how to properly retrieve the password information.
I believe that the example does not handle password hashes. The LDIF file to preload the embedded LDAP server of the example application simply contains clear text passwords for the userPassword attribute. Also the passwordEncoder in the example code looks like a No Op Encoder. How should I change this to make it work with the AD?
Here is my code:
package com.asbnotebook.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.ldap.DefaultLdapUsernameToDnMapper;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.userdetails.LdapUserDetailsManager;
#Configuration
public class LdapSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public UserDetailsService userDetailsService() {
var cs = new DefaultSpringSecurityContextSource("ldaps://ad.company.local/dc=company,dc=local");
cs.setUserDn("cn=robot1,ou=robots");
cs.setPassword("secret");
cs.afterPropertiesSet();
var manager = new LdapUserDetailsManager(cs);
manager.setUsernameMapper(new DefaultLdapUsernameToDnMapper("ou=company_user", "cn"));
manager.setGroupSearchBase("ou=company_groups");
return manager;
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
After considering Gabriel Luci's comment, I have now found a simple way to authenticate with our ActiveDirectory:
package com.asbnotebook.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
#Configuration
public class LdapSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception
{
ActiveDirectoryLdapAuthenticationProvider adProvider =
new ActiveDirectoryLdapAuthenticationProvider(
"company.de","ldaps://ad.company.local","dc=company,dc=local");
adProvider.setConvertSubErrorCodesToExceptions(true);
adProvider.setUseAuthenticationRequestCredentials(true);
auth.authenticationProvider(adProvider);
auth.eraseCredentials(false);
}
}
Login is possible using either the email address or sAMAccountName.
I'm using Grails 4.0.9 and spring security core grails plugin 4.0.3, I searched for a way to capture the user failed and success login attempts but I didn't reach anything, is there a way to make this?
You could register ApplicationListener which listens to AbstractAuthenticationFailureEvent to handle failed attempts and register the same in grails-app/conf/spring/resources.groovy. Such as:
package com.objectcomputing.example
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.springframework.context.ApplicationListener
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent
#Slf4j
#CompileStatic
class LoginFailedAuthEventListener implements ApplicationListener<AbstractAuthenticationFailureEvent> {
#Override
void onApplicationEvent(AbstractAuthenticationFailureEvent event) {
final Object username = event.authentication.principal
log.warn("Failed login attempt for username {}", username)
}
}
Read more information at https://grails-plugins.github.io/grails-spring-security-core/4.0.x/index.html#registeringEventListener
Please note that the above listener is very basic which only logs the information but you could write whatever you need to do.
Example Application: https://github.com/puneetbehl/grails-spring-security-demo
I am running two different Payara Micro microservices in one cluster.
The issue I have is that when I try to access the OpenAPI URL of MyApp1 like http://mylink.com/myApp1/openapi it does not work. It actually works when I use URL http://mylink.com/openapi.
This becomes an issue when I want to see the API for the other microservice like http://mylink.com/myApp2/openapi which does not work.
Is there a way in Payara Micro of telling OpenAPI to use the application's context in it's path just like all the other URL in the application do?
As you can see in my previous comment, I've also struggled with the same situation.
Context - openapi and microprofile
First let me say that having /openapi URL in the root is the intended behaviour of microprofile-open. Documentation always uses /openapi path as the right to get the document LINK
In the implementation, is very clear that this behaviour is both wanted as enforced:
In the ServletContainerInitializer for OpenApi one can see the following code
// Only deploy to app root
if (!"".equals(ctx.getContextPath())) {
return;
}
Workaround aka Solution.
Now that is clear that we cannot configured this, since it's intended behaviour, one solution ( the one I'm proposing ) is to proxy the request to /YOUR_APP/openapi to /openapi.
Since my application is a jax-rs one, deployed on openshift, and I don't want to have a dedicated proxy application for this, I've just created a simple Resource/Controller to proxy this specific request for me.
The outstanding method behind:
#GET
#Path("")
public Response proxyOpenApiCall(){
log.debug("proxyOpenApiCall called");
String entity = client.target("http://localhost:8080")
.path("openapi").request()
.get(String.class);
return Response.ok(entity).build();
}
I was able to fix this with a small forward proxy. Therefore I create a new REST enpoint wich is callable from public and returns the content of internal http endpoint.
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.context.RequestScoped;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
#RequestScoped
#ApplicationPath("/")
#Path("/")
public class OpenApiProxyRestFacade extends Application {
private Client client;
#PostConstruct
public void init() {
this.client = ClientBuilder.newClient();
}
#GET
#Path("/openapi")
#Produces(MediaType.APPLICATION_JSON)
public Response proxyOpenApiCall() {
String entity = client.target("http://localhost:9080").path("openapi").request().get(String.class);
return Response.ok(entity).build();
}
#GET
#Path("/openapi/ui")
#Produces(MediaType.APPLICATION_JSON)
public Response proxyOpenApiUiCall() {
String entity = client.target("http://localhost:9080/openapi").path("ui").request().get(String.class);
return Response.ok(entity).build();
}
#PreDestroy
public void destroy() {
this.client.close();
}
}
For openapi, you can set this property for change of url, so it is configurable after all
mp.openapi.extensions.path=/yourapi/whatever
and for the openapi-UI set this
openapi.ui.yamlUrl=/yourapi/whatever
Sources: I first googled for mp.openapi.xxx parameters, (I found them in source code) which led me to this url
https://download.eclipse.org/microprofile/microprofile-open-api-1.0/microprofile-openapi-spec.html
and after looking for more stuff there was one simple sentence mentioning that there is also mp.openapi.extensions and after googling those further I found this random doc here https://github.com/wildfly/wildfly/blob/main/docs/src/main/asciidoc/_admin-guide/subsystem-configuration/MicroProfile_OpenAPI.adoc
I have a working UserService similar to HeroService from AngularDart tutorial with BrowserClient.
And now I need to use localStorage to save the API response.
After I import dart:html, I refresh my browser, suddenly I got error on console:
EXCEPTION: No provider found for dynamic: UserService -> dynamic.
When I remove the import, the service running well.
this is my UserService code.
import 'dart:convert';
import 'dart:html';
import 'package:http/http.dart';
import 'package:my_app/src/instance_logger.dart';
class UserService with InstanceLogger {
static final _headers = {'Content-Type': 'application/json'};
static const _authUrl = 'https://my.domain/api-token-auth/';
String get loggerPrefix => 'UserService'; // or set ''
final Client _http;
UserService(this._http);
dynamic _extractData(Response resp) => json.decode(resp.body)['data'];
Exception _handleError(dynamic e) {
log(e); // for demo purposes only
return Exception('Server error; cause: $e');
}
}
in Component meta-data:
providers: [ClassProvider(UserService)],
how to use dart:html within service? If I want to access localStorage.
thank you.
Both dart:html and package:http/http.dart unfortunately have a class named Client.
How you get around this collision is to hide an unwanted class from being imported with its package.
Change
import 'dart:html';
to
import 'dart:html' hide Client;
That will take care of the collision. You could alternatively give the import an alias such as:
import 'dart:html' as html;
Then you can use localStorage by using the prefix html.localStorage;
When I run into this problem I just hide the Client class from the dart:html import.