How to create a principal based on an arbitrary http header? - spring-boot

I'm using spring security to secure my application, but I can't understand how the principal is actually created.
On the web I can only find references about how to provide a custom AuthenticationProvider but problem is that the principal will always be empty, this is because I need to construct it based on an http header, namely "MY_AUTH_HEADER".
This header contains data that I have to use in conjuction with a db.
So, starting by:
public class MedMadAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
authentication.getName(); //how can I create an "Authentication" object based on headers?
return null;
}
#Override
public boolean supports(Class<?> authentication) {
// TODO Auto-generated method stub
return false; //How can I specify my own auth class?
}
}
What do I have to define to be before this filter and create the Authentication object starting from headers?

The Authentication object gets extracted in an AbstractAuthenticationProcessingFilter.
You can create a custom AbstractAuthenticationProcessingFilter that creates an Authentication object based on information in your custom header.
In the example below, the CustomAuthenticationFilter will be invoked on requests to "/login".
It will create a custom Authentication object, namely CustomAuthenticationToken, based on the value in the header MY_AUTH_HEADER.
public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public CustomAuthenticationFilter(AuthenticationManager manager) {
super("/login");
setAuthenticationManager(manager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String authToken = request.getHeader("MY_AUTH_HEADER");
CustomAuthenticationToken authentication = new CustomAuthenticationToken(authToken);
return this.getAuthenticationManager().authenticate(authentication);
}
}
You can then add the custom filter to the security chain.
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.addFilterAt(new CustomAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
}
Depending on your authentication mechanism, you may be able to reuse an existing Authentication, such as BearerTokenAuthenticationToken or UsernamePasswordAuthenticationToken.
If you need to create a custom Authentication object, here is an example.
public class CustomAuthenticationToken extends AbstractAuthenticationToken {
private String token;
public CustomAuthenticationToken(String token) {
super(Collections.emptyList());
this.token = token;
}
#Override
public Object getCredentials() {
return this.token;
}
#Override
public Object getPrincipal() {
return this.token;
}
}
If you are using a custom Authentication object, then you can create a custom AuthenticationProvider to support your custom Authentication, as shown below.
This is not necessary if you are using an existing Authentication (e.g UsernamePasswordAuthenticationToken) as there is already an AuthenticationProvider that supports it.
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if ("secret".equals(authentication.getCredentials())) {
return authentication;
} else {
throw new BadCredentialsException("Header did not match expected value");
}
}
#Override
public boolean supports(Class<?> authentication) {
return CustomAuthenticationToken.class.isAssignableFrom(authentication);
}
}
This section of the Spring Security reference documentation explains how all these components interact with one another.

Related

Unable to use permitAll() with Spring Boot 2.3.4 to allow access to Swagger UI after integrating with API-Key Authentication

I tried integrating API-Key authentication mechanism to Spring Boot Application in the following way:
Created a CustomAPIKeyAuthFilter that extends AbstractPreAuthenticatedProcessingFilter where it gets the preauthenticated principal from the headers of the request.
public class CustomAPIKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
private String principalRequestHeader;
private String principalAuthKey;
public CustomAPIKeyAuthFilter(String principalRequestHeader, String principalAuthKey) {
this.principalRequestHeader = principalRequestHeader;
this.principalAuthKey = principalAuthKey;
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
return request.getHeader(principalRequestHeader);
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
// anything to be returned here??
return "TBD";
}
}
Created WebSecurityConfig that extends WebSecurityConfigurerAdapter. In this one, the custom filter is injected inside the overridden method protected void configure(HttpSecurity httpSecurity) {}
#EnableWebSecurity
#Order(1)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${superuser}")
private String principalRequestHeader;
#Value("${superuserauthkey}")
private String principalRequestValue;
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
CustomAPIKeyAuthFilter filter = new CustomAPIKeyAuthFilter(principalRequestHeader, principalRequestValue);
filter.setAuthenticationManager(new AuthenticationManager() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String principal = (String) authentication.getPrincipal();
if (principalRequestValue.equals(principal)){
authentication.setAuthenticated(true);
} else {
throw new BadCredentialsException("Missing API Key");
}
return authentication;
}
});
httpSecurity.
cors().and().
csrf().disable().authorizeRequests()
.antMatchers("**swagger**").permitAll() // this is the part that is not working for me
.anyRequest().authenticated()
.and()
.addFilter(filter)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
As you can see from the comment above, even though I used permitAll, I get the error 401 No pre-authenticated principal found in request at runtime if I try to access Swagger UI which was working before introducing spring-boot-starter-security related dependencies in my pom.xml. Is there a better way to exclude swagger UI alone from the list of URL end points that need API-key based authentication ?
Note: I am using springfox-swagger2 implementation of Swagger and the version used is 2.8.0.
Swagger have api endpoint which should be allowed in security level, add the below snippet in WebSecurityConfig.class
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/api-docs",
"/configuration/ui",
"/swagger-resources/**",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**");
}
You could also try permitAll() to the patterns included.This will exclude the swagger from being authenticated.

