This article and the source codes attached should be used as reference only.Please thoroughly test your implementation before making changes to production environment
Checkout our NEW Video Channel you can like and subscribe too!

Introduction

There various way we can get OAuth enable in our application.We can use readymade solutions or build our own AuthServer.Here we are going to create our own AuthServer using Spring OAuth2.

SourceCode

AuthServer Configurer

AuthServerConfigurer

This is the core class for OAuth2 configuration,this needs to be annotated to @EnableAuthorizationServer.This class also sets a list of beans for configuring the AuthServer so needs to have @Configuration annotation too.

The AuthenticationManager bean gets initiated at WebSecurityConfigurerAdapter and it enables UsernamePasswordAuthentication for token endpoint which helps to enable password grant flow in Spring OAuth2 by default it is disabled.Note that our AuthenticationManager is shared between Web and Auth configurations and its configured with passwordEncoder,which means both user password and client password will be encrypted and stored in the db.Updating a client manually might result in bad client credentials unless it is encrypted ahead of insertion.It is advisable to add clients from dashboard UI which automatically takes care of encryption before insertion.

  @Bean
    @Override
    @Primary
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

userDetailsService holds all User related information for OAuth2Authentication object.By default Spring uses loadbyusername to populate the userdetails object.Note that loadbyusername returns type is a User object which holds specific information like username,password,authorities etc.We have extended this Object to return com.lnl.config.user.ExtendedUser additional information like firstname,lastname,userId etc.

  @Bean
    @Override
    @Primary
    public UserDetailsService userDetailsService() {
        return new UserDetailsServiceImpl();
    }
@Component("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

    private final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private UserRepository userRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(final String login) {
   return new ExtendedUser(userFromDatabase.getEmail().toLowerCase(),
                userFromDatabase.getPassword(),
                true,
                true,
                true,
                true,
                grantedAuthorities,
                userFromDatabase.getEmail().toLowerCase(),
                userFromDatabase.getUserId(),
                userFromDatabase.getFirstName(),
                userFromDatabase.getLastName());

As we are persisting the clientDetailsService we need to create a bean for JdbcClientDetailsService which will hold client details information.Note that the model behind this pre-defined in Spring and should be adhered to.We can always extend this class to accommodate more information but the base class model mapping should exists.

    //Instantiates the client details service object
    //note that we are storing the client details in a database so we need this
    //other option could have been using in memory and config properties
    @Bean
    @Primary
    public JdbcClientDetailsService clientDetailsService() {
        return new JdbcClientDetailsService(oauthDataSource());
    }

We need to persist the tokens generated by the server, so we use tokenStore bean.Note that Spring doesn treat token expiration as first class attribute while persisting the values.So we have extended that with com.lnl.config.auth.CustomJdbcTokenStore.Also note that based on this expiration we are setting up a purge task to clean obsolete tokens.

   //The tokens are backed up in db.This intiatlized a customjbcstoken store
    //Note the reason we use custom jdbctoken store is because token_expiry is not a first class attriburte in jdbcstore.
    //check com.lnl.config.auth.CustomJdbcTokenStore
    @Bean
    public TokenStore tokenStore() {
        return new CustomJdbcTokenStore(oauthDataSource());
    }

ClientDetailsServiceConfigurer registers the clients defined in Jdbc backed client details,Note that there are various ways we can add clients,it can be in memory,from properties or jdbc backed as in our case.

  //Note:Clients can be registered in no. of ways.This is just one implementation.
    //we attach clientDetailsService here which is has been initialized earlier
    @Override
    @Primary
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService());
    }

AuthorizationServerEndpointsConfigurer this the core method that exposes the OAuth2 endpoints.For all beans we write we need to make sure it gets propagated to this configurer endpoint to take effect.We also have a switch to generate a standard or a JWT token.JWT token has an added advantage in case of distributed system as it can self resolve a signed JWT token and take decision of allowing resources to server.

    //this configure is the core of this class.Does the final binding for all the required endpoints of OAuth2.
    //Note the jwt boolean,we can switch to A JWT based token from configs.
    //authenticationManager bean also used to create a filter that checks for UsernamePasswordAuthentication and inject into HttpSecurity configuration
    //look in to com.lnl.config.web.WebSecurityConfigurer

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .approvalStore(approvalStore())
                .authorizationCodeServices(authorizationCodeServices())
                .tokenStore(tokenStore())
                //.tokenEnhancer(tokenEnhancer())
                .authenticationManager(authenticationManager) //this is required for password grant flow to work
                .tokenServices(tokenServices())
                .userDetailsService(userDetailsService());
        if (jwt) endpoints.accessTokenConverter(accessTokenConverter()); //this gives a JWT token

    }

