Spring Security WebFlux and LDAP - spring-boot

What customization's are required in order to secure a Reactive Spring Boot application with LDAP? The examples I've seen so far are based on Spring MVC and the example for securing a WebFlux only shows a simple Reactive example with an in-memory Map.

Here is one solution for this that I have come up with and tested.
Deserving special attention is this information in this class: ReactiveAuthenticationManagerAdapter. There, it states:
Adapts an AuthenticationManager to the reactive APIs. This is somewhat
necessary because many of the ways that credentials are stored (i.e.
JDBC, LDAP, etc) do not have reactive implementations. What's more is
it is generally considered best practice to store passwords in a hash
that is intentionally slow which would block ever request from coming
in unless it was put on another thread.
First, create a configuration class. This will handle the connectivity to LDAP.
#Configuration
public class ReactiveLdapAuthenticationConfig {
// Set this in your application.properties, or hardcode if you want.
#Value("${spring.ldap.urls}")
private String ldapUrl;
#Bean
ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) {
BindAuthenticator ba = new BindAuthenticator(contextSource);
ba.setUserDnPatterns(new String[] { "cn={0},ou=people" } );
LdapAuthenticationProvider lap = new LdapAuthenticationProvider(ba);
AuthenticationManager am = new ProviderManager(Arrays.asList(lap));
return new ReactiveAuthenticationManagerAdapter(am);
}
#Bean
BaseLdapPathContextSource contextSource() {
LdapContextSource ctx = new LdapContextSource();
ctx.setUrl(ldapUrl);
ctx.afterPropertiesSet();
return ctx;
}
}
After that, you'll want to configure your security following the patterns here. The most basic chain configuration is about this:
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.anyExchange().authenticated()
.and()
.httpBasic();
return http.build();
}
For completeness, you'll want to make sure you have these:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
Other References
EnableReactiveMethodSecurity
BindAuthenticator
LdapAuthenticationProvider
ProviderManager

The above example didn't work for me using Windows Active Directory. I could get LDAP Authentication to work in stand-alone (non-Spring) Java, but the above solution always gave me error 52e (user known, but invalid password).
Following on from the example above, I used the same pom.xml and #EnableWebFluxSecurity ... SecurityWebFilterChain(...), but with the following;
#Configuration
public class ReactiveLdapAuthenticatoinConfig {
#Bean
ReactiveAuthenticationManager authenticationManager() {
ActiveDirectoryLdapAuthenticationProvider adlap =
new ActiveDirectoryLdapAuthenticationProvider(
"{my.domain}",
"ldap://{my.ldap.server}.{my.domain}"
);
AuthenticationManager am = new ProviderManager(Arrays.asList(adlap));
return new ReactiveAuthenticationManagerAdapter(am);
}
}
In order to return the signed-in user, one would use something like;
#GetMapping(value = '/user')
public Mono<String> getUser(Mono<Principal> principal) {
return principal.map(Principal::getName);
}

Related

Unable to store spring session in Hazelcast

