Spring Security + Firebase - spring

I have rest backend wrote on Spring Boot and oauth2 (provided by Google) auto redirect on "/login". I want to make Firebase auth on the backend for mobile beside with oauth for web, like on the following algorithm:
User authorizes on mobile -> User sends request -> Backend gets request -> Backend checks if user openid exists in local database -> Backend returns response or exception page
The following code is my current WebSecurityConfiguration:
#Configuration
#EnableWebSecurity
#EnableOAuth2Sso
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().mvcMatchers("/","/static/**","/public/**","/assets/**","/api/sensors/**", "/emulator/**").permitAll()
.anyRequest().authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and()
.csrf().disable();
}
#Bean
public PrincipalExtractor principalExtractor(PersonRepository personRepository) {
return map -> {
String id = (String) map.get("sub");
Person person1 = personRepository.findById(id).orElseGet(() -> {
Person person = new Person();
person.setPersonId(id);
person.getDetails().setFirstName((String) map.get("given_name"));
person.getDetails().setLastName((String) map.get("family_name"));
person.getDetails().setEmail((String) map.get("email"));
person.getDetails().setPictureUrl((String) map.get("picture"));
person.getSettings().setLocale(new Locale((String) map.get("locale")));
person.setPersonRole(PersonRole.USER);
person.setStatus(PersonStatus.NORMAL);
person.newToken();
return person;
});
return personRepository.save(person1);
};
}
}

Add Firebase Configuration Bean of the form:
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.google.firebase.*;
#Configuration
public class FirebaseConfig {
#Bean
public DatabaseReference firebaseDatabse() {
DatabaseReference firebase = FirebaseDatabase.getInstance().getReference();
return firebase;
}
#Value("${firebase.database.url}")
private String databaseUrl;
#Value("${firebase.config.path}")
private String configPath;
#PostConstruct
public void init() {
/**
* https://firebase.google.com/docs/server/setup
*
* Create service account , download json
*/
InputStream inputStream = FirebaseConfig.class.getClassLoader().getResourceAsStream(configPath);
FirebaseOptions options = new FirebaseOptions.Builder().setServiceAccount(inputStream)
.setDatabaseUrl(databaseUrl).build();
FirebaseApp.initializeApp(options);
}
}
In your application.properties, add
firebase.config.path=Configuration.json
firebase.database.url=<firebase-database-path>
You can download your Configuration.json for your Firebase project by referring to this page

Related

Convert JWT Authentication Principal to something more usable in spring

