Security requirements are different from application to application. JSON Web Tokens or JWT (pronounced like the word “jot”) are a type of token that is a JSON data structure, the claims, that contain information about the user.
In one of the earlier blog post series, we have seen how to implement Spring Security and OAuth2 to protect the REST API endpoints. In this post, we will look into what it takes to implement Spring Security OAuth2 that makes use of JSON Web Tokens.
We will learn the following topics in this post:
- What JSON Web Tokens (JWT) are
- Why we should use JSON Web Tokens
- How to implement JSON Web Tokens
- Verifying JSON Web Tokens
- Best Practices for JSON Web Tokens
What JSON Web Tokens (JWT) are:
JSON Web Token (JWT) is an open standard that defines a compact and self-contained mechanism for securely transmitting information between parties as a JSON object in a way that can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA.
Since it’s compact in size, JWTs can be sent through the HTTP Authorization headers or URI query parameters.
JWT is just a string that consists of three parts separated by dots (.), which are:
- Header
- Payload
- Signature
JWT typically looks like the following format:
header.payload.signature
Header
The header typically consists of two parts: the type of the token, which is JWT, and the hashing algorithm being used, such as HMAC SHA256 or RSA. Basically, the header contains information about how the JWT signature should be computed.
An example of a Header could be:
{
"alg": "HS256",
"typ": "JWT"
}
The Header JSON is then Base64Url encoded to form the first part of the JWT.
Payload
The second part of the token is the payload, which contains the claims. Claims are statements about the user and additional metadata. There are several different standard claims for the JWT payload, such as:
- iss: The issuer of the token
- exp: The expiration timestamp
- iat: The time the JWT was issued
- jti: a unique identifier for the JWT that is used to prevent the JWT from being reused or replayed
Notice that the claim names are only three characters long since JWT is meant to be compact.
An example of a payload could be:
{
"exp": 1505659116,
"jti": "1fa8fdb6-312c-4a65-a696-e4a75be8fa4a",
"client_id": "client1",
"authorities": [
"ROLE_USER",
"ROLE_ADMIN"],
"scope": [
"read",
"write"],
"user_name": "johndoe"
}
The payload is then Base64Url encoded to form the second part of the JSON Web Token.
Signature
The third and final part of the JWT is a signature generated based on the encoded header, encoded payload, secret, and the algorithm specified in the header.
The signature is used to verify that the sender of the JWT is who they claim to be and ensures that the message wasn’t changed along the way.
Why we should use JSON Web Tokens:
JSON Web Tokens make it easy to send read-only signed “claims” between services. Claims are any bits of data that you want someone else to be able to read and verify, but not alter. The JWT contains all information necessary to validate the user on the token itself, meaning that it doesn’t depend on a back-end storage to conclude the authentication process.
Using the same secret you used to produce the JWT, calculate your own version of the signature and compare. This calculation is much more efficient than looking up an access token in a database to determine who it belongs to and whether it is valid.
Let’s first understand how the basic Oauth2 scenario works. Only then will it be much easier to understand the advantage of JWT. The following picture is taken from the post: “How to Secure REST API using Spring Security and OAuth2”
In the above picture, you can see what the Oauth2 basic scenario looks like:
- The client requests the protected resource from the resource server by presenting the access token.
- The resource server asks the authorization server for the user identity associated with the token and whether the token is valid or not. The authorization server makes a call to the database server to retrieve the user info and token details.
- The resource server responds to the request if the token is valid.
The Access token in a basic Oauth2 scenario is just a string – it has no useful information about the user. In other words, we are not able to get the user identity from the OAuth2 access token. In a basic Oauth2 scenario, access tokens are stored in the database. So for each client request, the resource server makes a round trip to an authorization server and a database lookup to verify the token.
Large-scale deployments may have more than one resource server that are separate, but they all share the same authorization server. So using basic Oauth2 access token may not be a good idea in large scale deployments.
Another scenario where using a basic Oauth2 access token is not an efficient approach is with Microservices applications. In a Microservices world, each service runs an independent application. If we want to reduce the number of calls from each Resource server service to the Authorization server, then a better solution is to use JWT.
How to implement JSON Web Tokens:
Since we are going to be using JWT on top of Spring Security OAuth2, we will only address the logic needed to implement the JWT in this section. The complete source code is available on GitHub. To learn more about Spring Security and OAuth2, refer to this series.
Add the following dependency to your pom.xml file.
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.5.RELEASE</version>
</dependency>
Update the SecurityConfig class under the com.stl.crm.security package as follows.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// @Autowired
// private DataSource dataSource;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private CrmUserDetailsService crmUserDetailsService;
@Override
@Order(Ordered.HIGHEST_PRECEDENCE)
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/about").permitAll()
.antMatchers("/signup").permitAll()
.antMatchers("/oauth/token").permitAll()
//.antMatchers("/api/**").authenticated()
.anyRequest().authenticated()
.and()
.httpBasic()
.realmName("CRM_REALM");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(crmUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//-- use the JdbcTokenStore to store tokens
/*
@Bean
public JdbcTokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
*/
//-- use the JwtTokenStore instead of JdbcTokenStore
@Bean
public TokenStore tokenStore() {
//return new JdbcTokenStore(dataSource);
return new JwtTokenStore(jwtTokenEnhancer());
}
@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
/*
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "mySecretKey".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
*/
//-- for the simple demo purpose, used the secret for signing.
//-- for production, it is recommended to use public/private key pair
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("Demo-Key-1");
return converter;
}
@Bean
@Autowired
public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore){
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(tokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
return handler;
}
@Bean
@Autowired
public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore);
return store;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
The JdbcTokenStore from Spring Security OAuth2.0 stores token data in a database. The JSON Web Token (JWT) version of the store (JwtTokenStore) encodes all the data about the grant into the token itself. The JwtTokenStore doesn’t persist any data.
In the preceding code, we created the JwtTokenStore and JwtAccessTokenConverter beans. Next, we need to wire them up to the AuthorizationServerEndpointsConfigurer in the AuthorizationServerConfig class. Update the AuthorizationServerConfig class as follows.
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private static String REALM="CRM_REALM";
@Autowired
private DataSource dataSource;
@Autowired
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtTokenEnhancer;
@Autowired
private UserApprovalHandler userApprovalHandler;
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
private CrmUserDetailsService crmUserDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
/*
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager)
.userDetailsService(crmUserDetailsService);
}
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore).tokenEnhancer(jwtTokenEnhancer).userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager)
.userDetailsService(crmUserDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.realm(REALM);
}
}
We need to use the following code to obtain the information about the current user using the JWT token.
/**
* Obtaining information about the current user
*/
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("logged in user name:: " + authentication.getName());
Database Table:
We only need the ‘oauth_client_details’ table in the database to store the Client’s registration details. The DDL (Data Definition Langauge) and DML (Data Manipulation Langauge) SQL commands are given below.
drop table if exists oauth_client_details;
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
-- insert client details
INSERT INTO oauth_client_details
(client_id, client_secret, scope, authorized_grant_types,
authorities, access_token_validity, refresh_token_validity)
VALUES
('crmClient1', 'crmSuperSecret', 'read,write,trust', 'password,refresh_token',
'ROLE_CLIENT,ROLE_TRUSTED_CLIENT', 900, 2592000);
That’s all the changes needed to implement JWT on top of Spring Security OAuth2.
Testing:
Build and Deploy the application. Then head over to our favorite tool, Postman and issue a token request as follows:
http://localhost:8080/crm-oauth2-jwt/oauth/token
You should get the response similar to the following screenshot.
The JWT token is little longer than basic OAuth2 access tokens, still, they’re relatively compact. Copy the above token details to any text editor for future reference.
Use the JWT access_token to get the customers as follows.
Continue to test all other endpoints (add, get, update, and delete) using JWT access_token. If you need any help using the Postman app to test these endpoints, refer to this blog.
POST: http://localhost:8080/crm-oauth2-jwt/api/customers
GET: http://localhost:8080/crm-oauth2-jwt/api/customers/<id>
PUT: http://localhost:8080/crm-oauth2-jwt/api/customers/<id>
DELETE: http://localhost:8080/crm-oauth2-jwt/api/customers/<id>
Also, try using the Refresh Token to get a new JWT access_token.
Verifying JSON Web Tokens:
We learned from the beginning of this post that a JWT token consists of three parts (Header, Payload & Signature) separated by dots. Copy the access_token from the above response to any text editor and verify that the preceding is true.
We also learned that JWT token contains information about the user. To verify that, we need to decode the JWT token to see its content since it is Base64Url encoded. Let’s do that.
Decoding JWT token:
From your browser, go to the following URL and paste the access_token that was earlier copied into your text editor.
https://py-jwt-decoder.appspot.com/
Click the ‘Decode JWT’ button and you should see the content similar to the following:
Based on the above decoded JWT, we can clearly see that JWT is not just plain text. It contains all the necessary information for the logged in user.
The best Practices for JSON Web Tokens:
- Consider issuing short-lived JWTs and require them to be re-issued regularly using a Refresh Token. JWT tokens cannot be invalidated. By design, they will be valid until they expire.
- Secure the secret signing key used for calculating and verifying the signature. If you are using JWT for Single Sign-On (SSO) or Microservices applications environment, use an asymmetric key (public/private key pair using RSA) to sign the token. The Authorization server should use the private key and all other Resource servers should use the public key.
- Use TLS/SSL in conjunction with JWT to prevent man-in-the-middle attacks.
Conclusion
JWTs are a convenient way of representing authentication and authorization claims for your application. In this post, we learned the fundamentals of JSON Web Tokens (JWT), the advantage of using JWTs, implementation details, verifying JWT, and the best Practices.
The source code used for this post is available on GitHub.
Share this post: on Twitter on Facebook on Google+