I'm developing a spring boot application with form login. I have a problem if my application works as more than one replica.
When I check where session is located, I found InMemoryWebSessionStore service. And I understood the source of the problem. I made sure that I had to store the session in a central point like redis, hazelcast.
I researched how to do this, and I read that I should use spring-session.
I faced following error:
Caused by:
org.springframework.boot.autoconfigure.session.SessionRepositoryUnavailableException:
No session repository could be auto-configured, check your
configuration (session store type is 'hazelcast')
application.yml:
spring:
session:
store-type: hazelcast
First of all am I on the right way? Anyone have a better solution?
SecurityConfig.kt
#EnableWebFluxSecurity
class SecurityConfig {
#Bean
fun adminWebFilterChain(
http: ServerHttpSecurity,
userService: UserService,
passwordEncoder: PasswordEncoder
): SecurityWebFilterChain {
val userDetailsService = CustomUserDetailsService(userService, passwordEncoder)
val manager = CustomUserAuthenticationManager(userDetailsService)
manager.setPasswordEncoder(passwordEncoder)
return http
.csrf().disable()
.authenticationManager(manager)
.authorizeExchange()
.pathMatchers("/login", "/logout").permitAll()
.anyExchange().authenticated()
.and().formLogin()
.and().logout()
.and().build()
}
#Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
}
HazelcastHttpSessionConfig.kt
#Configuration
#EnableHazelcastHttpSession
class HazelcastHttpSessionConfig {}
Dependencies:
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("io.micrometer:micrometer-registry-prometheus")
implementation("org.springframework.session:spring-session-core")
implementation("org.springframework.session:spring-session-hazelcast")
Am I on the right way? Do you have a better solution?
And do you have any idea about an error that I faced?
Here is what I have done to implement Spring Session with Hazelcast.
HazelcastConfig.java
#Configuration
#EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = 3600)
public class HazelcastConfig {
#Bean
public Config hazelCastConfig() {
final Config config = new Config().setInstanceName("hazelcast-instance");
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
.addMapAttributeConfig(springSessionAttributeConfig()).addMapIndexConfig(
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
return config;
}
private MapAttributeConfig springSessionAttributeConfig() {
return new MapAttributeConfig()
.setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
.setExtractor(PrincipalNameExtractor.class.getName());
}
}
In my pom.xml I have the following:
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-hazelcast</artifactId>
</dependency>
I don't set the spring.session.store-type. You can tailor the configuration to your needs (adding kubernetes support, using it as a distributed query cache, etc).

Java - Spring security, Shibboleth (apache) and onelogin

The actual Spring Security configuration is like this:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/uri1/**").hasAuthority(Permission.AUTHORITY1.toString())
.antMatchers("/uri2/**").hasAuthority(Permission.AUTHORITY2.toString())
.anyRequest().hasAuthority(Permission.AUTHORITY3.toString())
.and().httpBasic()
.realmName("App").and().csrf().disable();
http.authorizeRequests();
http.headers().frameOptions().sameOrigin().cacheControl().disable();
}
#Bean
public Filter shallowEtagHeaderFilter() {
return new ShallowEtagHeaderFilter();
}
}
And the web MVC configuration is like this:
#Configuration
public class DefaultView extends WebMvcConfigurerAdapter{
#Override
public void addViewControllers( ViewControllerRegistry registry ) {
registry.addViewController( "/" ).setViewName( "forward:myPage.html" );
registry.setOrder( Ordered.HIGHEST_PRECEDENCE);
super.addViewControllers( registry );
}
}
I have to replace the httpBasic authentification done in Spring Security by an authentification using onelogin (so with SAML if I understood what I found on the Internet).
By doing research, I found that a possibility was to use Shibboleth on the Apache server and an other was to use a plugin in Spring Security to manage SAML.
For the first solution (shibboleth), the aim is to manage onelogin authentification directly on Apache server (if not connected, the user is redirected on onelogin authentification page, if connected, the ressource is accessible) and to have needed informations returned in SAML response (like username and other need data) in the header of the request (to be abble to have them in Spring app).
With this solution, is it possible to keep httpBasic authentification in Spring security and to have "Basic XXXX" in the header of each request set by Shibboleth? Or, have I to remove the httpBasic authentification from Spring Security?
For the second solution (plugin to manage SAML in Spring Security), is it the same result as the first solution and how it must be implemented?
Thank you in advance for your reply.
welcome to stackoverflow.
... and to have needed informations returned in SAML response (like
username and other need data) in the header of the request (to be
abble to have them in Spring app)
If I understood correctly, you are already using spring security. This means your application is already using spring security populated context for authentication and authorization in your controller/service layers. If you use said approach, where apache is populating the authenticate user information in headers, than this is NOT going to populate the spring security context all by itself UNLESS you add a preAuthFilter in your chain to extract this information and populate your spring context appropriately.
With this solution, is it possible to keep httpBasic authentification
in Spring security and to have "Basic XXXX" in the header of each
request set by Shibboleth? Or, have I to remove the httpBasic
authentification from Spring Security?
If you are able to do it then what I said above would be a bit relaxed. Having said that, to best of my knowledge, there is no option where you can deduce a Basic authentication header using shibboleth apache module. In addition, I'll also advice to be careful with this approach since, with this approach, you'll still have to authenticate the user in your app with a dummy password (since you are NOT going to get user's correct password via SAML in this header) and this opens up your application for security exploits. I'll strongly advise against this approach. Shibboleth already has some Spoof Checking covered in their documentation. 
[EDIT]
Based on the additional information, following is what you can do to achieve all handling by apache and still use spring security effectively
First provide implementation of PreAuthenticatedAuthenticationToken in your application, you can use AbstractPreAuthenticatedProcessingFilter for this purpose. A skeleton for the implementation is provided below, this is excerpt from one of my past work and very much stripped down keeping only the essential elements which are relevant for your scenario. Also take a close look at AuthenticationManager and Authentication docs and make sure you fully understand what to use and for what purpose. Please read javadocs for all these 4 classes carefully to understand the contract as it can be confusing to get it right in spring security otherwise. I have added necessary details as TODO and comments in skeleton blow that you'll have to fill in yourself in your implementation.
public class ShibbolethAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
private final String containsValidPrincipalHeader = "_valid_shibboleth_header_present";
private final String shibbolethHeader = "_shibboleth_header";
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* This authentication manager's authenticate method MUST return a fully populated
* org.springframework.security.core.Authentication object. You may very well use
* either PreAuthenticatedAuthenticationToken OR UsernamePasswordAuthenticationToken
* with any credentials set, most important is to correctly populate the Authorities
* in the returned object so that hasAuthority checks works as expected.
*
* Another point, you can use authentication.getPrincipal() in the implementation
* of authenticate method to access the same principal object as returned by
* getPreAuthenticatedPrincipal method of this bean. So basically you pass the
* using Principal object from this bean to AuthenticationManager's authenticate
* method which in turn return a fully populated spring's Authentication object
* with fully populated Authorities.
*/
#Autowired
private ShibbolethAuthenticationManager authenticationManager;
#Override
public void afterPropertiesSet() {
setAuthenticationManager(authenticationManager);
super.afterPropertiesSet();
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String authHeader = request.getHeader(shibbolethHeader);
if (authHeader == null) {
logger.trace("No {} header found, skipping Shibboleth Authentication", shibbolethHeader);
return null;
}
// TODO - validate if all header and it's contents are what they should be
ShibbolethAuthToken authToken = /* TODO - provide your own impl to supply java.security.Principal object here */;
request.setAttribute(containsValidPrincipalHeader, Boolean.TRUE);
return authToken;
}
/**
* No password required thus Credentials will return null
*/
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
if (Boolean.TRUE.equals(request.getAttribute(containsValidPrincipalHeader)))
return System.currentTimeMillis(); // just returning non null value to satisfy spring security contract
logger.trace("Returning null Credentials for non authenticated request");
return null;
}
}
Register this as servlet filter in your app using following registrar
#Configuration
public class ShibbolethFilterRegistrar {
/*
* We don't want to register Shibboleth Filter in spring global chain thus
* added this explicit registration bean to disable just that.
*/
#Bean
public FilterRegistrationBean shibbolethFilterRegistrar(Shibboleth shibbolethAuthFilter) {
FilterRegistrationBean registration = new FilterRegistrationBean(shibbolethAuthFilter);
registration.setEnabled(false);
return registration;
}
#Bean
public ShibbolethAuthFilter shibbolethAuthFilter() {
return new ShibbolethAuthFilter();
}
}
Followed by this, change your WebSecurityConfig to following
#Override
protected void configure(HttpSecurity http) throws Exception {
/* autowire shibbolethAuthFilter bean as well */
http
.addFilterBefore(shibbolethAuthFilter, AbstractPreAuthenticatedProcessingFilter.class);
.authorizeRequests()
.antMatchers("/uri1/**").hasAuthority(Permission.AUTHORITY1.toString())
.antMatchers("/uri2/**").hasAuthority(Permission.AUTHORITY2.toString())
.anyRequest().hasAuthority(Permission.AUTHORITY3.toString())
.and()
.realmName("App").and().csrf().disable();
http.authorizeRequests();
http.headers().frameOptions().sameOrigin().cacheControl().disable();
}
Hope these pointers helps you to integrate external auth successfully.
IMHO, following is still valid - as much as I have understood your scenario, if I had to do it, I'll personally prefer to use spring security inbuilt SAML auth for this purpose since that provides very smooth integration with spring security in every possible context within the framework. In addition, it also simplifies my deployment scenario where I'll also have to take care of provisioning apache which'll typically fall under additional workload for DevOps team. For simplicity and scalability, spring security inbuilt SAML SSO support would be my first choice unless there's a constraint which is forcing me to do otherwise (which I am not able to see in current discussion context based on the explanation provided). There are ample tutorials and examples available on net to get it done. I know this is not what you asked for but I thought to share with you what I have done myself in past for similar SSO solutions in spring distributed apps and learning that I had. Hope it helps!!
This is the entire solution I used to connect to my application using Onelogin, shibboleth (Apache) and Spring Security. I used http but you have to adapt if you want to use https.
Onelogin
Configure a "SAML Test Connector (SP Shibboleth)" with the following configuration:
Login URL : http://myserver:<port>/my-app
ACS (Consumer) URL : http://myserver:<port>/Shibboleth.sso/SAML2/POST
SAML Recipient : http://myserver:<port>/Shibboleth.sso/SAML2/POST
SAML Single Logout URL : http://myserver:<port>/Shibboleth.sso/Logout
ACS (Consumer) URL Validator : ^http://myserver:<port>/Shibboleth.sso/SAML2/POST$
Audience : http://myserver:<port>/my-app
An parameter "username" has been added and a value is defined for this parameter for each user.
Apache and shibboleth
See: https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPJavaInstall
I installed shibboleth.
I activated AJP (module mod_proxy_ajp). It is recommended to use AJP instead of HTTP request headers.
I updated my apache conf file:
<VirtualHost *:[port]>
...
ProxyIOBufferSize 65536
<location /my-app >
ProxyPass "ajp://myappserver:<portAJPApp>"
AuthType shibboleth
ShibRequestSetting requireSession 1
Require valid-user
ProxyPassReverse /
ProxyHTMLEnable On
ProxyHTMLURLMap http://myappserver:<portHttpApp>/ /my-app/
ProxyHTMLURLMap / /my-app/
</location>
<Location /Shibboleth.sso>
SetHandler shib
</Location>
...
</VirtualHost>
In shibboleth2.xml:
<SPConfig xmlns="urn:mace:shibboleth:2.0:native:sp:config"
xmlns:conf="urn:mace:shibboleth:2.0:native:sp:config"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
clockSkew="180">
...
<ApplicationDefaults id="default" policyId="default"
entityID="http://myserver:<port>/my-app"
REMOTE_USER="eppn persistent-id targeted-id"
signing="false" encryption="false"
attributePrefix="AJP_">
<!-- entityId in IdP metadata file -->
<SSO entityID="https://app.onelogin.com/saml/metadata/XXXX">
SAML2
</SSO>
<MetadataProvider type="XML"
uri="https://app.onelogin.com/saml/metadata/XXX"
backingFilePath="onelogin_metadata.xml" reloadInterval="7200">
</MetadataProvider>
</ApplicationDefaults>
...
</SPConfig>
In attribute-map.xml:
<Attributes xmlns="urn:mace:shibboleth:2.0:attribute-map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
...
<!-- OneLogin attributes: "name" corresponds to the attribute name defined in Onelogin and received in SAML response. "id" is the name of the attribute in shibboleth session accissible by http://myserver:<port>/Shibboleth.sso/Session -->
<Attribute name="username" nameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" id="username">
<AttributeDecoder xsi:type="StringAttributeDecoder"/>
</Attribute>
...
</Attributes>
Spring-boot
Tomcat configuration to add an AJP connector (attributes loaded from yml with a "server" property):
#Configuration
#ConfigurationProperties(prefix = "server")
public class TomcatConfiguration {
private int ajpPort;
private boolean ajpAllowTrace;
private boolean ajpSecure;
private String ajpScheme;
private boolean ajpEnabled;
#Bean
public EmbeddedServletContainerCustomizer customizer() {
return container -> {
if (container instanceof TomcatEmbeddedServletContainerFactory) {
TomcatEmbeddedServletContainerFactory tomcatServletFactory = ((TomcatEmbeddedServletContainerFactory) container);
...
// New connector for AJP
// Doc: http://tomcat.apache.org/tomcat-7.0-doc/config/ajp.html
if (isAjpEnabled()) {
Connector ajpConnector = new Connector("AJP/1.3");
ajpConnector.setPort(getAjpPort());
ajpConnector.setSecure(isAjpSecure());
ajpConnector.setAllowTrace(isAjpAllowTrace());
ajpConnector.setScheme(getAjpScheme());
ajpConnector.setAttribute("packetSize", 65536);
tomcatServletFactory.addAdditionalTomcatConnectors(ajpConnector);
}
}
};
}
// Getters and setters
}
Spring security configuration (the shibboleth filter can be activated through yml with a "shibboleth-filter" property defined in an "authentication" property):
#Configuration
#ConfigurationProperties(prefix = "authentication")
#EnableWebSecurity
#Import(ShibbolethFilterRegistrar.class)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private boolean shibbolethFilter;
#Autowired
private ShibbolethAuthFilter shibbolethAuthFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
if(isShibbolethFilter()) {
http.addFilterBefore(shibbolethAuthFilter, AbstractPreAuthenticatedProcessingFilter.class)
.authorizeRequests()
.antMatchers("/uri1/**").hasAuthority(Permission.AUTHORITY1.toString())
.antMatchers("/uri2/**").hasAuthority(Permission.AUTHORITY2.toString())
.anyRequest().hasAuthority(Permission.AUTHORITY3.toString())
.and().csrf().disable();
http.authorizeRequests();
http.headers().frameOptions().sameOrigin().cacheControl().disable();
}
else {
http
.authorizeRequests()
.antMatchers("/uri1/**").hasAuthority(Permission.AUTHORITY1.toString())
.antMatchers("/uri2/**").hasAuthority(Permission.AUTHORITY2.toString())
.anyRequest().hasAuthority(Permission.AUTHORITY3.toString())
.and().httpBasic()
.realmName("MyApp")
.and().csrf().disable();
http.authorizeRequests();
http.headers().frameOptions().sameOrigin().cacheControl().disable();
}
}
// Getter and setter for shibbolethFilter loaded from yml
}
ShibbolethFilterRegistrar:
#Configuration
public class ShibbolethFilterRegistrar {
#Bean
public ShibbolethAuthenticationManager shibbolethAuthenticationManager() {
return new ShibbolethAuthenticationManager();
}
#Bean
public FilterRegistrationBean shibbolethFilterRegistration(ShibbolethAuthFilter shibbolethAuthFilter) {
FilterRegistrationBean registration = new FilterRegistrationBean(shibbolethAuthFilter);
registration.setEnabled(false);
return registration;
}
#Bean
public ShibbolethAuthFilter shibbolethAuthFilter() {
return new ShibbolethAuthFilter();
}
}
ShibbolethAuthFilter:
public class ShibbolethAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
private static final String USERNAME_ATTRIBUTE_NAME = "username";
private static final String VALID_SHIBBOLETH_ATTR = "_valid_shibboleth_attribute";
#Autowired
private ShibbolethAuthenticationManager shibbolethAuthenticationManager;
#Override
public void afterPropertiesSet() {
setAuthenticationManager(shibbolethAuthenticationManager);
super.afterPropertiesSet();
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
// Attribute received in AJP request
Object username = request.getAttribute(USERNAME_ATTRIBUTE_NAME);
if(username == null) {
return null;
}
request.setAttribute(VALID_SHIBBOLETH_ATTR, Boolean.TRUE);
ShibbolethAuthToken authToken = new ShibbolethAuthToken(username.toString());
return authToken;
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
if (Boolean.TRUE.equals(request.getAttribute(VALID_SHIBBOLETH_ATTR))) {
return System.currentTimeMillis(); // just returning non null value to satisfy spring security contract
}
logger.trace("Returning null Credentials for non authenticated request");
return null;
}
}
ShibbolethAuthenticationManager:
public class ShibbolethAuthenticationManager implements AuthenticationManager {
#Autowired
private MyAuthenticationProvider myAuthenticationProvider;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
ShibbolethAuthToken principal = (ShibbolethAuthToken) authentication.getPrincipal();
Object credentials = authentication.getCredentials();
UserDetails userDetails = myAuthenticationProvider.loadUserByUsername(principal.getName());
if(userDetails == null || userDetails.getAuthorities() == null || userDetails.getAuthorities().isEmpty()) {
throw new BadCredentialsException("User rights cannot be retrieved for user " + principal.getName());
}
return new PreAuthenticatedAuthenticationToken(principal, credentials, userDetails.getAuthorities());
}
}
ShibbolethAuthToken implements Principal.
Thank you for your help.

