Spring Boot in Azure - Client Certificate in Request Header - spring

We currently implemented mutual authentication in our Spring Boot application and need to deploy it in Azure.
Azure's loadbalancer redirects the client certificate (Base64 encoded) in the request header field "X-ARR-ClientCert" and Spring is not able to find it there.
=> Authentication fails
The microsoft documentation shows how to handle this in a .NET application: https://learn.microsoft.com/en-gb/azure/app-service-web/app-service-web-configure-tls-mutual-auth
I tried to extract the certificate from the header in an OncePerRequestFilter and set it to the request like this:
public class AzureCertificateFilter extends OncePerRequestFilter {
private static final Logger LOG = LoggerFactory.getLogger(AzureCertifacteFilter.class);
private static final String AZURE_CLIENT_CERTIFICATE_HEADER = "X-ARR-ClientCert";
private static final String JAVAX_SERVLET_REQUEST_X509_CERTIFICATE = "javax.servlet.request.X509Certificate";
private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----\n";
private static final String END_CERT = "\n-----END CERTIFICATE-----";
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
X509Certificate x509Certificate = extractClientCertificate(httpServletRequest);
// azure redirects the certificate in a header field
if (x509Certificate == null && StringUtils.isNotBlank(httpServletRequest.getHeader(AZURE_CLIENT_CERTIFICATE_HEADER))) {
String x509CertHeader = BEGIN_CERT + httpServletRequest.getHeader(AZURE_CLIENT_CERTIFICATE_HEADER) + END_CERT;
try (ByteArrayInputStream certificateStream = new ByteArrayInputStream(x509CertHeader.getBytes())) {
X509Certificate certificate = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certificateStream);
httpServletRequest.setAttribute(JAVAX_SERVLET_REQUEST_X509_CERTIFICATE, certificate);
} catch (CertificateException e) {
LOG.error("X.509 certificate could not be created out of the header field {}. Exception: {}", AZURE_CLIENT_CERTIFICATE_HEADER, e.getMessage());
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private X509Certificate extractClientCertificate(HttpServletRequest request) {
X509Certificate[] certs = (X509Certificate[]) request.getAttribute(JAVAX_SERVLET_REQUEST_X509_CERTIFICATE);
if (certs != null && certs.length > 0) {
LOG.debug("X.509 client authentication certificate:" + certs[0]);
return certs[0];
}
LOG.debug("No client certificate found in request.");
return null;
}
}
But this fails later in the Spring filter chain with the following exception:
sun.security.x509.X509CertImpl cannot be cast to [Ljava.security.cert.X509Certificate; /oaa/v1/spaces
The configuration looks like this:
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("**/docs/restapi/**").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.disable()
.addFilterBefore(new AzureCertificateFilter(), X509AuthenticationFilter.class)
.x509()
.subjectPrincipalRegex("CN=(.*?)(?:,|$)")
.userDetailsService(userDetailsService());
}

I should have read the exception more carefully:
sun.security.x509.X509CertImpl cannot be cast to [Ljava.security.cert.X509Certificate; /oaa/v1/spaces
I had to set an array of certificates like this:
httpServletRequest.setAttribute(JAVAX_SERVLET_REQUEST_X509_CERTIFICATE, new X509Certificate[]{x509Certificate});

Related

Spring Boot Authentication succeeds with embedded Tomcat, but returns 403 with Open/WAS Liberty

I use Spring Security to authenticate/authorize against Active Directory. Below code works just fine if I run it in Spring embedded Tomcat.
But when I switch to Open/WAS Liberty server, I get 403 on authenticate (/auth endpoint):
My WebSecurityConfiguration class looks like:
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Value("${active.dir.domain}")
private String domain;
#Value("${active.dir.url}")
private String url;
#Value("${active.dir.userDnPattern}")
private String userDnPattern;
private final Environment environment;
public WebSecurityConfiguration(Environment environment) {
this.environment = environment;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(activeDirectoryAuthenticationProvider()).eraseCredentials(false);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf().disable()
.authorizeRequests()
.antMatchers("/auth").permitAll()
.anyRequest()
.authenticated()
.and()
.addFilter(getAuthenticationFilter())
.addFilter(new AuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
public AuthenticationProvider activeDirectoryAuthenticationProvider() {
String adSearchFilter = "(&(sAMAccountName={1})(objectClass=user))";
ActiveDirectoryLdapAuthenticationProvider ad = new ActiveDirectoryLdapAuthenticationProvider(domain, url, userDnPattern);
ad.setConvertSubErrorCodesToExceptions(true);
ad.setUseAuthenticationRequestCredentials(true);
ad.setSearchFilter(adSearchFilter);
return ad;
}
//CORS configuration source
#Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://some.url"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("*"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
//Customize the Spring default /login url to overwrite it with /auth.
private AuthenticationFilter getAuthenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
filter.setFilterProcessesUrl("/auth");
return filter;
}
}
Here is my AuthorizationFilter class:
public class AuthorizationFilter extends BasicAuthenticationFilter {
public AuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
//Extracts username from Jwt token
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null) {
token = token.replace("Bearer ", "");
String username = Jwts.parser()
.setSigningKey("somesecret")
.parseClaimsJws(token)
.getBody()
.getSubject();
if (username != null) {
return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
}
}
return null;
}
}
Here is my AuthenticationFilter class:
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
UserLoginRequestModel userLoginRequestModel = extractCredentials(request);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
userLoginRequestModel.getUsername()
, userLoginRequestModel.getPassword()
, new ArrayList<>());
return authenticationManager.authenticate(token);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication auth) throws IOException, ServletException {
String userId = ((UserDetails)auth.getPrincipal()).getUsername();
Instant now = Instant.now();
String jwtToken = Jwts.builder()
.setSubject(userId)
.setIssuer("me")
.setAudience("myapp")
.setId(UUID.randomUUID().toString())
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plus(30000)))
.signWith(SignatureAlgorithm.HS512, SecurityConstants.getTokenSecret())
.compact();
response.addHeader("Authorization", "Bearer " + jwtToken);
response.addHeader("Access-Control-Expose-Headers", accessControlHeaders.toString());
}
private UserLoginRequestModel extractCredentials(HttpServletRequest request) {
UserLoginRequestModel userLoginRequestModel = new UserLoginRequestModel();
String authorizationHeader = request.getHeader("Authorization");
try {
if (authorizationHeader != null && authorizationHeader.toLowerCase().startsWith("basic")) {
String base64Credentials = authorizationHeader.substring("Basic".length()).trim();
byte[] decodedCredentials = Base64.getDecoder().decode(base64Credentials);
String headerCredentials = new String(decodedCredentials, StandardCharsets.UTF_8);
final String[] credentialsValues = headerCredentials.split(":", 2);
userLoginRequestModel.setUsername(credentialsValues[0]);
userLoginRequestModel.setPassword(credentialsValues[1]);
} else {
userLoginRequestModel = new ObjectMapper().readValue(request.getInputStream(), UserLoginRequestModel.class);
}
return userLoginRequestModel;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
In Postman, I call:
POST: http://localhost/myapi/v1/auth
And I pass it BasicAuth with username and password.
I get 403 Forbidden back if I run this on Open/WAS Liberty. Same code, with no change whatsoever, runs just fine in embedded Tomcat that comes with Spring and I get 200 OK.
The reason I was experiencing this was that in my Liberty server.xml, I was missing defined context-path. As it looks like, Liberty does not consider context-path set up in your application.properties file.
Below is the context-path I have in my application.properties file.
Unfortunatelly, Liberty does not read (or considers) it and just uses the app name as the context-path instead of using the setting in application.properties or application.yml file:
server.servlet.context-path=/myapi/v1
As a result, the above context-path will work just fine if deployment in Spring Boot embedded Tomcat container but not in Liberty container.
When you deploy it to OpenLiberty/WASLiberty, you might find that your endpoints will stop working and you get 403 and/or 404 errors.
In my example above, I have getAuthenticationFilter() method, in my WebSecurityConfiguration class. Below, I added little bit more comments to it to explain:
//Customize the /login url to overwrite the Spring default provided /login url.
private AuthenticationFilter getAuthenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
// This works fine on embedded tomcat, but not in Liberty where it returns 403.
// To fix, in server.xml <appllication> block, add
// <application context-root="/myapi/v1" ... and then both
// auth and other endpoints will work fine in Liberty.
filter.setFilterProcessesUrl("/auth");
// This is temporary "fix" that creates rather more issues, as it
// works fine with Tomcat but fails in Liberty and all other
// endpoints still return 404
//filter.setFilterProcessesUrl("/v1/auth");
return filter;
}
Based on the above context-path, on Tomcat, it becomes /myapi/v1/auth while on Liberty, it ends up being just /myapi/auth which is wrong. I think what Liberty does, it will just take the name of the api and add to it the endpoint, therefore ignoring the versioning.
As a result of this, AntPathRequestMatcher class matches() method will result in a non-matching /auth end point and you will get 403 error. And the other endpoints will result in 404 error.
SOLUTION
In your application.properties, leave:
server.servlet.context-path=/myapi/v1
, this will be picked up by embedded Tomcat and your app will continue to work as expected.
In your server.xml configuration for Open/WAS Liberty, add
matching context-root to the section like:
<application context-root="/myapi/v1" id="myapi" location="location\of\your\myapi-0.0.1.war" name="myapi" type="war">
, this will be picked up by Open/WASLiberty and your app will continue to work as expected on Liberty container as well.

Using JWT with #PathVariable but only allow access url for spesific user

I am creating simple Rest social media application with Spring Boot. I use JWT for authentication in application.
In my mobile application when users register, i am getting some information from users and create account and profile of the user.
By the way, you can see (simplified) database object of account and profile. I use Mongo DB for database.
account:
{
“_id”: “b6164102-926e-47d8-b9ff-409c44dc47c0“,
“email”: “xxx#yy.com”
….
}
profile:
{
“_id”: “35b06171-c16a-4559-90f3-df81ace6d64a“,
“accountId”: “b6164102-926e-47d8-b9ff-409c44dc47c0”,
profileImages: [
{
“imageId”: “1431b0bc-feb7-436d-9d3a-7b9094547bf6”,
“imageLink”: “https://this_is_some_link_to_image.com
}
….
]
….
}
When user login to app, i add accountId to JWT and then in my mobile app i call below endpoint to get profile information of user. I take accountId from jwt and find profile of that account id.
#GetMapping("/profiles")
public ResponseEntity<BaseResponse> getUserProfile(#AuthenticationPrincipal AccountId accountId) {
var query = new Query(accountId);

 var presenter = new GetUserProfilePresenter();


 useCase.execute(query, presenter);


 return presenter.getViewModel();
}
In the app, users can upload photo to their profile using below endpoint;
#PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#PathVariable("profileId") UUID profileId, #RequestParam("image") MultipartFile image) throws IOException {

......
}
Everything works fine but the problem is someone can use their token to call this url with another person’s profileId. Because profileId is not a hidden id. In my mobile app users can shuffle and see other users profile using below url.
This url is accessible by any authenticated users.
#GetMapping(path = "/profiles/{profileId}")
public ResponseEntity<BaseResponse> getProfile(#PathVariable("profileId") UUID profileId) {
......
}
Now, my question is how can i make "/profiles/{profileId}/images" this url is only accesible for user of this profile without changing path format.
For exampe;
User A - Profile Id = XXX
User B - Profile Id = YYY
I want that if User A calls this url with own JWT Token, uploads image only to own profile not another one profile.
I have come up with some solutions but these solutions cause me to change the url path;
Solution 1:
I can use accountId in the jwt. Find profile of user with this accountId so that, every call to this url guaranteed upload image only to profile of token user.
But this solution change url path like below because i dont need to get any profileId from path.
#PostMapping(path = "/profiles/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

