By extending WebSecurityConfigurerAdapter, How to construct configure with custom authentication logic - java

I am using okta to do authentication. Our company's okta disabled the 'default' authorization server. So right now I cannot use 'okta-spring-security-starter' to simple do this to verify token passed from url headers:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/health").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
http.cors();
Okta.configureResourceServer401ResponseBody(http);
}
}
So I need to hit okta introspect endpoint (https://developer.okta.com/docs/reference/api/oidc/#introspect) to verify. So I am wondering can I integrate this procedure within the config of WebSecurityConfigurerAdapter. maybe something like this???:
import com.okta.spring.boot.oauth.Okta;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/health").permitAll()
.anyRequest().authenticated()
.and()
/*add something there*/
http.cors();
}
}
I saw something like override AuthenticationProvider(Custom Authentication provider with Spring Security and Java Config), and use httpbasic auth. Can I do similiar thing if I use .oauth2ResourceServer().jwt().
My idea is override the authentication provider and in the provider, hit the okta introspect endpoint, will this work???

Spring Security 5.2 ships with support for introspection endpoints. Please take a look at the Opaque Token sample in the GitHub repo.
To answer briefly here, though, you can do:
http
.authorizeRequests(authz -> authz
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaque -> opaque
.introspectionUri("the-endpoint")
.introspectionClientCredentials("client-id", "client-password")
)
);
If you are using Spring Boot, then it's a bit simpler. You can provide those properties in your application.yml:
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: ...
client-id: ...
client-secret: ...
And then your DSL can just specify opaqueToken:
http
.authorizeRequests(authz -> authz
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaque -> {})
);

I don't use Okta thus I don't know how exactly it works. But I have 2 assumptions:
Every request contains an accessToken in the Authorization header
You make a POST request to ${baseUrl}/v1/introspect and it will answer you with true or false to indicate that accessToken is valid or not
With these 2 assumptions in mind, if I have to manually implement custom security logic authentication, I would do following steps:
Register and implement a CustomAuthenticationProvider
Add a filter to extract access token from request
Registering custom authentication provider:
// In OktaOAuth2WebSecurityConfig.java
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider());
}
#Bean
CustomAuthenticationProvider customAuthenticationProvider(){
return new CustomAuthenticationProvider();
}
CustomAuthenticationProvider:
public class CustomAuthenticationProvider implements AuthenticationProvider {
private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationProvider.class);
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
logger.debug("Authenticating authenticationToken");
OktaTokenAuthenticationToken auth = (OktaTokenAuthenticationToken) authentication;
String accessToken = auth.getToken();
// You should make a POST request to ${oktaBaseUrl}/v1/introspect
// to determine if the access token is good or bad
// I just put a dummy if here
if ("ThanhLoyal".equals(accessToken)){
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("USER"));
logger.debug("Good access token");
return new UsernamePasswordAuthenticationToken(auth.getPrincipal(), "[ProtectedPassword]", authorities);
}
logger.debug("Bad access token");
return null;
}
#Override
public boolean supports(Class<?> clazz) {
return clazz == OktaTokenAuthenticationToken.class;
}
}
To register the filter to extract accessToken from request:
// Still in OktaOAuth2WebSecurityConfig.java
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(accessTokenExtractorFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests().anyRequest().authenticated();
// And other configurations
}
#Bean
AccessTokenExtractorFilter accessTokenExtractorFilter(){
return new AccessTokenExtractorFilter();
}
And the filter it self:
public class AccessTokenExtractorFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(AccessTokenExtractorFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.debug("Filtering request");
Authentication authentication = getAuthentication(request);
if (authentication == null){
logger.debug("Continuing filtering process without an authentication");
filterChain.doFilter(request, response);
} else {
logger.debug("Now set authentication on the request");
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
private Authentication getAuthentication(HttpServletRequest request) {
String accessToken = request.getHeader("Authorization");
if (accessToken != null){
logger.debug("An access token found in request header");
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("USER"));
return new OktaTokenAuthenticationToken(accessToken, authorities);
}
logger.debug("No access token found in request header");
return null;
}
}
I have uploaded a simple project here for your easy reference: https://github.com/MrLoyal/spring-security-custom-authentication
How it works:
The AccessTokenExtractorFilter is placed right after the UsernamePasswordAuthenticationFilter, which is a default filter by Spring Security
A request arrives, the above filter extracts accessToken from it and place it in the SecurityContext
Later, the AuthenticationManager calls the AuthenticationProvider(s) to authenticate request. This case, the CustomAuthenticationProvider is invoked
BTW, your question should contain spring-security tag.
Update 1: About AuthenticationEntryPoint
An AuthenticationEntryPoint declares what to do when an unauthenticated request arrives ( in our case, what to do when the request does not contain a valid "Authorization" header).
In my REST API, I simply response 401 HTTP status code to client.
// CustomAuthenticationEntryPoint
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.reset();
response.setStatus(401);
// A utility method to add CORS headers to the response
SecUtil.writeCorsHeaders(request, response);
}
Spring's LoginUrlAuthenticationEntryPoint redirects user to login page if one is configured.
So if you want to redirect unauthenticated requests to Okta's login page, you may use a AuthenticationEntryPoint.