refresh_token grant_type error: UserDetailsService is required. But I dont want to specify one

I'm trying to create an Oauth authentication/authorization server using spring boot and dependencies
* spring-security-oauth2-autoconfigure
* nimbus-jose-jwt
and I'm following docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-security-oauth2-authorization-server
The issue is that I don't want to specify a UserDetailsService since the information about the user account is in another service that doesn't expose passwords. That service just has an API in which input is user/pass and output is user info (if the user exists/credentials are correct).
So my code/configuration is a little deviated from the documentation.
#EnableAuthorizationServer
#Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
//injections
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(jwtTokenStore)
.accessTokenConverter(accessTokenConverter)
.authenticationManager(authenticationManager);
}
}
and
#EnableWebSecurity
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//injections
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
authenticationManagerBuilder.authenticationProvider(travelerAuthenticationProvider); //my custom // authentication provider that calls the other service for checking credentials
}
}
and
#Component
public class TravelerAuthenticationProvider implements AuthenticationProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(TravelerAuthenticationProvider.class);
private OrderTravelerProfileClient travelerProfileClient;
public TravelerAuthenticationProvider(OrderTravelerProfileClient travelerProfileClient) {
this.travelerProfileClient = travelerProfileClient;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication.getName() == null || (authentication.getCredentials().toString().isEmpty())) {
return null;
}
var username = authentication.getName();
var password = authentication.getCredentials().toString();
try {
travelerProfileClient.authenticate(username, password);
} catch (Exception e) {
LOGGER.error("checking traveler {} credentials failed", username, e);
throw new BadCredentialsException("wrong traveler credentials");
}
var authorities = Set.of(new SimpleGrantedAuthority("traveler"));
var updatedAuthentication = new UsernamePasswordAuthenticationToken(username, password, authorities);
return updatedAuthentication;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Everything related to client_credentials and password flow works but when I try to use refresh_token flow, it complains that UserDetailsService is required. How should I solve the issue without defining a UserDetailsService and just relaying on my custom authentication provider?
UPDATE:
apparently refresh_token flow has a recheck for authentication (credentials) which needs another authentication provider for type PreAuthenticatedAuthenticationToken.class.
So I created a new auth provider like this:
#Component
public class TravelerRefreshTokenBasedAuthenticationProvider implements AuthenticationProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(TravelerRefreshTokenBasedAuthenticationProvider.class);
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
var currentAuthentication = (PreAuthenticatedAuthenticationToken) authentication;
//.....
return updatedAuthentication;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(PreAuthenticatedAuthenticationToken.class);
}
}
and update my security configs to:
#EnableWebSecurity
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//injections
//this bean will be more configured by the method below and it will be used by spring boot
//for authenticating requests. Its kind of an equivalent to userDetailsService
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
authenticationManagerBuilder.authenticationProvider(travelerUserPassBasedAuthenticationProvider);
authenticationManagerBuilder.authenticationProvider(travelerRefreshTokenBasedAuthenticationProvider);
}
}
the issue is spring doesn't recognize my auth providers in refresh_token flow and tries to use a default one. And the default one is trying to use a UserDetailsService that doesn't exist.
I also feel that I don't need to create another provider and I can reuse the previous one. Because the check for which spring is failing to use my custom provider is a check against user/pass; which I was doing in my previous auth provider.
so all in all, until now, I feel I have to introduce my custom provider to spring differently for refresh_token flow comparing to password flow
Your AuthenticationProvider implementation only supports UsernamePasswordAuthenticationToken, which is used for username/password authentication, while the refresh_token flow tries to renew authentication using PreAuthenticatedAuthenticationToken (see DefaultTokenServices.java).
So you need to create another AuthenticationProvider for PreAuthenticatedAuthenticationToken and add it to AuthenticationManagerBuilder.
Update:
I've found that AuthorizationServerEndpointsConfigurer creates a new instance of DefaultTokenServices, if none is assigned, which in turn creates a new instance of PreAuthenticatedAuthenticationProvider and does not use the provided AuthenticationManager. To avoid this, you can create your own instance of DefaultTokenServices and pass it to AuthorizationServerEndpointsConfigurer:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(jwtTokenStore)
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(accessTokenConverter)
.authenticationManager(authenticationManager)
.tokenServices(createTokenServices(endpoints, authenticationManager));
}
private DefaultTokenServices createTokenServices(AuthorizationServerEndpointsConfigurer endpoints, AuthenticationManager authenticationManager) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAuthenticationManager(authenticationManager);
return tokenServices;
}