public ResponseEntity<BaseResponse> uploadProfileImage(

 #AuthenticationPrincipal AccountId accountId, #RequestParam("image") MultipartFile image) throws IOException {


 ......
}
Solution 2:
This is very similar to first solution only different is when i create jwt for user. I will put profileId of user to inside of JWT. So when the user calls the url i will get profileId from jwt and put inside of Authentication object. And in the controller i will get this profileId for using to find profile of user then upload image to this spesific profile.
But also, this solution change url path format because i dont need to get profileId from url path.
So if i back to my main question. What is the best practices and solutions for these kinda problems and situations?
~~~EDIT~~~
For those whose wonder, i didn't change my path. Actually i implemented solution 1 with a twist.
Now i use accountId from JWT and profileId at the same time so when i want to find a profile of exactly that user i search the database using accountId and profileId together.
With this change, i didn't need to change other paths.
For example; (GET) /profiles/{profileId} this path still meaningful for all authenticated users.
But (POST) /profiles/{profileId}/images this path only meaningful for that spesific (owner of token) user.
By the way, i starts paths with "api/admin/**" prefix for my admin role operations.
Final code (Controller);
#PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#AuthenticationPrincipal AccountId accountId,
#PathVariable("profileId") UUID profileId,
#RequestParam("image") MultipartFile image) throws IOException {
....
}
Final code (Repository);
#Repository
public interface ProfileJpaRepository extends MongoRepository<ProfileDto, String> {
Optional<ProfileDto> findByAccountId(String accountId);
Optional<ProfileDto> findByIdAndAccountId(String profileId, String accountId);
}
The best practice to handle this kind of scenarios is to have two endpoints, each needing different kind of permissions:
"/profiles/{profileId}/images" will be available for admins, so that if an admin wants to change another user's profile image, they can do so by calling this endpoint.
"/profiles/images" will be responsible for changing the most generic users with the lowest privileges.
So, in both scenarios you need to extract the AccountId from the JWT and you should not get the AccountId from the user directly, unless for administration purposes where you check the privileges to authorize the user.
Now, the best way to implement such a system, is to use Spring Security and to create a custom AuthenticationToken, then to customize AbstractUserDetailsAuthenticationProvider, * AbstractAuthenticationProcessingFilter* and UsernamePasswordAuthenticationToken.
After doing so, you can then configure Spring to use the custom provider for authentication.
UsernamePasswordAuthenticationToken
public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {
private Payload payload; // Payload can be any model class that encapsulates the payload of the JWT.
private boolean creationAllowed;
public JwtAuthenticationToken(String jwtToken) throws Exception {
super(null, jwtToken);
// Verify JWT and get the payload
this.payload = // set the payload
}
public JwtAuthenticationToken(String principal, JwtAuthenticationToken authToken, Collection<? extends GrantedAuthority> authorities) {
super(principal, authToken.getCredentials(), authorities);
this.payload = authToken.payload;
authToken.eraseCredentials(); // not sure if this is needed
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public Payload getPayload() {
return this.firebaseToken;
}
public boolean isCreationAllowed() {
return creationAllowed;
}
public void setCreationAllowed(boolean creationAllowed) {
this.creationAllowed = creationAllowed;
}
}
AbstractUserDetailsAuthenticationProvider
#Component
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
#Autowired
AppUserService appUserService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(JwtAuthenticationToken.class, authentication, () ->
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only JwtAuthenticationToken is supported")
);
JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) authentication;
String principal;
try {
principal = jwtAuthToken.getPayload().getEmail(); // Here I'm using email as the user identifier, this can be anything, for example AccountId
} catch (RuntimeException re) {
throw new AuthenticationException("Could not extract user's email address.");
}
AppUser user = (AppUser) this.retrieveUser(principal, jwtAuthToken);
return this.createSuccessAuthentication(principal, jwtAuthToken, user);
}
#Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
JwtAuthenticationToken result = new JwtAuthenticationToken((String) principal, (JwtAuthenticationToken) authentication, user.getAuthorities());
result.setDetails(user);
return result;
}
#Override
public UserDetails retrieveUser(String s, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
UserDetails userDetails = appUserService.loadUserByUsername(s);
JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) usernamePasswordAuthenticationToken;
if (userDetails != null)
return userDetails; // You need to create an UserDetails which will be set by the framework to the Security Context as the authenticated user, this will be useful later when you want to check the privileges.
else
throw new AuthenticationException("Creating the user details is not allowed.");
}
#Override
protected void additionalAuthenticationChecks(final UserDetails d, final UsernamePasswordAuthenticationToken auth) {
// Nothing to do
}
#Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}
}
AbstractAuthenticationProcessingFilter
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationFilter() {
super("/**"); // The path that this filter needs to process, use "/**" to make sure all paths must be proessed.
}
#Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return true; // Here I am returning true to require authentication for all requests.
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String authorization = request.getHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer "))
throw new AuthenticationException("No JWT token found in request headers");
String authToken = authorization.substring(7);
JwtAuthenticationToken token = new JwtAuthenticationToken(authToken);
return getAuthenticationManager().authenticate(token);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
// Authentication process succeed, filtering the request in.
// As this authentication is in HTTP header, after success we need to continue the request normally
// and return the response as if the resource was not secured at all
chain.doFilter(request, response);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
super.unsuccessfulAuthentication(request, response, failed);
// Authentication process failed, filtering the request out.
}
}
UserDetails
public class AppUser implements UserDetails {
// A class to be used as a container for user details, you can add more details specific to your application here.
}
Finally, you need to configure Spring boot to use this classes:
SecurityConfig
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
// -- public paths, for example: swagger ui paths
new AntPathRequestMatcher("/swagger-ui.html"),
new AntPathRequestMatcher("/swagger-resources/**"),
new AntPathRequestMatcher("/v2/api-docs"),
new AntPathRequestMatcher("/webjars/**")
);
private JwtAuthenticationProvider provider;
public SecurityConfig(JwtAuthenticationProvider provider) {
this.provider = provider;
}
#Override
public void configure(final WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS) // Allowing browser pre-flight
.requestMatchers(PUBLIC_URLS);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
// this entry point handles when you request a protected page and you are not yet authenticated
//.defaultAuthenticationEntryPointFor(forbiddenEntryPoint(), PROTECTED_URLS)
.authenticationEntryPoint(forbiddenEntryPoint())
.and()
.authenticationProvider(this.provider)
.addFilterBefore(jwtAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
}
#Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
final JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
filter.setAuthenticationManager(this.authenticationManager());
filter.setAuthenticationSuccessHandler(this.successHandler());
filter.setAuthenticationFailureHandler(this.failureHandler());
return filter;
}
#Bean
JwtAuthenticationSuccessHandler successHandler() {
return new JwtAuthenticationSuccessHandler();
}
#Bean
JwtAuthenticationFailureHandler failureHandler() {
return new JwtAuthenticationFailureHandler();
}
/**
* Disable Spring boot automatic filter registration.
*/
#Bean
FilterRegistrationBean disableAutoRegistration(JwtAuthenticationFilter filter) {
final FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
#Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(FORBIDDEN);
}
}
AuthenticationFailureHandler
public class JwtAuthenticationFailureHandler implements AuthenticationFailureHandler {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
Map<String, Object> data = new HashMap<>();
data.put("exception", e.getMessage());
httpServletResponse.getOutputStream().println(objectMapper.writeValueAsString(data));
}
}
AuthenticationSuccessHandler
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
}
}
OKAY!
Now that you have implemented the security correctly, you can access user details and privileges from anywhere using the last piece:
UserDetailsService
#Service
public class AppUserService implements UserDetailsService {
#Autowired
private AppUserRepository appUserRepository;
public AppUser getCurrentAppUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null)
return (AppUser) authentication.getDetails();
return null;
}
public String getCurrentPrincipal() {
return (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
#Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Optional<AppUser> appUserOptional = this.appUserRepository.findByEmailsContains(new EmailEntity(s)); // This should be changed in your case if you are using something like AccountId
appUserOptional.ifPresent(AppUser::loadAuthorities);
return appUserOptional.orElse(null);
}
}
Great.
Let's see how to use it in your Controllers:
#PostMapping(path = "/profiles/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(#RequestParam("image") MultipartFile image) throws IOException {