Related

Invalid password is accepted in spring security using custom Authentication Provider

I am using spring security with custom Authentication Provider using basic auth.
When I am trying to hit backend API GET call through postman it is working fine only when I make changes in username
Here is the problem statement - whenever I modify the user name then only custom authenticator provider works. once I added the correct username and password then it works but after that when I am making any changes in password (giving wrong password) always showing 200 success response. If I am making changes in username (giving wrong username) then only call to custom authenticator provider happened and getting 401 response.
Java Spring code
#Configuration
#EnableWebSecurity
#ComponentScan("com.authentication.service")
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{
#Autowired
private AuthService authProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception { http
.httpBasic().and().logout().clearAuthentication(true).and() .authorizeRequests()
.antMatchers("/index.html", "/", "/home", "/login", "/assets/**").permitAll()
.anyRequest().authenticated() .and() .csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); }
}
#Service
public class AuthService implements AuthenticationProvider{
#Override
public Authentication authenticate(Authentication authentication)
throws org.springframework.security.core.AuthenticationException {
String userName = (String) authentication.getPrincipal();
String userPassword = authentication.getCredentials().toString();
if(authenticateUser(userName, userPassword)) {
return new UsernamePasswordAuthenticationToken(userName, userPassword, new ArrayList<>());
} else {
return null;
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
public Boolean authenticateUser(String userName, String userPassword) {
// Using some third party service
// return true if user is authenticated else false
}
}
This is occurring because Spring Security is creating a session that is returned as a Cookie upon successful authentication (You can check this in the Cookies panel in the Postman's response). For Further requests, even if you provide invalid credentials, Postman will send this session cookie that will be used for Authentication.
To remove this effect, you can update your session management policy to be SessionCreationPolicy.STATELESS, this will make sure no session is created by the application and the Basic Auth credentials that are sent in the request are used for authentication.
You can update the Session Management Policy like this:
#Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}

Microservices Spring Cloud Gateway + Spring Security LDAP as SSO + JWT - Token lost between request/response

I am developing a microservice ecosystem using spring-boot. The microservices which are in place at the moment :
Spring Cloud Gateway - Zuul (responsible also for authorization requests downstream for microservices - extracting tokens from requests and validates whether the user has the right role to perform requests),
SSO using spring security LDAP ( responsible for authenticate user and generate JWT tokens) , SSO has also just a login page using thymeleaf
Web interface using Thymeleaf without login page ( not sure if I should use here spring security, at the moment)
Another microservice which provides data to web ui based on request from the browser
Discovery services using Eureka
The idea is filtering all the requests on the gateway for validating and forward the requests. If the user is not authenticated or token is experied then forward the user to SSO for login.
The firewall will expose only the port on Gateway side then others one will be theirs ports blocked using firewall rules.
Now i am blocked without knowing where to go or if I should move the SSO together with the gateway ( conceptually wrong but it might be a workaround if i do not find any solution)
Following the issue : The user hits the gateway (ex. http://localhost:7070/web) then the gateway forward the user to (ex. http://localhost:8080/sso/login), after the credentials have been validated , the SSO creates the JWT tokens and add it to the Header of response.
Afterwards the SSO redirect the request back to the gateway (ex. http://localhost:7070/web).
Until here, everything works fine but when the request reaches the gateway there is no 'Authorization' header on request which means NO JWT token.
So the gateway should extract the token, check the credentials and forward the request to the Web interface (ex. http://localhost:9090)
I am aware that using Handler on SSO to redirect request won't work at all due to 'Redirect' from spring will remove the token from the header before redirect.
But I do not know whether there is another way to set again the JWT on the header after Spring has removed it from the request or not.
Is there any conceptually issue on the architecture side? How can I forward the JWT to the gateway for being checked?
SSO
#EnableWebSecurity
public class SecurityCredentialsConfig extends WebSecurityConfigurerAdapter {
#Value("${ldap.url}")
private String ldapUrl;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// Stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin()
.loginPage("/login")
// Add a handler to add token in the response header and forward the response
.successHandler(jwtAuthenticationSuccessHandler())
.failureUrl("/login?error")
.permitAll()
.and()
// handle an authorized attempts
.exceptionHandling()
.accessDeniedPage("/login?error")
.and()
.authorizeRequests()
.antMatchers( "/dist/**", "/plugins/**").permitAll()
.anyRequest().authenticated();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups")
.userSearchFilter("uid={0}")
.groupSearchBase("ou=groups")
.groupSearchFilter("uniqueMember={0}")
.contextSource()
.url(ldapUrl);
}
#Bean
public AuthenticationSuccessHandler jwtAuthenticationSuccessHandler() {
return new JwtAuthenticationSuccessHandler();
}
}
public class JwtAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
#Autowired
private JwtConfig jwtConfig;
#Autowired
private JwtTokenService jwtTokenService;
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException {
String token = jwtTokenService.expiring(ImmutableMap.of(
"email", auth.getName(),
"authorities", auth.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.map(Object::toString)
.collect(Collectors.joining(","))));
response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token);
DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
if(defaultSavedRequest != null){
getRedirectStrategy().sendRedirect(request, response, defaultSavedRequest.getRedirectUrl());
}else{
getRedirectStrategy().sendRedirect(request, response, "http://localhost:7070/web");
}
}
}
Gateway
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtConfig jwtConfig;
#Value("${accessDeniedPage.url}")
private String accessDeniedUrl;
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf().disable() // Disable CSRF (cross site request forgery)
// we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin()
.loginPage("/sso/login")
.permitAll()
.and()
// handle an authorized attempts
// If a user try to access a resource without having enough permissions
.exceptionHandling()
.accessDeniedPage(accessDeniedUrl)
//.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
// Add a filter to validate the tokens with every request
.addFilterBefore(new JwtTokenAuthenticationFilter(jwtConfig), UsernamePasswordAuthenticationFilter.class)
// authorization requests config
.authorizeRequests()
.antMatchers("/web/**").hasAuthority("ADMIN")
// Any other request must be authenticated
.anyRequest().authenticated();
}
}
#RequiredArgsConstructor
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtConfig jwtConfig;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 1. get the authentication header. Tokens are supposed to be passed in the authentication header
String header = request.getHeader(jwtConfig.getHeader());
// 2. validate the header and check the prefix
if(header == null || !header.startsWith(jwtConfig.getPrefix())) {
chain.doFilter(request, response); // If not valid, go to the next filter.
return;
}
// If there is no token provided and hence the user won't be authenticated.
// It's Ok. Maybe the user accessing a public path or asking for a token.
// All secured paths that needs a token are already defined and secured in config class.
// And If user tried to access without access token, then he/she won't be authenticated and an exception will be thrown.
// 3. Get the token
String token = header.replace(jwtConfig.getPrefix(), "");
try { // exceptions might be thrown in creating the claims if for example the token is expired
// 4. Validate the token
Claims claims = Jwts.parser()
.setSigningKey(jwtConfig.getSecret().getBytes())
.parseClaimsJws(token)
.getBody();
String email = claims.get("email").toString();
if(email != null) {
String[] authorities = ((String) claims.get("authorities")).split(",");
final List<String> listAuthorities = Arrays.stream(authorities).collect(Collectors.toList());
// 5. Create auth object
// UsernamePasswordAuthenticationToken: A built-in object, used by spring to represent the current authenticated / being authenticated user.
// It needs a list of authorities, which has type of GrantedAuthority interface, where SimpleGrantedAuthority is an implementation of that interface
final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
email, null, listAuthorities
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()));
// 6. Authenticate the user
// Now, user is authenticated
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
// In case of failure. Make sure it's clear; so guarantee user won't be authenticated
SecurityContextHolder.clearContext();
}
// go to the next filter in the filter chain
chain.doFilter(request, response);
}
}
#Component
public class AuthenticatedFilter extends ZuulFilter {
#Override
public String filterType() {
return PRE_TYPE;
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() throws ZuulException {
final Object object = SecurityContextHolder.getContext().getAuthentication();
if (object == null || !(object instanceof UsernamePasswordAuthenticationToken)) {
return null;
}
final UsernamePasswordAuthenticationToken user = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
final RequestContext requestContext = RequestContext.getCurrentContext();
/*
final AuthenticationDto authenticationDto = new AuthenticationDto();
authenticationDto.setEmail(user.getPrincipal().toString());
authenticationDto.setAuthenticated(true);
authenticationDto.setRoles(user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList())); */
try {
//requestContext.addZuulRequestHeader(HttpHeaders.AUTHORIZATION, (new ObjectMapper()).writeValueAsString(authenticationDto));
requestContext.addZuulRequestHeader(HttpHeaders.AUTHORIZATION, (new ObjectMapper()).writeValueAsString("authenticationDto"));
} catch (JsonProcessingException e) {
throw new ZuulException("Error on JSON processing", 500, "Parsing JSON");
}
return null;
}
}
There is an issue about JWT. It is called "Logout Problem". First you need to understand what it is.
Then, check TokenRelay filter (TokenRelayGatewayFilterFactory) which is responsible for passing authorization header to downstream.
If you look at that filter, you will see that JWTs are stored in ConcurrentHashMap (InMemoryReactiveOAuth2AuthorizedClientService). The key is session, the value is JWT. So, session-id is returned instead of JWT header as the response provided.
Until here, everything works fine but when the request reaches the
gateway there is no 'Authorization' header on request which means NO
JWT token.
Yes. When the request comes to gateway, TokenRelay filter takes session-id from request and find JWT from ConcurrentHashMap, then it passes to Authorization header during downstream.
Probably, this flow is designed by spring security team to address JWT logout problem.

Spring Security authorization each request in REST app (without login form)

I have POST request with something like this in my Spring project:
{"clientKey":"XXX", "accessKey":"ZZZ", ... }
My backend works in very simple paradigm: get clientKey (login) and accessKey (password) params from POST body, check their persistence in DB and then do some business logic.
I need to implement minimal security check logic using Spring Security for each incoming request (without sessions and tokens).
SecurityConfig.java
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").hasRole("USER")
.and().csrf().disable();
http.addFilterBefore(new ApiAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
ApiAuthorizationFilter.java
public class ApiAuthorizationFilter extends UsernamePasswordAuthenticationFilter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//
// always prints "{}", why?
//
Logger.getLogger("test").log(Level.INFO, request.getParameterMap().toString());
//
// Ok, I will make some manual auth operations for testing purposes.
// Seems what it isn't work too..
//
Set<SimpleGrantedAuthority> authorities = new HashSet<>(1);
authorities.add(new SimpleGrantedAuthority("USER"));
Authentication auth = new UsernamePasswordAuthenticationToken(
"94fc97a7b3fd2175472ec4a41bcb3b14",
"746b2aa32fe90f0ba53e6efe7a8d1f1f",
authorities);
SecurityContextHolder.getContext().setAuthentication(auth);
chain.doFilter(request, response);
}
}
What I doing wrong? Does UsernamePasswordAuthenticationFilter work only with login forms when submitted or I need another filter in security chain?