Authorization Error handling when Authorization service down in Spring boot security doesn't work

In my spring boot web app, I hit a third party service for Authorization and my application is just a content provider. Parent application uses site minder for authentication. My application gets user Id in header and makes call to third party api to set UserDetails with authorities.
My requirement is to handle scenario when third party service for authorization is down. Currently in this case I set UserDetails with no roles and since every endpoint is bound by authorization so I get 403 if third party service for authorization is down.
But I want to display different message if user lacks authorization and if authorization service is down.
If I handle authorization service down by throwing custom exception from UserDetailsServiceImpl -> loadUserByUserName() then RequestHeaderAuthenticationFilter encounters this exception and request gets filtered out. Any idea how to get this done ?
SecurityConfiguration
public class WebSecurityCustomConfig extends WebSecurityConfigAdapter {
private UserDetailsService userDetails;
protected void configure(HttpSecurity http) {
http.csrf().disable().authorizeRequests().antMatchers("/*).permitAll()
.anyRequests()
.hasAnyAuthority("MODULEX","MODULEY");
http.addFilterBefore(requestHeaderAuthFilter(),
BasicAuthenticationFilter.class);
http.exceptionHandling().authenticationEntryPoint(customEntryPoint());
}
protect void configure(AuthenticationManagerBuilder builder) {
PreAuthenticaticatedAuthenticationProvider auth = new
PreAuthenticaticatedAuthenticationProvider ();
auth.setPreAuthenticatedUserDetailsService(new
UserDetailsByNameServiceWrapper<>(userDetails));
}
}
Custom UserDetailsService
public class CustomUserDetailsService implements UserDetailsService {
private final AuthorizationService authService;
#Inject
public CustoUserDetailsService(AuthorizationService authService) {
this.authService = authService;
}
public UserDetails loadUserByUsername(String username) {
return new User(username, "",
authService.getAuthorities(username));
// authService is a third party jar and if their upstream service
//is down , it throws a runtime exception
}
}
If I handle their error as follows then I end up with 403 but I want 503 in case service is down and 403 if user doesnt have right authority for endpoint he is accessing.
current handling auth service exception
public UserDetails loadUserByUsername(String username) {
try{
return new User(username, "",
authService.getAuthorities(username));
}
catch(AuthServiceException e) {
return new User(username, "",
Collections.emptyList());
}
}
Implement AuthenticationEntryPoint and override commence() method as follows.
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
And after that create a method in your WebSecurityCustomConfig class that initialize the RestAuthenticationEntryPoint class as follows.
public AuthenticationEntryPoint authenticationEntryPoint() {
RestAuthenticationEntryPoint ep = new RestAuthenticationEntryPoint();
return ep;
}
Then change your following line to http.exceptionHandling().authenticationEntryPoint(customEntryPoint());
to
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());

How to implement UsernamePasswordAuthenticationFilter for email field instead of username in Spring Security

There is UsernamePasswordAuthenticationFilter in SpringSecurity itself but I want to use email instead of username.
I had used this class for username like this :
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter{
private AuthenticationManager authenticationManager;
private JwtUserDetailsToUserDTOConverter jwtUserDetailsToUserDTOConverter = new JwtUserDetailsToUserDTOConverter();
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String token = JwtGenerator.builder().build().generate((JwtUserDetails) authResult.getPrincipal());
response.addHeader(JwtConstants.HEADER,JwtConstants.PREFIX + token);
}
}
I want to implement AuthenticationFilter for email and password. There is no any specified class for this intent inside of SpringSecurity.
How can I customize it for email?
UsernamePasswordAuthenticationFilter just extracts username (which could be email) and password from request and forwards it to the configured AuthenticationManager.
The default (assuming you haven't overridden it), implementation of AuthenticationManager used is ProviderManager.
ProviderManager in turn is configured with an AuthenticationProvider. This would be DaoAuthenticationProvider in most cases.
From DaoAuthenticationProvider, call reaches UserDetailsService#loadUserByUsername() which tries to look up a user from DB based on given username.
So you have couple of options here:
Create a custom UserDetailsService, with overridden loadUserByUsername(), which matches the given username against user's email. This can then be set into AuthenticationManagerBuilder instance like this:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(myUserDetailsService);
}
}
Custom AuthenticationProvider which validates user against email. This is a bit of an overkill for your scenario and would result in duplication of code. But if you want to do this, use the same code as #1 but invoke auth.authenticationProvider(myAuthProviderInstance); instead

Multiple custom authentication with spring security