AuthorizationServerSecurityConfigurer lets us configure few key security features on the exposed OAuth2 endpoints.One of them is allowFormAuthenticationForClients which allows url based request to get token.Look at OAuth code functionality section for more details on this.This method also sets the password encoder.Note this essential otherwise we can get bad credentials while client is authenticated.We enable checkTokenAccess for authenticated tokens only,

    //allowFormAuthenticationForClients is requried to allow form based authorize requests.Currently lnl uses this flow
    //Also note we need to attach an passwordEncoder bean to it so that it can validate against the encrypted client credential secrets
    //checkTokenAccess is given isAuthenticated() which means only authenticated access is allowed to checktoken
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients()
                .passwordEncoder(passwordEncoder);
    }

Check token can be very handy to check any given token attributes like expiration,scope etc.Note that this endpoint is also important in case a resource server is isolated and uses a remotetoken validation methodology. For the record, we are not using that so far anywhere.

curl -X POST \
  'http://localhost:7070/oauth/check_token?token=c9d0b860-f18b-4d56-a109-5d1121792964' \
  -H 'Authorization: Basic dmlvbWU6dmlvbWUxMjM=' \
  -H 'Content-Type: application/json' \

AccessTokenConverter and CustomTokenEnhancer initializes the JWT token.It requires a keystore and p-p key pair.CustomTokenConverter does 2 important modifications here.It adds additional information to the JWT token as required (for example: phone,email,location of user).It also can modify the token return response and customize based on needs.For example we are currently interested in access_token only so(we are hiding scope,refresh_token information in the response token).Have look at com.lnl.config.auth.CustomTokenConverter.

 //This bean initialize the JWT token.It requires a keystore and p-p key pair
 //CustomTokenConverter does 2 important modifications here
 //It adds additonal information to the JWT token as required (for example: phone,email,location of user)
 //It also can modfiy the token return response and customize based on needs.For example we are currently interested in access_token only so
 // we are hiding scope,refresh_token information in the response token.Have look at com.lnl.config.auth.CustomTokenConverter
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
                keystore, keystorePassword.toCharArray());
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair(
                keyAlias, keyPassword.toCharArray());
        CustomTokenConverter tokenConverter = new CustomTokenConverter();
        tokenConverter.setKeyPair(keyPair);
        return tokenConverter;
    }
public class CustomTokenConverter extends JwtAccessTokenConverter {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,OAuth2Authentication authentication) {
        ExtendedUser extendedUser = (ExtendedUser)authentication.getPrincipal();
        final Map<String, Object> additionalInfo = new HashMap<>();
        additionalInfo.put("email123", extendedUser.getEmail().toLowerCase());
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        accessToken = super.enhance(accessToken, authentication);
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(new HashMap<>());
        authentication = super.extractAuthentication(additionalInfo);
        authentication.setDetails(additionalInfo);
        ((DefaultOAuth2AccessToken) accessToken).setScope(null);
        ((DefaultOAuth2AccessToken) accessToken).setExpiration(null);
        ((DefaultOAuth2AccessToken) accessToken).setRefreshToken(null);
        return accessToken;
    }

}

In case of JWT token the custom token converter also helps in adding any custom attribute to the token.For example firstname,location etc. Note that this information need to flow through the Extended User object of UserDetailService we previously extended.the converter takes OAuth2Authentication object which will eventually hold the extended user object via UserDetails service.So in short, to add any customization in JWT token follow as below

public class CustomTokenEnhancer  extends  JwtAccessTokenConverter  {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,OAuth2Authentication authentication) {
        //Enable this fields carefully.Make sure the lnlprovider is expecting this fields or marked as optional.
        //otherwise we might get parse exception at client side and client needs to be modified as required.
        ((DefaultOAuth2AccessToken) accessToken).setScope(null);
        ((DefaultOAuth2AccessToken) accessToken).setExpiration(null);
        ((DefaultOAuth2AccessToken) accessToken).setRefreshToken(null);
        return accessToken;
    }

}