AppUser user = this.appUserService.getCurrentAppUser();
Long id = user.getAccountId(); // Or profile id or any other identifier that you needed and extracted from the JWT after verification.
// set the profile picture.
// save changes of repository and return.
}
For admin purposes:
#PreAuthorize ("hasRole('ROLE_ADMIN')")
#PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#PathVariable("profileId") UUID profileId, #RequestParam("image") MultipartFile image) throws IOException {

AppUser user = this.appUserService.getCurrentAppUser();
// set the profile picture using profileId parameter
// save changes of repository and return.
}
The only remaining task is to assign the ROLE_ADMIN to the right user when loading it from the database. To do this, there are a lot of different approaches and it totally depends on your requirements. Overall, you can save a role in the database and relate it to a specific user and simply load it using an Entity.
Let's get few things right here , I am assuming that you have like two entities - Account and Profile and you wish to upload/update new profile image using same API -
#PostMapping(path = "/profiles/{profileId}/images
If ADMIN role , update profile image for #PathVariable("profileId") OR if USER role update their own profile image using #PathVariable("profileId") and not any other Profile entity image using ProfileId if current user is authenticated.
Please check this link for Role-Permission Authentication
Spring Boot : Custom Role - Permission Authorization using SpEL
User Principal
#Getter
#Setter
#Builder
public class UserPrincipal implements UserDetails {
/**
* Generated Serial ID
*/
private static final long serialVersionUID = -8983688752985468522L;
private Long id;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
private Collection<? extends GrantedAuthority> permissions;
public static UserPrincipal createUserPrincipal(Account account) {
if (userDTO != null) {
List<GrantedAuthority> authorities = userDTO.getRoles().stream().filter(Objects::nonNull)
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
List<GrantedAuthority> permissions = account.getRoles().stream().filter(Objects::nonNull)
.map(Role::getPermissions).flatMap(Collection::stream)
.map(permission -> new SimpleGrantedAuthority(permissionDTO.getName().name()))
.collect(Collectors.toList());
return UserPrincipal.builder()
.id(account.getId())
.email(account.getEmail())
.authorities(authorities)
.permissions(permissions)
.build();
}
return null;
}
AuthenticationFilter
public class AuthTokenFilter extends OncePerRequestFilter {
#Autowired
private JwtUtils jwtUtils;
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwtToken = getJwtTokenFromHttpRequest(request);
if (StringUtils.isNotBlank(jwtToken) && jwtUtils.validateToken(jwtToken)) {
Long accountId = jwtUtils.getAccountIdFromJwtToken(jwtToken);
UserDetails userDetails = customUserDetailsService.loadUserByUserId(accountId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails
.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception exception) {
}
filterChain.doFilter(request, response);
}
private String getJwtTokenFromHttpRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (!StringUtils.isEmpty(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
AuthUtil
#UtilityClass
public class AuthUtils {
public boolean isAdmin(UserPrincipal userPrincipal){
if(CollectionUtils.isNotEmpty(userPrincipal.getAuthorities())){
return userPrincipal.getRoles().stream()
.filter(Objects::nonNull)
.map(GrantedAuthority::getName)
.anyMatch(role -> role.equals("ROLE_ADMIN"));
}
return false;
}
}
Profile Service
#Service
public class ProfileService {
#Autowired
private ProfileRepository profileRepository;
public Boolean validateProfileIdForAccountId(Integer profileId, Long accountId) throws NotOwnerException,NotFoundException {
Profile profile = profileRepository.findByAccountId(profileId,accountId);
if(profile == null){
throw new NotFoundException("Profile does not exists for this account");
} else if(profile.getId() != profileId){
throw new NotOwnerException();
}
return true;
}
}
ProfileController
#PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')")
#PostMapping(path = "/profiles/{profileId}/images", consumes =
MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#AuthenticationPrincipal UserPrincipal currentUser,
#PathVariable("profileId") UUID profileId,
#RequestParam("image") MultipartFile image) throws IOException {
if(!AuthUtils.isAdmin(currentUser)){
profileService.validateProfileIdForAccountId(profileId, currentUser.getId());
}
}
Now you can validate whether the #PathVariable("profileId") does indeed belong to the authenticated CurrentUser, you are also checking if the CurrentUser is ADMIN.
You can also add & check any specific permission for ROLES for facilitating UPLOAD/UPDATE
#PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER') or hasPermission('UPDATE')")

Spring Security: Custom CSRF Implementation by extending CsrfRepository

I am trying to create a customized CSRF implementation in my Spring Boot application by implementing the CsrfRepository interface provided by Spring Security.
Below is how my custom repository looks like:
public class CustomCookieCsrfTokenRepository implements CsrfTokenRepository {
static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
#Override
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.DEFAULT_CSRF_HEADER_NAME, this.DEFAULT_CSRF_PARAMETER_NAME, createNewToken());
}
#Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
String tokenValue = token == null ? "" : token.getToken();
Cookie cookie = new Cookie(this.DEFAULT_CSRF_COOKIE_NAME, tokenValue);
cookie.setSecure(request.isSecure());
response.addCookie(cookie);
}
#Override
public CsrfToken loadToken(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, this.DEFAULT_CSRF_COOKIE_NAME);
if (cookie == null) {
return null;
}
String token = cookie.getValue();
if (!StringUtils.hasLength(token)) {
return null;
}
return new DefaultCsrfToken(this.DEFAULT_CSRF_HEADER_NAME, this.DEFAULT_CSRF_PARAMETER_NAME, token);
}
private String createNewToken() {
String unsignedToken = UUID.randomUUID().toString();
return RSAUtil.signMessage(unsignedToken, privateKey);
}
}
QUESTION: As you can see, I want to sign my cookie value using a private key and validate it using a public key. The question is where should this verification logic take place? I am guessing loadToken() method can have the logic to validate the signature. Is this the correct place or should it take place elsewhere?
Can someone provide some snippets or samples on how and where to handle this?
No, the verification logic should be in generateToken(HttpServletRequest request) of your custom CsrfTokenRepository implementation. The saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) should save the token (or delete the saved token when the passed 'token' param is null) and loadToken(HttpServletRequest request) should return the existing saved token (which was saved by saveToken method) for the current request/session;
#Component
public class CustomCsrfTokenRepository implements CsrfTokenRepository {
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
private String headerName = DEFAULT_CSRF_HEADER_NAME;
private String cookieName = "USER_INFO";
private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = CustomCsrfTokenRepository2.class
.getName().concat(".CSRF_TOKEN");
private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
#Override
public CsrfToken generateToken(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
if (cookie == null) {
return new DefaultCsrfToken(this.headerName, this.parameterName,
createNewToken());
}
String cookieValue = cookie.getValue();
String token = cookieValue.split("\\|")[0];
if (!StringUtils.hasLength(token)) {
return new DefaultCsrfToken(this.headerName, this.parameterName,
createNewToken());
}
return new DefaultCsrfToken(this.headerName, this.parameterName, token);
}
#Override
public void saveToken(CsrfToken token, HttpServletRequest request,
HttpServletResponse response) {
if (token == null) {
HttpSession session = request.getSession(false);
if (session != null) {
session.removeAttribute(this.sessionAttributeName);
}
}
else {
HttpSession session = request.getSession();
session.setAttribute(this.sessionAttributeName, token);
}
}
#Override
public CsrfToken loadToken(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return (CsrfToken) session.getAttribute(this.sessionAttributeName);
}
private String createNewToken() {
return UUID.randomUUID().toString();
}
}
And you need to set your customCsrfRepoImpl bean in HttpSecurity configuration as shown below
#Configuration
#EnableWebSecurity
public class SecurityConfigurarion extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {
#Autowired
private CsrfTokenRepository customCsrfTokenRepository; //your custom csrfToken repository impl class
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().csrfTokenRepository(customCsrfTokenRepository) //set your custom csrf impl in httpSecurity
.and()
.authorizeRequests()
.antMatchers(permittedUrlsArr).permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.logout()
}
}