I have a spring boot microservice that validates a JWT (issued by a different service) for authentication. It is working nicely, and I can access the JWT details in my controller like so:
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
// MyController.java
#RestController
#RequestMapping("/")
public class MyController {
#GetMapping()
public String someControllerMethod(#AuthenticationPrincipal Jwt jwt) {
int userId = Integer.parseInt(jwt.getClaim("userid"));
...
}
}
That works great. I can extract what I need from the JWT and go on to talk to my database with the correct userid etc.
However I find it a bit tedious to have to use the Jwt type to get these values in each controller. Is there a way I can inject a different type as the #AuthenticationPrincipal?
E.g. my own class which has already extracted what is needed from the JWT, and exposes something like .getUserId() that returns an int?
That would also let me centralise the logic of parsing the claims or throwing exceptions if they are not as expected etc.
UPDATE
After more google spelunking, it seems I have two options
Option1: #ControllerAdvice and #ModelAttribute
As explained in this answer. I can do something like:
import com.whatever.CustomPrincipal; // a basic "data" class with some properties, getters, setters and constructor
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
#ControllerAdvice
public class SecurityControllerAdvice {
#ModelAttribute
public CustomPrincipal customPrincipal(Authentication auth) throws Exception {
CustomPrincipal customPrincipal;
if (auth != null && auth.getPrincipal() instanceof Jwt) {
Jwt jwt = (Jwt) auth.getPrincipal();
String sessionId = jwt.getClaimAsString("sessionid");
int userId = Integer.parseInt(jwt.getClaimAsString("userid"));
customPrincipal = new CustomPrincipal(userId, sessionId);
} else {
// log an error and throw an exception?
}
return customPrincipal;
}
}
and then
import com.whatever.CustomPrincipal;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestController;
#RestController
#ControllerAdvice
public class HelloWorldController {
#GetMapping("/controlleradvice")
public String index(#ModelAttribute CustomPrincipal cp) {
log.info(cp.getUserId());
return "whatever";
}
}
This seems pretty succinct, and neat and tidy. 1 new class with #ControllerAdvice, and bob's your uncle!
Option2: Using jwtAuthenticationConverter()
This answer shows another way to do it, using a "converter", which seems to convert the default Principal from a JWT to a custom object (that extends AbstractAuthenticationToken) that contains the JWT (.getCredentials()) as well as a custom object like CustomPrincipal (or a User class or something).
#EnableWebSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors().disable()
.csrf().disable()
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer().jwt(customizer -> customizer.jwtAuthenticationConverter((new MyPrincipalJwtConvertor())));
return http.build();
}
}
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jwt.Jwt;
public class MyPrincipalJwtConvertor implements Converter<Jwt, MyAuthenticationToken> {
#Override
public MyAuthenticationToken convert(Jwt jwt) {
var principal = new MyPrincipal(Integer.parseInt(jwt.getClaimAsString("userid")), jwt.getClaimAsString("sessionid"));
return new MyAuthenticationToken(jwt, principal);
}
}
#RestController
public class HelloWorldController {
#GetMapping("/converter")
public String converter(#AuthenticationPrincipal MyPrincipal myPrincipal) {
log.info("/converter triggered");
log.info("" + myPrincipal.getUserId());
return "woo";
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
#Data
#AllArgsConstructor
public class MyPrincipal {
private int userId;
private String sessionId;
}
Option 1 is much simpler it seems.
But Option 2 is nice, as, I have Filter's that run to do additional validation (like validate the session id in the JWT). When that filter runs, when it calls SecurityContext.getContext().getAuthentication().getPrincipal(), it will get the MyPrincipal object, and not have to call Jwt.getClaimAsString() and cast it etc.
I guess I am asking, are there pros and cons to these two approaches I have not considered? Is one of them perhaps bastardising/abusing something in a way it is not meant to be?
Or is it much the same and I should select whichever I prefer?

How to set same-site cookie flag in Spring Boot?

Is it possible to set Same-Site Cookie flag in Spring Boot?
My problem in Chrome:
A cookie associated with a cross-site resource at http://google.com/
was set without the SameSite attribute. A future release of Chrome
will only deliver cookies with cross-site requests if they are set
with SameSite=None and Secure. You can review cookies in developer
tools under Application>Storage>Cookies and see more details at
https://www.chromestatus.com/feature/5088147346030592 and
https://www.chromestatus.com/feature/5633521622188032.
How to solve this problem?
Spring Boot 2.6.0
Spring Boot 2.6.0 now supports configuration of SameSite cookie attribute:
Configuration via properties
server.servlet.session.cookie.same-site=strict
Configuration via code
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration(proxyBeanMethods = false)
public class MySameSiteConfiguration {
#Bean
public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
return CookieSameSiteSupplier.ofStrict();
}
}
Spring Boot 2.5.0 and below
Spring Boot 2.5.0-SNAPSHOT doesn't support SameSite cookie attribute and there is no setting to enable it.
The Java Servlet 4.0 specification doesn't support the SameSite cookie attribute. You can see available attributes by opening javax.servlet.http.Cookie java class.
However, there are a couple of workarounds. You can override Set-Cookie attribute manually.
The first approach (using custom Spring HttpFirewall) and wrapper around request:
You need to wrap request and adjust cookies right after session is created. You can achieve it by defining the following classes:
one bean (You can define it inside SecurityConfig if you want to hold everything in one place. I just put #Component annotation on it for brevity)
package hello.approach1;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.stereotype.Component;
#Component
public class CustomHttpFirewall implements HttpFirewall {
#Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
return new RequestWrapper(request);
}
#Override
public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
return new ResponseWrapper(response);
}
}
first wrapper class
package hello.approach1;
import java.util.Collection;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.http.HttpHeaders;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* Wrapper around HttpServletRequest that overwrites Set-Cookie response header and adds SameSite=None portion.
*/
public class RequestWrapper extends FirewalledRequest {
/**
* Constructs a request object wrapping the given request.
*
* #param request The request to wrap
* #throws IllegalArgumentException if the request is null
*/
public RequestWrapper(HttpServletRequest request) {
super(request);
}
/**
* Must be empty by default in Spring Boot. See FirewalledRequest.
*/
#Override
public void reset() {
}
#Override
public HttpSession getSession(boolean create) {
HttpSession session = super.getSession(create);
if (create) {
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (ra != null) {
overwriteSetCookie(ra.getResponse());
}
}
return session;
}
#Override
public String changeSessionId() {
String newSessionId = super.changeSessionId();
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (ra != null) {
overwriteSetCookie(ra.getResponse());
}
return newSessionId;
}
private void overwriteSetCookie(HttpServletResponse response) {
if (response != null) {
Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
boolean firstHeader = true;
for (String header : headers) { // there can be multiple Set-Cookie attributes
if (firstHeader) {
response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None")); // set
firstHeader = false;
continue;
}
response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None")); // add
}
}
}
}
second wrapper class
package hello.approach1;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* Dummy implementation.
* To be aligned with RequestWrapper.
*/
public class ResponseWrapper extends HttpServletResponseWrapper {
/**
* Constructs a response adaptor wrapping the given response.
*
* #param response The response to be wrapped
* #throws IllegalArgumentException if the response is null
*/
public ResponseWrapper(HttpServletResponse response) {
super(response);
}
}
The second approach (using Spring's AuthenticationSuccessHandler):
This approach doesn't work for basic authentication.
In case basic authentication, response is flushed/committed right after controller returns response object, before SameSiteFilter#addSameSiteCookieAttribute is called.
package hello.approach2;
import java.io.IOException;
import java.util.Collection;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
addSameSiteCookieAttribute(response); // add SameSite=strict to Set-Cookie attribute
response.sendRedirect("/hello"); // redirect to hello.html after success auth
}
private void addSameSiteCookieAttribute(HttpServletResponse response) {
Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
boolean firstHeader = true;
for (String header : headers) { // there can be multiple Set-Cookie attributes
if (firstHeader) {
response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
firstHeader = false;
continue;
}
response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
}
}
}
The third approach (using javax.servlet.Filter):
This approach doesn't work for basic authentication.
In case basic authentication, response is flushed/committed right after controller returns response object, before SameSiteFilter#addSameSiteCookieAttribute is called.
package hello.approach3;
import java.io.IOException;
import java.util.Collection;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
public class SameSiteFilter implements javax.servlet.Filter {
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
addSameSiteCookieAttribute((HttpServletResponse) response); // add SameSite=strict cookie attribute
}
private void addSameSiteCookieAttribute(HttpServletResponse response) {
Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
boolean firstHeader = true;
for (String header : headers) { // there can be multiple Set-Cookie attributes
if (firstHeader) {
response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
firstHeader = false;
continue;
}
response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
}
}
#Override
public void destroy() {
}
}
You can look at this demo project on the GitHub for more details on the configuration for org.springframework.security.web.authentication.AuthenticationSuccessHandler or javax.servlet.Filter.
The SecurityConfig contains all the necessary configuration.
Using addHeader is not guaranteed to work because basically the
Servlet container manages the creation of the Session and Cookie. For
example, the second and third approaches won't work in case you return JSON in
response body because application server will overwrite Set-Cookie
header during flushing of response. However, second and third approaches will
work in cases, when you redirect a user to another page after successful
authentication.
Pay attention that Postman doesn't render/support SameSite cookie attribute under Cookies section (at least at the time of writing). You can look at Set-Cookie response header or use curl to see if SameSite cookie attribute was added.
This is an open issue with Spring Security (https://github.com/spring-projects/spring-security/issues/7537)
As I inspected in Spring-Boot (2.1.7.RELEASE), By Default it uses DefaultCookieSerializer which carry a property sameSite defaulting to Lax.
You can modify this upon application boot, through the following code.
Note: This is a hack until a real fix (configuration) is exposed upon next spring release.
#Component
#AllArgsConstructor
public class SameSiteInjector {
private final ApplicationContext applicationContext;
#EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
DefaultCookieSerializer cookieSerializer = applicationContext.getBean(DefaultCookieSerializer.class);
log.info("Received DefaultCookieSerializer, Overriding SameSite Strict");
cookieSerializer.setSameSite("strict");
}
}
From spring boot version 2.6.+ you may specify your samesite cookie either programatically or via configuration file.
Spring boot 2.6.0 documentation
If you would like to set samesite to lax via configuration file then:
server.servlet.session.cookie.same-site=lax
Or programatically
#Configuration
public class MySameSiteConfiguration {
#Bean
public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
return CookieSameSiteSupplier.ofLax();
}
}
Ever since the last update, chrome started showing that message to me too. Not really an answer regarding spring, but you can add the cookie flag to the header of the session. In my case, since I'm using spring security, I intend to add it when the user logs in, since I'm already manipulating the session in order to add authentication data.
For more info, check this answer to a similar topic: https://stackoverflow.com/a/43250133
To add the session header right after the user logs in, you can base your code on this topic (by creating a spring component that implements AuthenticationSuccessHandler): Spring Security. Redirect to protected page after authentication
For me none of the above worked. My problem was, that after a login, the SameSite flag created with other methods mentioned in this post was simply ignored by redirect mechanizm.
In our spring boot 2.4.4 application I managed to get it done with custom SameSiteHeaderWriter:
import org.springframework.security.web.header.HeaderWriter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import static javax.ws.rs.core.HttpHeaders.SET_COOKIE;
/**
* This header writer just adds "SameSite=None;" to the Set-Cookie response header
*/
public class SameSiteHeaderWriter implements HeaderWriter {
private static final String SAME_SITE_NONE = "SameSite=None";
private static final String SECURE = "Secure";
#Override
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
if (response.containsHeader(SET_COOKIE)) {
var setCookie = response.getHeader(SET_COOKIE);
var toAdd = new ArrayList<String>();
toAdd.add(setCookie);
if (! setCookie.contains(SAME_SITE_NONE)) {
toAdd.add(SAME_SITE_NONE);
}
if (! setCookie.contains(SECURE)) {
toAdd.add(SECURE);
}
response.setHeader(SET_COOKIE, String.join("; ", toAdd));
}
}
}
then in my WebSecurityConfigurerAdapter#configure I just added this header writer to the list using:
if (corsEnabled) {
httpSecurity = httpSecurity
.cors()
.and()
.headers(configurer -> {
configurer.frameOptions().disable();
configurer.addHeaderWriter(new SameSiteHeaderWriter());
});
}
This feature have to be explicitly enabled in our app by user knowing the risks.
Just thought this might help someone in the future.
Starting from Spring Boot 2.6.0 this is now possible and easy:
import org.springframework.http.ResponseCookie;
ResponseCookie springCookie = ResponseCookie.from("refresh-token", "000")
.sameSite("Strict")
.build();
and return it in a ResponseEntity, could be like this :
ResponseEntity
.ok()
.header(HttpHeaders.SET_COOKIE, springCookie.toString())
.build();
If you use spring-redis-session, you can customize the Cookie (🍪) by creating a bean like the following:
#Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
serializer.setSameSite(null);
return serializer;
}
You can look here more detail information.
Follow the documentation to solve this issue:
https://github.com/GoogleChromeLabs/samesite-examples
It has examples with different languages