Add attribute to ExtendedUser » Update loadbyusername in UserDetailsService » Populate CustomTokenConverter using OAuth2Authentication object.

TokenServices this takes care of wiring the previous to beans and injecting it to the configure endpoint.Note that it switches between a standard token and JWT token format after retrieving the token details from token store.Validity is a key attribute to be noted here.Validity have the highest precedence in database(it will override any attempt to update it via DefaultTokenServices So in order for application to derive token validity(based on request) set refresh_token_validity as null in db.

    //token service deals with the token store.
    //we modify the tokens before they been stored in the database with the additional customization we want on the tokens
    //we configure the token validity and refresh token
    //Note: Validity have the highest precedence in database(it will override any attempt to update it via DefaultTokenServices
    //So in order for application to derive token validity(based on request) set refresh_token_validity=0 in db
    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(false); //refresh token is not used as of now in lnl Oauth2 flow
        defaultTokenServices.setAccessTokenValiditySeconds(validity); //default validity is loaded from config
        if (jwt)
            defaultTokenServices.setTokenEnhancer(accessTokenConverter()); //set token enhancer to inject CustomTokenConverter for JWT
        else
            defaultTokenServices.setTokenEnhancer(tokenEnhancer()); //set token response using CustomTokenEnhancer

        return defaultTokenServices;
    }

ResourceServer

Resource server is part of AuthServer itself and not distributed as a separate entity.This class is annotated with @EnableResourceServer which forms the basis of making this class protected.Need to annotate with @EnableGlobalMethodSecurity to allow Pre/Post annotation on the protected methods.This takes care of associating protected methods with role based authorization

@Configuration
@EnableWebSecurity
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfigurer extends ResourceServerConfigurerAdapter {

In this implementation, we assume /api as our protected resources.HttpSecurty is configured with antmatcher as /api and it demands authorization by authorizeRequests().anyRequest().

   @Override
    public void configure(HttpSecurity http) throws Exception {
        http.httpBasic().disable();
        http.requestMatchers()
                .antMatchers(SECURED_PATTERN).and().authorizeRequests().anyRequest().authenticated();
    }

Spring defines its own AuthenticationException exception when an invalid token is passed requesting protected resource.Inorder to return a custom exception we have to extend RestAuthenticationEntryPoint and override commence and use handlerresolver to throw the custom exception.The controllerAdvice then returns the customized response entity object again the InvalidAccessTokenException thrown.The custom AuthenticationEntryPoint needs to be intiatited as a bean and injected into ResourceServerSecurityConfigurer.

 @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.authenticationEntryPoint(customAuthEntryPoint());
    }
  @Bean
    public AuthenticationEntryPoint customAuthEntryPoint(){
        return new RestAuthenticationEntryPoint();
    }

Filters

Currently there are 3 filters and they have different purpose.They are executed in order below.

HmacFilter > SimpleAuthenticationFilter>EventAuthenticationFailureHandler

HmacFilter:This is a custom filter that checks a specific x-auth header when calling getAccessTokenByEmail endpoint.Note that exception thrown from this class are before even controller is reached so @ControllerAdvice annotation will not be able to catch this.We have to use handler resolver and throw the custom exception so that it reaches the @ControllerAdvice.This methodology can be followed anywhere we would like to throw exceptions before the controller is reached.Also this should be executed at first so it has order of 1 @ Order(1)