How to add a custom OpenId Filter in a Spring boot application?

I am trying to implement the backend side of an OpenId Connect authentication. It is a stateless API so I added a filter that handles the Bearer token.
I have created the OpenIdConnect Filter that handles the Authentication and added it in a WebSecurityConfigurerAdapter.
public class OpenIdConnectFilter extends
AbstractAuthenticationProcessingFilter {
#Value("${auth0.clientId}")
private String clientId;
#Value("${auth0.issuer}")
private String issuer;
#Value("${auth0.keyUrl}")
private String jwkUrl;
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
public OpenIdConnectFilter() {
super("/connect/**");
setAuthenticationManager(new NoopAuthenticationManager());
}
#Bean
public FilterRegistrationBean registration(OpenIdConnectFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
try {
Authentication authentication = tokenExtractor.extract(request);
String accessToken = (String) authentication.getPrincipal();
String kid = JwtHelper.headers(accessToken)
.get("kid");
final Jwt tokenDecoded = JwtHelper.decodeAndVerify(accessToken, verifier(kid));
final Map<String, Object> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
verifyClaims(authInfo);
Set<String> scopes = new HashSet<String>(Arrays.asList(((String) authInfo.get("scope")).split(" ")));
int expires = (Integer) authInfo.get("exp");
OpenIdToken openIdToken = new OpenIdToken(accessToken, scopes, Long.valueOf(expires), authInfo);
final OpenIdUserDetails user = new OpenIdUserDetails((String) authInfo.get("sub"), "Test", openIdToken);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (final Exception e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
public void verifyClaims(Map claims) {
int exp = (int) claims.get("exp");
Date expireDate = new Date(exp * 1000L);
Date now = new Date();
if (expireDate.before(now) || !claims.get("iss").equals(issuer) || !claims.get("azp").equals(clientId)) {
throw new RuntimeException("Invalid claims");
}
}
private RsaVerifier verifier(String kid) throws Exception {
JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
Jwk jwk = provider.get(kid);
return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}
Here is security configuration:
#Configuration
#EnableWebSecurity
public class OpenIdConnectWebServerConfig extends
WebSecurityConfigurerAdapter {
#Bean
public OpenIdConnectFilter myFilter() {
final OpenIdConnectFilter filter = new OpenIdConnectFilter();
return filter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.antMatcher("/connect/**").authorizeRequests()
.antMatchers(HttpMethod.GET, "/connect/public").permitAll()
.antMatchers(HttpMethod.GET, "/connect/private").authenticated()
.antMatchers(HttpMethod.GET, "/connect/private-
messages").hasAuthority("read:messages")
.antMatchers(HttpMethod.GET, "/connect/private-
roles").hasAuthority("read:roles")
.and()
.addFilterBefore(myFilter(),
UsernamePasswordAuthenticationFilter.class);
}
Rest endpoints looks like following:
#RequestMapping(value = "/connect/public", method = RequestMethod.GET,
produces = "application/json")
#ResponseBody
public String publicEndpoint() throws JSONException {
return new JSONObject()
.put("message", "All good. You DO NOT need to be authenticated to
call /api/public.")
.toString();
}
#RequestMapping(value = "/connect/private", method = RequestMethod.GET,
produces = "application/json")
#ResponseBody
public String privateEndpoint() throws JSONException {
return new JSONObject()
.put("message", "All good. You can see this because you are
Authenticated.")
.toString();
}
If I remove completely the filter for configuration and also the #Bean definition, the configuration works as expected: /connect/public is accessible, while /connect/private is forbidden.
If I keep the #Bean definition and add it in filter chain the response returns a Not Found status for requests both on /connect/public and /connect/private:
"timestamp": "18.01.2019 09:46:11",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/
When debugging I noticed that filter is processing the token and returns an implementation of Authentication.
Is the filter properly added in filter chain and in correct position?
Why is the filter invoked also on /connect/public path when this is supposed to be public. Is it applied to all paths matching super("/connect/**") call?
Why is it returning the path as "/" when the request is made at /connect/private
Seems that is something wrong with the filter, cause every time it is applied, the response is messed up.

How to access a protected spring boot resource with access_token from command line , e.g. curl or httpie

In our spring boot application we use mitreid for openid connect. If we access a protected resource from browser, we are redirected to keycloak, our openid provider, and then after successful authentication, to the desired resource. This is working like intended.
Now we want to access the same resource from command line, e.g curl or httpie.
But nothing is working.
Getting access_token from keycloak returns an jwt with access_token and id_token.
But access with curl -H "Authorization: Bearer 'access_token>'" http://localhost:8080 doesn't work.
Here is my oidc configuration
public class OIDCSecurityConfiguration extends WebSecurityConfigurerAdapter {
public static final Logger LOG = LoggerFactory.getLogger(OIDCSecurityConfiguration.class);
private final String loginPath = OIDCAuthenticationFilter.FILTER_PROCESSES_URL;
#Value("${oidc.issuer-uri}")
private String issuerUri;
#Value("${oidc.client-uri}")
private String clientUri;
#Value("${oidc.client-id}")
private String clientId;
#Value("${oidc.client-secret}")
private String clientSecret;
#Value("${oidc.client-scopes}")
private String clientScopes;
private String getLogoutUri() {
final ServerConfiguration serverConfiguration = serverConfigurationService().getServerConfiguration(issuerUri);
if (serverConfiguration == null) {
LOG.error("OpenID Connect server configuration could not be retrieved");
return "/";
}
try {
return serverConfiguration.getEndSessionEndpoint() + "?redirect_uri=" + URLEncoder.encode(clientUri, "UTF-8");
} catch (UnsupportedEncodingException e) {
LOG.error("Cannot encode redirect uri");
return "/";
}
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(authenticationFilter(), AbstractPreAuthenticatedProcessingFilter.class)
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers("/logout").permitAll()
.and()
.formLogin().loginPage("/openid_connect_login")
.and()
.logout().logoutSuccessUrl(getLogoutUri());
}
#Autowired
public void configureAuthenticationProvider(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new LoginUrlAuthenticationEntryPoint(loginPath);
}
#Bean
public WebMvcConfigurerAdapter mvcInterceptor() {
return new UserInfoInterceptorAdapter();
}
#Bean
public AuthenticationProvider authenticationProvider() {
final OIDCExtendedAuthenticationProvider authenticationProvider = new OIDCExtendedAuthenticationProvider();
authenticationProvider.setAuthoritiesMapper(new KeycloakAuthoritiesMapper(clientId));
return authenticationProvider;
}
#Bean
public AbstractAuthenticationProcessingFilter authenticationFilter() throws Exception {
final OIDCAuthenticationFilter filter = new OIDCAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
filter.setServerConfigurationService(serverConfigurationService());
filter.setClientConfigurationService(clientConfigurationService());
final StaticSingleIssuerService issuer = new StaticSingleIssuerService();
issuer.setIssuer(this.issuerUri);
filter.setIssuerService(issuer);
// TODO: change to signed or encrypted builder for production.
filter.setAuthRequestUrlBuilder(new PlainAuthRequestUrlBuilder());
return filter;
}
#Bean
public ClientConfigurationService clientConfigurationService() {
final StaticClientConfigurationService service = new StaticClientConfigurationService();
final RegisteredClient client = new RegisteredClient();
client.setClientId(clientId);
client.setClientSecret(clientSecret);
client.setClientName(clientId);
client.setScope(Arrays.stream(clientScopes.split(",")).collect(Collectors.toSet()));
client.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
client.setRedirectUris(Collections.singleton(clientUri + loginPath));
client.setRequestObjectSigningAlg(JWSAlgorithm.RS256);
service.setClients(Collections.singletonMap(this.issuerUri, client));
return service;
}
#Bean
public ServerConfigurationService serverConfigurationService() {
final DynamicServerConfigurationService service = new DynamicServerConfigurationService();
service.setWhitelist(Collections.singleton(issuerUri));
return service;
}
private static class UserInfoInterceptorAdapter extends WebMvcConfigurerAdapter {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
}

Resources