Spring Security: mapping OAuth2 claims with roles to secure Resource Server endpoints

I'm setting up a Resource Server with Spring Boot and to secure the endpoints I'm using OAuth2 provided by Spring Security. So I'm using the Spring Boot 2.1.8.RELEASE which for instance uses Spring Security 5.1.6.RELEASE.
As Authorization Server I'm using Keycloak. All processes between authentication, issuing access tokens and validation of the tokens in the Resource Server are working correctly. Here is an example of an issued and decoded token (with some parts are cut):
{
"jti": "5df54cac-8b06-4d36-b642-186bbd647fbf",
"exp": 1570048999,
"aud": [
"myservice",
"account"
],
"azp": "myservice",
"realm_access": {
"roles": [
"offline_access",
"uma_authorization"
]
},
"resource_access": {
"myservice": {
"roles": [
"ROLE_user",
"ROLE_admin"
]
},
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "openid email offline_access microprofile-jwt profile address phone",
}
How can I configure Spring Security to use the information in the access token to provide conditional authorization for different endpoints?
Ultimately I want to write a controller like this:
#RestController
public class Controller {
#Secured("ROLE_user")
#GetMapping("userinfo")
public String userinfo() {
return "not too sensitive action";
}
#Secured("ROLE_admin")
#GetMapping("administration")
public String administration() {
return "TOOOO sensitive action";
}
}
After messing around a bit more, I was able to find a solution implementing a custom jwtAuthenticationConverter, which is able to append resource-specific roles to the authorities collection.
http.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new JwtAuthenticationConverter()
{
#Override
protected Collection<GrantedAuthority> extractAuthorities(final Jwt jwt)
{
Collection<GrantedAuthority> authorities = super.extractAuthorities(jwt);
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
Map<String, Object> resource = null;
Collection<String> resourceRoles = null;
if (resourceAccess != null &&
(resource = (Map<String, Object>) resourceAccess.get("my-resource-id")) !=
null && (resourceRoles = (Collection<String>) resource.get("roles")) != null)
authorities.addAll(resourceRoles.stream()
.map(x -> new SimpleGrantedAuthority("ROLE_" + x))
.collect(Collectors.toSet()));
return authorities;
}
});
Where my-resource-id is both the resource identifier as it appears in the resource_access claim and the value associated to the API in the ResourceServerSecurityConfigurer.
Notice that extractAuthorities is actually deprecated, so a more future-proof solution should be implementing a full-fledged converter
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken>
{
private static Collection<? extends GrantedAuthority> extractResourceRoles(final Jwt jwt, final String resourceId)
{
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
Map<String, Object> resource;
Collection<String> resourceRoles;
if (resourceAccess != null && (resource = (Map<String, Object>) resourceAccess.get(resourceId)) != null &&
(resourceRoles = (Collection<String>) resource.get("roles")) != null)
return resourceRoles.stream()
.map(x -> new SimpleGrantedAuthority("ROLE_" + x))
.collect(Collectors.toSet());
return Collections.emptySet();
}
private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private final String resourceId;
public CustomJwtAuthenticationConverter(String resourceId)
{
this.resourceId = resourceId;
}
#Override
public AbstractAuthenticationToken convert(final Jwt source)
{
Collection<GrantedAuthority> authorities = Stream.concat(defaultGrantedAuthoritiesConverter.convert(source)
.stream(),
extractResourceRoles(source, resourceId).stream())
.collect(Collectors.toSet());
return new JwtAuthenticationToken(source, authorities);
}
}
I have tested both solutions using Spring Boot 2.1.9.RELEASE, Spring Security 5.2.0.RELEASE and an official Keycloak 7.0.0 Docker image.
Generally speaking, I suppose that whatever the actual Authorization Server (i.e. IdentityServer4, Keycloak...) this seems to be the proper place to convert claims into Spring Security grants.
Here is another solution
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
}
The difficulty you are experiencing is partly due to your roles being positioned in the JWT under resource_server->client_id. This then requires a custom token converter to extract them.
You can configure keycloak to use a client mapper that will present the roles under a top-level claim name such as "roles". This makes the Spring Security configuration simpler as you only need JwtGrantedAuthoritiesConverter with the authoritiesClaimName set as shown in the approach taken by #hillel_guy.
The keycloak client mapper would be configured like this:
As already mentioned by #hillel_guy's answer, using an AbstractHttpConfigurer should be the way to go. This worked seamlessly for me with spring-boot 2.3.4 and spring-security 5.3.4.
See the spring-security API documentation for reference: OAuth2ResourceServerConfigurer
UPDATE
Full example, as asked in the comments:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String JWT_ROLE_NAME = "roles";
private static final String ROLE_PREFIX = "ROLES_";
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and().csrf().disable()
.cors()
.and().oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
// create a custom JWT converter to map the roles from the token as granted authorities
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(JWT_ROLE_NAME); // default is: scope, scp
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(ROLE_PREFIX ); // default is: SCOPE_
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
In my case, I wanted to map roles from the JWT instead of scope.
Hope this helps.
2022 update
I maintain a set of tutorials and samples to configure resource-servers security for:
both servlet and reactive applications
decoding JWTs and introspecting access-tokens
default or custom Authentication implementations
any OIDC authorization-server(s), including Keycloak of course (most samples support multiple realms / identity-providers)
The repo also contains a set of libs published on maven-central to:
mock OAuth2 identities during unit and integration tests (with authorities and any OpenID claim, including private ones)
configure resource-servers from properties file (including source claims for roles, roles prefix and case processing, CORS configuration, session-management, public routes and more)
Sample for a servlet with JWT decoder
#EnableMethodSecurity(prePostEnabled = true)
#Configuration
public class SecurityConfig {}
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
com.c4-soft.springaddons.security.cors[0].path=/sample/**
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<version>6.0.3</version>
</dependency>
No, nothing more requried.
Unit-tests with mocked authentication
Secured #Component without http request (#Service, #Repository, etc.)
#Import({ SecurityConfig.class, SecretRepo.class })
#AutoConfigureAddonsSecurity
class SecretRepoTest {
// auto-wire tested component
#Autowired
SecretRepo secretRepo;
#Test
void whenNotAuthenticatedThenThrows() {
// call tested components methods directly (do not use MockMvc nor WebTestClient)
assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
}
#Test
#WithMockJwtAuth(claims = #OpenIdClaims(preferredUsername = "Tonton Pirate"))
void whenAuthenticatedAsSomeoneElseThenThrows() {
assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
}
#Test
#WithMockJwtAuth(claims = #OpenIdClaims(preferredUsername = "ch4mpy"))
void whenAuthenticatedWithSameUsernameThenReturns() {
assertEquals("Don't ever tell it", secretRepo.findSecretByUsername("ch4mpy"));
}
}
Secured #Controller (sample for #WebMvcTest but works for #WebfluxTest too)
#WebMvcTest(GreetingController.class) // Use WebFluxTest or WebMvcTest
#AutoConfigureAddonsWebSecurity // If your web-security depends on it, setup spring-addons security
#Import({ SecurityConfig.class }) // Import your web-security configuration
class GreetingControllerAnnotatedTest {
// Mock controller injected dependencies
#MockBean
private MessageService messageService;
#Autowired
MockMvcSupport api;
#BeforeEach
public void setUp() {
when(messageService.greet(any())).thenAnswer(invocation -> {
final JwtAuthenticationToken auth = invocation.getArgument(0, JwtAuthenticationToken.class);
return String.format("Hello %s! You are granted with %s.", auth.getName(), auth.getAuthorities());
});
when(messageService.getSecret()).thenReturn("Secret message");
}
#Test
void greetWitoutAuthentication() throws Exception {
api.get("/greet").andExpect(status().isUnauthorized());
}
#Test
#WithMockAuthentication(authType = JwtAuthenticationToken.class, principalType = Jwt.class, authorities = "ROLE_AUTHORIZED_PERSONNEL")
void greetWithDefaultMockAuthentication() throws Exception {
api.get("/greet").andExpect(content().string("Hello user! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
}
}
Advanced use-cases
The most advanced tutorial demoes how to define a custom Authentication implementation to parse (and expose to java code) any private claim into things that are security related but not roles (in the sample it's grant delegation between users).
It also shows how to extend spring-security SpEL to build a DSL like:
#GetMapping("greet/on-behalf-of/{username}")
#PreAuthorize("is(#username) or isNice() or onBehalfOf(#username).can('greet')")
public String getGreetingFor(#PathVariable("username") String username) {
return ...;
}
If you are using Azure AD Oath there is a much easier way now:
http
.cors()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new AADJwtBearerTokenAuthenticationConverter("roles", "ROLE_"));
The ADDJwtBearerTokenAuthenticationConverter allows you to add your claim name as the first argument and what you want your role prefixed with as the second argument.
My import so you can find the library:
import com.azure.spring.aad.webapi.AADJwtBearerTokenAuthenticationConverter;

Spring Security - Authenticate Users With Active Directory against LDAP in Spring Boot

I am gettting LDAP authentication error when we configured for LDAP authentication. My property files as below configuration:
ldap.urls=ldap://***.***.local:8389
ldap.base.dn=dc=test,dc=com
ldap.user.dn.pattern=(&(objectClass=user)(userPrincipalName={0})(memberof=CN=Group Name,OU=***,OU=****,DC=test,DC=com))
While accessing wsdl by passing valid username & password getting below error:
While accessing wsdl it is asking username & Password. If we provided then it saying that “ActiveDirectoryLdapAuthenticationProvider - Active Directory authentication failed: Supplied password was invalid
and while starting the application i am able to see below log on console:
`org.springframework.ldap.core.support.AbstractContextSource - Property 'userDn' not set - anonymous context will be used for read-write operation`
for SOAP Calls as i have provided some more in SOAPWebServiceConfig.java even not working.
//XwsSecurityInterceptor
#Bean
public XwsSecurityInterceptor securityInterceptor(){
XwsSecurityInterceptor securityInterceptor = new XwsSecurityInterceptor();
//Callback Handler -> SimplePasswordValidationCallbackHandler
securityInterceptor.setCallbackHandler(callbackHandler());
//Security Policy -> securityPolicy.xml
securityInterceptor.setPolicyConfiguration(new ClassPathResource("securityPolicy.xml"));
return securityInterceptor;
}
#Bean
public SimplePasswordValidationCallbackHandler callbackHandler() {
SimplePasswordValidationCallbackHandler handler = new SimplePasswordValidationCallbackHandler();
handler.setUsersMap(Collections.singletonMap("user", "password"));
return handler;
}
//Interceptors.add -> XwsSecurityInterceptor
#Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
interceptors.add(securityInterceptor());
}
I am not getting what is the issue here. Can anyone please suggest on this.
Active-Directory has LDAP compatible protocol but uses some special conventions compared to other ldap directories.
To get those configured right (e.g. append the domain to usernames) use ActiveDirectoryLdapAuthenticationProvider instead of the LdapAuthenticationProvider which will be uses when using the auto-configuration via Properties. Remove or rename "ldap.urls" and the other properties from your application.yml then.
package com.test;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
ActiveDirectoryLdapAuthenticationProvider adProvider = new ActiveDirectoryLdapAuthenticationProvider("domain.org",
"ldap://activedirectory-url:389");
adProvider.setConvertSubErrorCodesToExceptions(true);
adProvider.setUseAuthenticationRequestCredentials(true);
auth.authenticationProvider(adProvider);
}
}

SpringBoot Rest API custom authentication

I build a Rest Api using SpringBoot and the authentication I implemented using Firebase.
My problem right now is that I want to have control of the client applications that will access my application. The problem of using SpringSecurity is that as far as I know I have to do the authentication for it and I just want to "allow the client application."
Does anyone have any idea how to do?
Provide a unique key to your client. Which your microservice recognises and authenticates any request based on that key. This can be also given as a request parameter.
let say you add your key into a parameter called my-key, now before working on your logic inside you spring-boot app validate your key. like this -
your Rest Controller would look like this-
#RestController
class MyRest{
private static final String KEY = "someValue";
#RequestMapping("/some-mapping")
public #ResponseBody myMethod(#RequestParam(value="my-key", required=true) String key){
if(!validateRequest(key)){
//return error as response
}
System.out.println("Key Validation Successful!");
//here goes your logic
}
private boolean validateRequest(String key){
return key.equals(KEY);
}
}
in order to access this rest use - http://your-host:port/some-mapping?my-key=someValue
If you want to allow some of the clients to bypass the authentication, have a list of whitelisted IP addresses and check the IP of each incoming request. if the IP is in the list of whitelisted APIs, no need to authenticate.
Use HttpServletRequest.getRemoteAddr() to get the IP address.
Solution 1
Custom interceptor MyHandlerInterceptor.java:
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class MyHandlerInterceptor implements HandlerInterceptor {
private static final String YOUR_KEY = "KEY_VALUE";
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
String key = request.getHeader("X-Key");
boolean isValid = YOUR_KEY.equals(key);
if (!isValid) {
//invalid key
response.setStatus(401);
PrintWriter writer = response.getWriter();
writer.write("invalid key");
}
return isValid;
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
}
}
Configure interceptor WebConfig.java:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyHandlerInterceptor());
}
}

Resources