@Order(1) //this filter should get executed at first
@Component
@ControllerAdvice
@Slf4j
//Hmac filter implementation class
public class HmacFilter extends OncePerRequestFilter {

SimpleAuthenticationFilter:This overrides UsernamePasswordAuthenticationFilter attemptAuthentication method and does a pre check before attempt to authentication is made.This feature is required during the user account lock scenario where if user is blocked the request does not gets entertained for further processing and attempt for authentication will be ignored.This bean needs to be created in WebSecurityConfigurer and injected in the configure method as addFilterBefore so that it gets triggered before Spring attempts for authentication at UsernamePasswordAuthenticationFilter.class.Incase of authentication failure EventAuthenticationFailureHandler needs to get triggered.See EventAuthenticationFailureHandler section below to see how it is done,

@Slf4j
//This class intercepts all login calls.Need to register this as a bean to HttpSecurity.
//this should intercept request prior to UsernamePasswordAuthenticationFilter gets executed
public class SimpleAuthenticationFilter extends UsernamePasswordAuthenticationFilter implements InitializingBean {
   //if the account is blocked we dont even allow to enter to authentication process
        log.info("userId and lastAceesTime " + userId + lastAceesTime);
        if (attemptService.isBlocked(userId, lastAceesTime) && lastAceesTime.isPresent()) {
            log.info("account locked!");
            accessAudit.setStatus(HttpDTO.account_locked.toString());
            this.loginError(response, HttpDTO.account_locked.name() + "_" + (int) attemptService.calculateLockDown(userId), errorRemoved);
            auditRepository.save(accessAudit);
            return null;
        } else {
            auditRepository.save(accessAudit);
            request.getSession().setAttribute(Config.ACS_TRACE_ID, accessAudit.getAcsAuditNo());
            log.info("before authentication failure handler" + errorRemoved);
            return super.attemptAuthentication(request, response);
        }
    //core configuration  to for  httpsecurity.
    //We have authenticationFilter which gets executed before the  UsernamePasswordAuthenticationFilter
    //we use this to keep track on the no failed of attempts and redirect back to login page
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(authenticationFilter(),
                UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/login", "/logout.do", "/webjars/**").permitAll()
                .antMatchers("/**").authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout.do"))
                .and()
                .userDetailsService(userDetailsService())
                .exceptionHandling().authenticationEntryPoint(new AuthenticationProcessingFilterEntryPoint("/login"));

        if (xHeader) http.headers().frameOptions().disable();
        if (csrf) http.csrf().disable();
    }

Failure Handler

@Slf4j
public class EventAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private AttemptCounterRepository attemptCounterRepository;

    @Autowired
    private AuditRepository auditRepository;

    @Autowired
    private AttemptService attemptService;

    @Override
    public void onAuthenticationFailure(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException exception)
            throws IOException, ServletException {
        Date date = new Date();
        String requestUrl = Optional.ofNullable(request.getParameter("request_url")).orElse("");
        String errorRemoved = StringUtils.sliceError(requestUrl);
        log.info("errorRemoved----" + errorRemoved);
        AccessAudit accessAudit = auditRepository.findOne(UUID.fromString(request.getSession().getAttribute(Config.ACS_TRACE_ID).toString()));
        Optional<AttemptCounter> attemptCounter = Optional.ofNullable(attemptCounterRepository.findOne(accessAudit.getUserId()));
        log.info("attemptCounter value----" + attemptCounter + accessAudit.getUserId());
        Integer counter;
        counter = attemptCounter.map(s -> s.getCounter() + 1).orElse(1);
        attemptCounterRepository.save(new AttemptCounter(accessAudit.getUserId(), counter, new Timestamp(date.getTime())));

        if (attemptCounter.isPresent()) {
            if (attemptService.isMaxAttemptReached(accessAudit.getUserId())) {
                this.setDefaultFailureUrl("/login?error=" + HttpDTO.account_locked.name()
                        + "_"
                        + (int) attemptService.calculateLockDown(accessAudit.getUserId())
                        + errorRemoved);
            } else {
                this.setDefaultFailureUrl("/login?error=" + HttpDTO.invalid_user_name_pwd.name() + errorRemoved);
            }
        } else {
            this.setDefaultFailureUrl("/login?error=" + HttpDTO.invalid_user_name_pwd.name() + errorRemoved);
        }
        super.onAuthenticationFailure(request, response, exception);
    }
}

This gets triggered when attempt to authentication fails.We need to create a bean as SimpleUrlAuthenticationFailureHandler and tie this up with SimpleAuthenticationFilter via setAuthenticationFailureHandler method. We need to remember that onAuthenticationFailure of parent SimpleUrlAuthenticationFailureHandler Spring core method redirects to setDefaultFailureUrl.We need to control this setDefaultFailureUrl dynamically to achieve various redirect errors like below

"/login?error=" + HttpDTO.invalid_user_name_pwd.name() - redirects with this when username/pwd mismatches 
"/login?error=" + HttpDTO.account_locked.name() - redirect when account is locked
"/login?error=true" - redirect for any generic error

Also note that while we set the setDefaultFailureUrl in EventAuthenticationFailureHandler to redirect , the actual redirect is taken care by .exceptionHandling().authenticationEntryPoint method injected in websecurityconfig.

Look at AuthenticationProcessingFilterEntryPoint section for more details.

WebSecurity Configurer

The WebSecurityConfigurerAdapter is core Spring security class that extends WebSecurityConfigurerAdapter.We annotate with @Configuration to intiatite beans and also with EnableJdbcHttpSession to storie Jdbc backed session object.Storing session object in jdbc gives an immediate advantage of decoupling application and session, which helps scaling without worrying about the state.

oauthDataSource: The datasource bean reading connection property from application config

   //the datasource,in our case this is postgres
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource oauthDataSource() {
        DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
        DataSource datasource = dataSourceBuilder.build();
        if(datasource instanceof org.apache.tomcat.jdbc.pool.DataSource){
            ((org.apache.tomcat.jdbc.pool.DataSource) datasource).setInitSQL("SET search_path = "+jdbcSchema);
        }
        return datasource;
    }

The override configure method takes care of setting the security strategy when request are made to the server.We add the authenticationFilter before UsernamePasswordAuthenticationFilter to preprocess the request and look for if the User is already blocked.Pages like "/login", "/logout.do", "/webjars/**" needs to be open to network so we use permitAll().We also add our custom authenticationEntryPoint which handles the redirect with url parameters via exceptionHandling.

X-frame origin have explicit deny.We need to set to *.lnl.For now it is disabled Also Csrf can be enabled/disabled from application configuration.

  @Value("${spring.csrf.disabled}")
    private Boolean csrf;

    @Value("${xframe-headers.disabled}")
    private Boolean xHeader;

SimpleAuthenticationFilter

This bean takes care of login attempts look at Filter section to know more

    //this bean keeps track of the login attempts that being made
    //also we have failureHandler to redirect failure to login page
    @Bean
    public SimpleAuthenticationFilter authenticationFilter() throws Exception {
        SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManagerBean());
        filter.setAuthenticationFailureHandler(failureHandler());
        return filter;
    }

Authentication Provider

Springs default authentication provider is DaoAuthenticationProvider which loads user details via userDetailsService.Note that we have custom implementation of user detail service which loads user details by email so we need to make the DaoAuthenticationProvider aware of that so that it doesn’t bind it with the default one which loads by name.

provider.setUserDetailsService(userDetailsService()); - custom implementation of userDetailsService

    //authentication Provider
    @Bean
    public AuthenticationProvider authProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService());
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

User Details Service

UserDetailsService is a custom implemented bean.This gets used wherever Spring tries to load a user details,in case of WebSecurityConfigurerAdapter or AuthServerConfigurer.

   @Bean
    @Override
    @Primary
    public UserDetailsService userDetailsService() {
        return new UserDetailsServiceImpl();
    }

Filter EntryPoint

When login attempt is made via a form post in html the parameters in url are lost.For example in a custom scenario, a url parameter hideClose is send which decides whether the login page is opened in a frame or in a page.So in case there is authentication failure the redirect url should carry forward the custom parameter to keep the UI behaviour as expected.This done by following strategy

1.First capture the custom url parameters from jquery and populate a hidden attribute

Req:https://local.lnl.com/oauth/authorize?response_type=code&hideClose=1&client_id=lnl&redirect_uri=http://localhost:9000/app/authenticate/Lnl

  $('#request_url').val(location.search.slice(1));
  <input type="hidden" name="request_url" id="request_url"/>

2.When the form gets submitted this gets passed along with the form post object. 3.Then on authentication failure AuthenticationProcessingFilterEntryPoint which is responsible for throwing AuthenticationException will be captured and update the redirectStrategy to include the custom parameter.

public class AuthenticationProcessingFilterEntryPoint extends LoginUrlAuthenticationEntryPoint {
    private final org.springframework.security.web.RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();


    public AuthenticationProcessingFilterEntryPoint(String loginFormUrl) {
        super(loginFormUrl);
    }

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        String redirectUrl = getLoginFormUrl() + Optional.ofNullable(request.getQueryString()).map(s -> "?" + s).orElse("");
        this.redirectStrategy.sendRedirect(request, response, redirectUrl);
    }
}
 .exceptionHandling().authenticationEntryPoint(new AuthenticationProcessingFilterEntryPoint("/login"));

We will discuss more configuration in part 2 section..

    Content