In this article, we will be discussing about OAUTH2 implementation with spring boot security and JWT token and securing REST APIs.In my last article of Spring Boot Security OAUTH2 Example, we created a sample application for authentication and authorization using OAUTH2 with default token store but spring security OAUTH2 implementation also provides functionality to define custom token store.Here, we will be creating a sample spring security OAUTH2 application using JwtTokenStore.Using JwtTokenStore as token provider allows us to customize the token generated with TokenEnhancer to add additional claims. For a role based OAUTH2 implementation, you can visit this article.
Most of the configurations in this application are very similar to my previous article of spring security OAUTH2 implementation and hence we may avoid some common codes and configuration that we built in our last application. Let us start with a brief introduction of JWT and then we will dive into creating our authorization server, resource server and later we will discuss about adding custom claims in the token.If you don't want to use OAUTH2 and simply want to create an authentication process using JWT token, then you can visit my previous article of Using JWT with Spring Boot Security With Angular.
Json Web Token
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.a stateless authentication mechanism as the user state is never saved in server memory.A JWT token consists of 3 parts seperated with a dot(.) i.e. Header.payload.signature
Header has 2 parts type of token and hashing algorithm used.The JSON structure comprising these two keys are Base64Encoded.
{ "alg": "HS256", "typ": "JWT" }
Payload contains the claims.Primarily, there are three types of claims: reserved, public, and private claims. Reserved claims are predefined claims such as iss (issuer), exp (expiration time), sub (subject), aud (audience).In private claims, we can create some custom claims such as subject, role, and others.
{ "sub": "Alex123", "scopes": [ { "authority": "ROLE_ADMIN" } ], "iss": "http://devglan.com", "iat": 1508607322, "exp": 1508625322 }
Signature ensures that the token is not changed on the way.For example if you want to use the HMAC SHA256 algorithm, the signature will be created in the following way:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Following is a sample JWT token.Here is a full stack spring boot application with jwt authentication application to secure REST APIs using jwt token mechanism.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBbGV4MTIzIiwic2N.v9A80eU1VDo2Mm9UqN2FyEpyT79IUmhg
Project Structure
Maven Dependencies
Here, spring-security-jwt provides JwtTokenStore support.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> </dependency>
Authorization Server Config
I hope you are familiar of OAUTH2 architecture and authorization server. I have explained about it in my last article of OAUTH2.The following configuration is very similar to our last configuration of Spring Boot Security OAUTH2 Example apart from the JwtAccessTokenConverter and TokenStore.Here, JwtAccessTokenConverter is the helper that translates between JWT encoded token values and OAuth authentication information. We have added our custom signature to make the JWT token more robust.Apart from JwtTokenStore, spring security also provides InMemoryTokenStore and JdbcTokenStore.
For a 3rd party authorization server such as Google, you can visit this - Spring Boot OAuth2 with Google.ClientDetailsServiceConfigurer
: a configurer that defines the client details service. Client details can be initialized, or you can just refer to an existing store.
AuthorizationServerSecurityConfigurer
: defines the security constraints on the token endpoint.
AuthorizationServerEndpointsConfigurer
: defines the authorization and token endpoints and the token services.
ClientDetailsServiceConfigurer can be used to define an in-memory or JDBC implementation of the client details service.In this example, we are using an in-memory implementation.
AuthorizationServerConfig.java@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { static final String CLIEN_ID = "devglan-client"; static final String CLIENT_SECRET = "devglan-secret"; static final String GRANT_TYPE_PASSWORD = "password"; static final String AUTHORIZATION_CODE = "authorization_code"; static final String REFRESH_TOKEN = "refresh_token"; static final String IMPLICIT = "implicit"; static final String SCOPE_READ = "read"; static final String SCOPE_WRITE = "write"; static final String TRUST = "trust"; static final int ACCESS_TOKEN_VALIDITY_SECONDS = 1*60*60; static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6*60*60; @Autowired private AuthenticationManager authenticationManager; @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("as466gf"); return converter; } @Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Override public void configure(ClientDetailsServiceConfigurer configurer) throws Exception { configurer .inMemory() .withClient(CLIEN_ID) .secret(CLIENT_SECRET) .authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT ) .scopes(SCOPE_READ, SCOPE_WRITE, TRUST) .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS). refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore()) .authenticationManager(authenticationManager) .accessTokenConverter(accessTokenConverter()); } }
Resource Server Config
Resource in our context is the REST API which we have exposed for the crud operation.To access these resources, client must be authenticated.In real-time scenarios, whenever an user tries to access these resources, the user will be asked to provide his authenticity and once the user is authorized then he will be allowed to access these protected resources.
resourceId:
the id for the resource (optional, but recommended and will be validated by the auth server if present).
Since, we have resource-server and auhorization server implementation in the same project, we don't require to redefine our JwtAccessTokenConverter in the resource server config else we need to provide similar JwtAccessTokenConverter implementation in resource server too.
Here, we have configured that /users is a protected resource and it requires an ADMIN role for the access.
For an integration with Google along with a custom login, you can visit this article - Spring Security OAuth2 Google Registration
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private static final String RESOURCE_ID = "resource_id"; @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId(RESOURCE_ID).stateless(false); } @Override public void configure(HttpSecurity http) throws Exception { http. anonymous().disable() .authorizeRequests() .antMatchers("/users/**").access("hasRole('ADMIN')") .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler()); } }
REST APIs
Now let us expose some protected REST resource using spring controller.
@RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @RequestMapping(value="/user", method = RequestMethod.GET) public List listUser(){ return userService.findAll(); } @RequestMapping(value = "/user", method = RequestMethod.POST) public User create(@RequestBody User user){ return userService.save(user); } @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE) public String delete(@PathVariable(value = "id") Long id){ userService.delete(id); return "success"; } }
Following is the userservice implementation to validate user.
@Service(value = "userService") public class UserServiceImpl implements UserDetailsService, UserService { @Autowired private UserDao userDao; public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException { User user = userDao.findByUsername(userId); if(user == null){ throw new UsernameNotFoundException("Invalid username or password."); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority()); } private List getAuthority() { return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")); } public List findAll() { List list = new ArrayList<>(); userDao.findAll().iterator().forEachRemaining(list::add); return list; } }
Above userservice is configured in SecurityConfig.java as below. You can use this Online Bcrypt Calculator to genertae Bcrypt password.
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource(name = "userService") private UserDetailsService userDetailsService; @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(encoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .anonymous().disable() .authorizeRequests() .antMatchers("/api-docs/**").permitAll(); } @Bean public BCryptPasswordEncoder encoder(){ return new BCryptPasswordEncoder(); } @Bean public FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); bean.setOrder(0); return bean; } }
Create User Script
INSERT INTO User (id, username, password, salary, age) VALUES (1, 'Alex123', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 3456, 33); INSERT INTO User (id, username, password, salary, age) VALUES (2, 'Tom234', '$2a$04$PCIX2hYrve38M7eOcqAbCO9UqjYg7gfFNpKsinAxh99nms9e.8HwK', 7823, 23); INSERT INTO User (id, username, password, salary, age) VALUES (3, 'Adam', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 4234, 45);
Testing OAUTH2 JWT Application
First, run Application.java as a java program and switch to postman and make POST request at http://localhost:8080/oauth/token to generate tokens.In the headers we have selected basic auth and provided username and password as devglan-client
and devglan-secret
. This will result access_token, token_type, refresh_token, expiry etc.
Now, we can use the same token to access protected resources.
Spring Boot 2 OAUTH2
While running this application with above configurations in Spring Boot 2, you will find below error.
Following are the changes in pom.xml
to make this example work with spring boot 2.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
...
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.0.10.RELEASE</version>
</dependency>
For Spring Boot 2 you need to Bcrypt CLIENT_SECRET,so in AuthorizationServerConfig.java change line 17 into:
static final String CLIENT_SECRET = "$2a$04$e/c1/RfsWuThaWFCrcCuJeoyvwCV0URN/6Pn9ZFlrtIWaU/vj/BfG";
Conclusion
Here, we discussed about using JWT as a token provider for spring boot security OAUTH2 implementation. In the next article we will be discussing about consuming this token in an angular 5 application.You can download the source from here.