Spring Security for URL with permitAll() and expired Auth Token

I'm using Spring 4 with Spring Security, custom GenericFilterBean and AuthenticationProvider implementations. I have mostly secured URLs with the exception of a URL to create new session: /v2/session (e.g. login based on the username and password and returns Auth Token to be used in the subsequent requests that require authentication) configured as follows:
#Configuration
#ComponentScan(basePackages={"com.api.security"})
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ApiAuthenticationProvider apiAuthenticationProvider;
#Autowired
private AuthTokenHeaderAuthenticationFilter authTokenHeaderAuthenticationFilter;
#Autowired
private AuthenticationEntryPoint apiAuthenticationEntryPoint;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(apiAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(authTokenHeaderAuthenticationFilter, BasicAuthenticationFilter.class) // Main auth filter
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/v2/session").permitAll()
.anyRequest().authenticated();
http.exceptionHandling()
.authenticationEntryPoint(apiAuthenticationEntryPoint);
}
}
The authTokenHeaderAuthenticationFilter runs on every request and gets Token from the request header:
/**
* Main Auth Filter. Always sets Security Context if the Auth token Header is not empty
*/
#Component
public class AuthTokenHeaderAuthenticationFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
final String token = ((HttpServletRequest) request).getHeader(RequestHeaders.AUTH_TOKEN_HEADER);
if (StringUtils.isEmpty(token)) {
chain.doFilter(request, response);
return;
}
try {
AuthenticationToken authRequest = new AuthenticationToken(token);
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
} catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
return;
}
chain.doFilter(request, response); // continue down the chain
}
}
The custom apiAuthenticationProvider will try to authenticate all requests based on the token provided in the header and if authentication is unsuccessful - throws AccessException and client will receive HTTP 401 response:
#Component
public class ApiAuthenticationProvider implements AuthenticationProvider {
#Autowired
private remoteAuthService remoteAuthService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
AuthenticationToken authRequest = (AuthenticationToken) authentication;
String identity = null;
try {
identity = remoteAuthService.getUserIdentityFromToken(authRequest.getToken());
} catch (AccessException e) {
throw new InvalidAuthTokenException("Cannot get user identity from the token", e);
}
return new AuthenticationToken(identity, authRequest.getToken(), getGrantedAuthorites());
}
}
This works perfectly fine for the requests that require authentication. This works fine for the /v2/session request without the Authentication Header in it. However, for the /v2/session request that has an expired Auth Token in the header (or in the cookie - not shown in the code samples; this may happen sometimes if the client didn't clear the headers or continues sending cookies with requests) the security context will be initialized and apiAuthenticationProvider will throw an exception and respond with HTTP 401 to the client.
Since /v2/session has been configured as
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/v2/session").permitAll()
I would expect Spring Security to determine that before calling ApiAuthenticationProvider.authenticate(). What should be the way for the filter or auth provider to ignore/not throw the exception for the URLs configured as permitAll()?
Spring security filters get triggered before the request authorisation checks are performed. For the authorisation checks to work, it is assumed that the request has been through the filters and the Spring security context has been set (or not, depending on whether authentication credentials have been passed in).
In your filter you have check that continues with the filter chain processing if the token is not there. Unfortunately, if it is, then it will be passed to your provider for authentication, which throws an exception because the token has expired thus you're getting the 401.
Your best bet is to bypass filter execution for the URLs that you consider public. You can either do this in the filter itself or in your configuration class. Add the following method to your SecurityConfig class:
#Override
public void configure(WebSecurity webSecurity) {
webSecurity.ignoring().antMatchers(HttpMethod.POST, "/v2/session");
}
What this will do, is bypass your AuthTokenHeaderAuthenticationFilter completely for POST /v2/sessions URL.

Spring Security custom authentication filter using Java Config

I'm trying to configure Spring Security using Java config in a basic web application to authenticate against an external web service using an encrypted token provided in a URL request parameter.
I would like (I think) to have a security filter that intercepts requests from the Login Portal (they all go to /authenticate), the filter will use an AuthenticationProvider to process the bussiness logic of the authentication process.
Login Portal --> Redirect '\authenticate' (+ Token) --> Authenticate Token back to Login Portal (WS) --> If success get roles and setup user.
I have created a filter..
#Component
public final class OEWebTokenFilter extends GenericFilterBean {
#Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
OEToken token = extractToken(request);
// dump token into security context (for authentication-provider to pick up)
SecurityContextHolder.getContext().setAuthentication(token);
}
}
chain.doFilter(request, response);
}
An AuthenticationProvider...
#Component
public final class OEWebTokenAuthenticationProvider implements AuthenticationProvider {
#Autowired
private WebTokenService webTokenService;
#Override
public boolean supports(final Class<?> authentication) {
return OEWebToken.class.isAssignableFrom(authentication);
}
#Override
public Authentication authenticate(final Authentication authentication) {
if (!(authentication instanceof OEWebToken)) {
throw new AuthenticationServiceException("expecting a OEWebToken, got " + authentication);
}
try {
// validate token locally
OEWebToken token = (OEWebToken) authentication;
checkAccessToken(token);
// validate token remotely
webTokenService.validateToken(token);
// obtain user info from the token
User userFromToken = webTokenService.obtainUserInfo(token);
// obtain the user from the db
User userFromDB = userDao.findByUserName(userFromToken.getUsername());
// validate the user status
checkUserStatus(userFromDB);
// update ncss db with values from OE
updateUserInDb(userFromToken, userFromDB);
// determine access rights
List<GrantedAuthority> roles = determineRoles(userFromDB);
// put account into security context (for controllers to use)
return new AuthenticatedAccount(userFromDB, roles);
} catch (AuthenticationException e) {
throw e;
} catch (Exception e) {
// stop non-AuthenticationExceptions. otherwise full stacktraces returned to the requester
throw new AuthenticationServiceException("Internal error occurred");
}
}
And my Spring Security Config
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
OESettings oeSettings;
#Bean(name="oeAuthenticationService")
public AuthenticationService oeAuthenticationService() throws AuthenticationServiceException {
return new AuthenticationServiceImpl(new OEAuthenticationServiceImpl(), oeSettings.getAuthenticateUrl(), oeSettings.getApplicationKey());
}
#Autowired
private OEWebTokenFilter tokenFilter;
#Autowired
private OEWebTokenAuthenticationProvider tokenAuthenticationProvider;
#Autowired
private OEWebTokenEntryPoint tokenEntryPoint;
#Bean(name="authenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(tokenAuthenticationProvider);
}
#Bean
public FilterRegistrationBean filterRegistrationBean () {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(tokenFilter);
registrationBean.setEnabled(false);
return registrationBean;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/authenticate**").permitAll()
.antMatchers("/resources/**").hasAuthority("ROLE_USER")
.antMatchers("/home**").hasAuthority("ROLE_USER")
.antMatchers("/personSearch**").hasAuthority("ROLE_ADMIN")
// Spring Boot actuator endpoints
.antMatchers("/autoconfig**").hasAuthority("ROLE_ADMIN")
.antMatchers("/beans**").hasAuthority("ROLE_ADMIN")
.antMatchers("/configprops**").hasAuthority("ROLE_ADMIN")
.antMatchers("/dump**").hasAuthority("ROLE_ADMIN")
.antMatchers("/env**").hasAuthority("ROLE_ADMIN")
.antMatchers("/health**").hasAuthority("ROLE_ADMIN")
.antMatchers("/info**").hasAuthority("ROLE_ADMIN")
.antMatchers("/mappings**").hasAuthority("ROLE_ADMIN")
.antMatchers("/metrics**").hasAuthority("ROLE_ADMIN")
.antMatchers("/trace**").hasAuthority("ROLE_ADMIN")
.and()
.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(tokenAuthenticationProvider)
.antMatcher("/authenticate/**")
.exceptionHandling().authenticationEntryPoint(tokenEntryPoint)
.and()
.logout().logoutSuccessUrl(oeSettings.getUrl());
}
}
My problem is the configuration of the filter in my SpringConfig class. I want the filter to only come into effect when the request is for the /authenticate URL, I've added .antMatcher("/authenticate/**") to the filter configuration.
.and()
.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(tokenAuthenticationProvider)
.antMatcher("/authenticate/**")
.exceptionHandling().authenticationEntryPoint(tokenEntryPoint)
When I have this line in all other URLs are no longer secured, I can manually navigate to /home without authenticating, remove the line and /home is authenticated.
Should I be declaring a filter that is only applicable to a specific URL?
How can I implement this whilst maintaining the security of other URLs?
I've resolved my issue by performing a check on the authentication status in the filter before involking the authentication provider....
Config
.and()
.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(tokenAuthenticationProvider)
.exceptionHandling().authenticationEntryPoint(tokenEntryPoint)
Filter
#Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
logger.debug(this + "received authentication request from " + request.getRemoteHost() + " to " + request.getLocalName());
if (request instanceof HttpServletRequest) {
if (isAuthenticationRequired()) {
// extract token from header
OEWebToken token = extractToken(request);
// dump token into security context (for authentication-provider to pick up)
SecurityContextHolder.getContext().setAuthentication(token);
} else {
logger.debug("session already contained valid Authentication - not checking again");
}
}
chain.doFilter(request, response);
}
private boolean isAuthenticationRequired() {
// apparently filters have to check this themselves. So make sure they have a proper AuthenticatedAccount in their session.
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
if ((existingAuth == null) || !existingAuth.isAuthenticated()) {
return true;
}
if (!(existingAuth instanceof AuthenticatedAccount)) {
return true;
}
// current session already authenticated
return false;
}

Categories

Resources