Camel REST and Spring Security Java Configuration

I'm trying to set up Camel REST to use basic auth with a simple username/password from my application.properties and can't for the life of me seem to configure Camel Spring Security to do that. I'm trying to follow the Spring Security component documentation which seems to be missing the example of configuring the required beans. I found the missing example here under 'Controlling access to Camel routes' but this only shows the xml configuration.
How do I set up the required SpringSecurityAuthorizationPolicy bean? It needs an AuthenticationManager and an AccessDecisionManager and it also seems to require that I set its SpringSecurityAccessPolicy which I have no idea how to do.
I haven't gotten to test these yet, because I can't get my beans set up, but my rest route looks like:
rest("/ingest")
.post("/json").consumes("application/json")
.route()
.process(authProcessor)
.policy(authPolicy) // this is the bean I don't know how to configure
.to("direct:ingest")
.endRest();
and my AuthProcessor (taken from the camel component doc) looks like:
#Component
public class AuthProcessor implements Processor {
public void process(Exchange exchange) {
String userpass = new String(Base64.decodeBase64(exchange.getIn().getHeader("Authorization", String.class)));
String[] tokens = userpass.split(":");
// create an Authentication object
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(tokens[0], tokens[1]);
// wrap it in a Subject
Subject subject = new Subject();
subject.getPrincipals().add(authToken);
// place the Subject in the In message
exchange.getIn().setHeader(Exchange.AUTHENTICATION, subject);
}
}
and here's my broken bean configuration for what it's worth:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public SpringSecurityAuthorizationPolicy springSecurityAuthorizationPolicy(
AuthenticationManager authenticationManager, AccessDecisionManager accessDecisionManager) {
SpringSecurityAuthorizationPolicy policy = new SpringSecurityAuthorizationPolicy();
SpringSecurityAccessPolicy springSecurityAccessPolicy = new SpringSecurityAccessPolicy();
policy.setAuthenticationManager(authenticationManager);
policy.setAccessDecisionManager(accessDecisionManager);
policy.setSpringSecurityAccessPolicy(????);
return policy;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("pass").roles("USER");
}
#Bean(name = BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public AccessDecisionManager accessDecisionManager() {
AffirmativeBased affirmativeBased = new AffirmativeBased(ImmutableList.of(
new RoleVoter()
));
affirmativeBased.setAllowIfAllAbstainDecisions(true);
return affirmativeBased;
}
}
I've been banging my head against the wall trying to understand this so an example of how to do this would be amazing. It looks like the xml configuration for what I want to do (in the second link) is simple enough but I can't seem to replicate it in Java configuration.
I know it's an old topic, but I ran into similar questions. I managed to get it working. Not by overriding the accessDecisionManager() method within the WebSecurityConfigurerAdapter class, but by constructing a new instance while building my SpringSecurityAuthorizationPolicy:
#Bean
public Policy adminPolicy(AuthenticationManager authenticationManager) {
RoleVoter roleVoter = new RoleVoter();
SpringSecurityAuthorizationPolicy policy = new SpringSecurityAuthorizationPolicy();
policy.setAuthenticationManager(authenticationManager);
policy.setAccessDecisionManager(new UnanimousBased(List.of(roleVoter)));
policy.setSpringSecurityAccessPolicy(new SpringSecurityAccessPolicy(roleVoter.getRolePrefix() + "<ROLE_NAME>");
return policy;
}

spring jwt decoder openid token

External OAuth2 Provider doesn't have public JwkUri, so I tried too override default behavior using following code snippet:
#EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("**/oauth2/code/esia**", "**/code/esia**", "**esia**").permitAll()
.antMatchers("/user").fullyAuthenticated()
.anyRequest().authenticated()
.and()
.csrf().disable()
.cors().disable()
.oauth2Client()
.clientRegistrationRepository(this.clientRegistrationRepository)
.authorizationCodeGrant()
.authorizationRequestResolver(new CustomAuthorizationRequestResolver(
this.clientRegistrationRepository, esiaConfig, signatureUtil, timeUtil))
.accessTokenResponseClient(customAccessTokenResponseClient())
.and().and().oauth2Login().tokenEndpoint().accessTokenResponseClient(customAccessTokenResponseClient())
.and().and().oauth2ResourceServer().jwt();
}
#Bean
JwtDecoder jwtDecoder() {
return new CustomJwtDecoder();
}
}
class CustomJwtDecoder implements JwtDecoder {
#Override
public Jwt decode(String token) throws JwtException {
System.out.println(token);
return null;
}
}
However Spring Security somehow still uses default realization and I am getting the following error...
[missing_signature_verifier] Failed to find a Signature Verifier for Client Registration: 'esia'. Check to ensure you have configured the JwkSet URI.
Also, I tried to set custom AuthenticationProvider but spring ignores it.
I guess the catch is that spring`s OAuth2LoginConfigurer method init(B http) calls new OidcAuthorizationCodeAuthenticationProvider(accessTokenResponseClient, oidcUserService)
I was facing the same issue even with 5.2.x release. In my case, the real problem was not in the JwtDecoder. I have fixed the issue by setting the jwk-set-uri property (you can change the provider name by the provider which you are using e.g okta, google etc.) :
security.oauth2.client.provider.azure.jwk-set-uri: https://login.microsoftonline.com/{tenant}/discovery/keys
For 5.1.3.RELEASE it looks like you cannot get around this problem easily.
It stems from the OidcAuthorizationCodeAuthenticationProvider.getJwtDecoder
This happens in line 156 which is a call to a private method
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse);
...
}
The one option I see is if you make a copy of this code, and modify it yourself. Extending the class itself is not meaningful because all of the logic happens pretty much in the authenticate method. So you're still overriding it. then you add your provider using the http.authenticationProvider method
Another option is to override the SecurityConfigurerAdapter.postProcess method of the OAuth2LoginConfigurer class and do something clever there. Like populating the JWT decoder map through reflection.
Neither are admittedly preferred solutions. I believe that's why the refactoring happened for the 5.2 release.
Given the latest 5.2.x release then
You're almost there, but you must override the correct bean
#Bean
public JwtDecoderFactory<ClientRegistration> jwtDecoderFactory() {
final JwtDecoder decoder = jwtDecoder();
return context -> decoder;
}
and if you don't want to use lambdas
#Bean
public JwtDecoderFactory<ClientRegistration> jwtDecoderFactory() {
final JwtDecoder decoder = jwtDecoder();
return new JwtDecoderFactory<ClientRegistration>() {
#Override
public JwtDecoder createDecoder(ClientRegistration context) {
return decoder;
}
};
}
How did I figure this out, well I took a look at the OAuth2LoginConfigurer.java class which does
JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = getJwtDecoderFactoryBean();
and the private method that fetches the bean look like this
private JwtDecoderFactory<ClientRegistration> getJwtDecoderFactoryBean() {
ResolvableType type = ResolvableType.forClassWithGenerics(JwtDecoderFactory.class, ClientRegistration.class);
String[] names = this.getBuilder().getSharedObject(ApplicationContext.class).getBeanNamesForType(type);
if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
}
if (names.length == 1) {
return (JwtDecoderFactory<ClientRegistration>) this.getBuilder().getSharedObject(ApplicationContext.class).getBean(names[0]);
}
return null;
}
(Found this while looking for a solution to overriding the Jwt and Oidc Token validation. Filip's answer helped me get to the solution so I figured I'd add this to help anyone who follows the same search.)
For a time-travel testing scenario, our jvm clock was set months in the future. Login was failing due to the validations done on Jwt and Oidc token timestamp.
This addition worked for our app on Spring Security 5.2.1
#Bean
public JwtDecoderFactory<ClientRegistration> getJWTDecoder() {
OidcIdTokenDecoderFactory factory = new OidcIdTokenDecoderFactory();
factory.setJwtValidatorFactory(new Function<ClientRegistration, OAuth2TokenValidator<Jwt>>() {
#Override
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
return new CustomTimestampIgnoringOidcTokenValidator(clientRegistration);
}
});
}
This just replaces the Default validators with a custom one which only validates the other claims.

Spring Boot Security - Thymeleaf sec:authorize-url not working

The sec:authorize-url Tag does not work with Spring boot security by default:
git clone https://github.com/spring-projects/spring-boot
Project spring-boot-sample-web-method-security:
Add dependency
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity3</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
Adapt the controller from the sample:
#RequestMapping("/")
public String home(Map<String, Object> model) {
model.put("message", "Hello World");
model.put("title", "Hello Home");
model.put("date", new Date());
return "home";
}
#RequestMapping("/admin/foo")
public String home2(Map<String, Object> model) {
model.put("message", "Hello World");
model.put("title", "Hello Home");
model.put("date", new Date());
return "home";
}
Add url matching to application security:
http.authorizeRequests().antMatchers("/login").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
...
Add testcode in home.html
<div sec:authorize="hasRole('ROLE_ADMIN')">
has role admin
</div>
<div sec:authorize-url="/admin/foo">
can see /admin
</div>
When I start the app and login I will always see the "can see /admin" part no matter if I can actually access the url or not. The role evaluation itself works as expected, as does the url permission itself (I get a 403 when I try to access it with ROLE_USER).
If I add a dummy privilegeEvaluator to the web security configuration that simply returns false for every request, the div will disappear correctly.
Am I missing something here? Is this expected behaviour and what do I need to define to make authorize-url work that way it used to when configuring security with xml?
Update: Basic authentication
This issue is connected to Basic authentication and its AutoConfiguration in SpringBootWebSecurityConfiguration:
In SampleMethodSecurityApplication change the ApplicationSecurity order by replacing:
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
with
#Order(SecurityProperties.BASIC_AUTH_ORDER + 1)
and deactivate basic in spring boot application.properties
security.basic.enabled: false
Now the authorize-url tag will work as expected, but you have lost http basic AutoConfiguration of course.
Leaving security.basic.enabled: true and changing the order of the ApplicationSecurity to be higher than BASIC_AUTH_ORDER will leave you with Basic authentication instead of form login...
Update - PrivilegeEvaluator
I have found the following workaround. Simply register the security interceptor manually in your SecurityConfig:
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class ApplicationSecurity extends WebSecurityConfigurerAdapter {
#Override
public void configure(final WebSecurity web) throws Exception {
final HttpSecurity http = getHttp();
web.postBuildAction(new Runnable() {
#Override
public void run() {
web.securityInterceptor(http.getSharedObject(FilterSecurityInterceptor.class));
}
});
}
It allows you to use the recommended ACCESS_OVERRIDE_ORDER and http basic auto configuration. I have posted more details here
Any explanation why this works is appreciated.
using thymeleaf-extras-springsecurity4 should solve the problem
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>

Resources