I have a spring application which uses a custom Authentication Filter say filter1 to authorize the request, this filter uses an authentication manager for authentication and is applicable for all urls in application.
Now, I want to implement a different Authentication Filter say filter2 which has to authorize special kind of request say with url (/api/). That is the all the request which has the url like (/api/**) has to use filter2.
Below is the code I've tried so far for this purpose.
public class SecurityAppConfig {
#Configuration
#Order(1)
public static class APISecurityConfig extends WebSecurityConfigurerAdapter {
private CustomAuthenticationManager1 manager1 = new CustomAuthenticationManager1();
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.formLogin().disable().csrf().disable().cors().disable().logout().disable();
if (manager1 != null) {
http.addFilterAfter(new Filter1(manager1),
AnonymousAuthenticationFilter.class);
}
}
}
#Configuration
#Order(2)
public static class OtherApiSecurityConfig extends WebSecurityConfigurerAdapter {
private AuthenticationManager2 manager2 = new AuthenticationManager2();
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.formLogin().disable().csrf().disable().cors().disable().logout().disable();
if (manager2 != null) {
http.antMatchers("/api/**").addFilterAfter(new Filter2(manager2),
AnonymousAuthenticationFilter.class);
}
}
}
}
At the time of app start up both the filter are getting registered with their manager but when this ("/api/**") request comes it goes to the first filter for authentication but never goes to the second filter. If I remove the first filter then it works properly but that would override the filters for other api request.
Below is how I've implemented managers and filters
public class Filter1 extends AbstractAuthenticationProcessingFilter {
//implementation omitted for brevity.
}
public class Filter2 extends AbstractAuthenticationProcessingFilter {
//implementation omitted for brevity.
}
public class AuthenticationManager1 implements AuthenticationManager {
//implementation omitted for brevity.
}
public class AuthenticationManager2 implements AuthenticationManager {
//implementation omitted for brevity.
}
Any thoughts on how can I get this working.
I don't think that you need two configs for your case. And I don't see why you need to implement your own authentication manager, even two of them. I guess you should use shared authentication manager instead, implement your own AuthenticationProvider (one for each type of authentication), and implement youe own authentication tokens. Besides that, since you're using AbstractAuthenticationProcessingFilter as a base class for you filters - you can set filterProcessesUrl into it, so your filter knows to which URL's it should be applied. So, in brief:
Authentication Tokens:
public class MyAuth1AuthenticationToken extends AbstractAuthenticationToken {
// Implementation depends on you auth scheme (you can look on
// `UsernamePasswordAuthenticationToken` for example)
}
public class MyAuth2AuthenticationToken extends AbstractAuthenticationToken {
// ...
}
Authentication Providers:
public class MyAuth1AuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// Implementation really depends on you auth scheme (you can look on
// `AbstractUserDetailsAuthenticationProvider` for example)
}
#Override
public boolean supports(Class<?> authentication) {
// By this we're saying that this auth provider is responsible for our MyAuth1 auth request
return (MyAuth1AuthenticationToken.class.isAssignableFrom(authentication));
}
}
public class MyAuth2AuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// ...
}
#Override
public boolean supports(Class<?> authentication) {
return (MyAuth2AuthenticationToken.class.isAssignableFrom(authentication));
}
}
Filters:
public class Auth1Filter extends AbstractAuthenticationProcessingFilter {
public Auth1Filter(AuthenticationManager authManager, String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
setAuthenticationManager(authManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// extract user info here
// ...
// populate auth request with your info
MyAuth1AuthenticationToken authRequest = new MyAuth1AuthenticationToken(...);
// authenticate
return this.getAuthenticationManager().authenticate(authRequest);
}
}
public class Auth2Filter extends AbstractAuthenticationProcessingFilter {
public Auth2Filter(AuthenticationManager authManager, String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
setAuthenticationManager(authManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// extract user info here
// ...
// populate auth request with your info
MyAuth2AuthenticationToken authRequest = new MyAuth1AuthenticationToken(...);
// authenticate
return this.getAuthenticationManager().authenticate(authRequest);
}
}
Security Config:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception
{
// registering our providers
auth
.authenticationProvider(new MyAuth1AuthenticationProvider())
.authenticationProvider(new MyAuth2AuthenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin().disable()
.csrf().disable()
.cors().disable()
.logout().disable();
AuthenticationManager authManager = http.getSharedObject(AuthenticationManager.class);
http.addFilterAfter(new Auth1Filter(authManager, "/**"), BasicAuthenticationFilter.class);
http.addFilterAfter(new Auth2Filter(authManager, "/api/**"), BasicAuthenticationFilter.class);
}
}
Hope it helps.

Resources