How to use Nimbus JOSE along with JWT in Spring framework ?

JSON Web Token (JWT) is an open standard that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.
JWT is easy to identify and is separated by dots .
Example Format : iiiiiiiiiiiiii.jjjjjjjjjjjj.kkkkkkk
Header = iiiiiiiiiiii
Payload = jjjjjjjjjjj
Signature = kkkkkkk
Brief use of JSON Web Tokens : Authorization and Information Exchange.
Java Library: This library implements the Javascript Object Signing and Encryption (JOSE) and JSON Web Token (JWT) specs.
Supports all standard signature (JWS) and encryption (JWE) algorithms. Supports Bitcoin and Ethereum related encryption if your business logic needs to implement that.
So with this brief introduction, now let’s go into the coding part.
I have shared three screen shots (1st one is the POST call to generate JWT and second one is for GET call to get the business logic) and have pasted the Java Code as well (3rd screenshot is this project structure). You can copy and use it or modify it however it helps you the most.
Note: Not using @Autowired here but using constructor injection which is the recommended way.

Find below the Java codes in this Nimbus Jose Spring Boot Application :

package com.example.demo.auth.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CustomJoseJwtEntryPoint implements AuthenticationEntryPoint {
//Make sure to implement Serializable Interface if sending data over Network. Not implementing here as this is local demo
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, " 401: UNAUTHORIZED REQUEST ");
}
}
package com.example.demo.auth.security;
import com.example.demo.user.CustomSpringUserDetailsImpl;
import com.example.demo.util.JoseJwtClaimGeneratorUtil;
import com.nimbusds.jose.JOSEException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
@Component
//Using OncePerRequestFilter to guarantee a single execution per request dispatch, on any servlet container.
public class RequestResponseJwtChainFilter extends OncePerRequestFilter {
private static final String AUTHORIZATION = "Authorization";
private static final String BEARER_TOKEN = "Bearer ";
private static final String TOKEN = "token";
private static final String USERNAME = "username";
private final CustomSpringUserDetailsImpl customSpringUserDetailsImpl;
private final JoseJwtClaimGeneratorUtil joseJwtClaimGeneratorUtil;
public RequestResponseJwtChainFilter(CustomSpringUserDetailsImpl customSpringUserDetailsImpl, JoseJwtClaimGeneratorUtil joseJwtClaimGeneratorUtil) {
this.customSpringUserDetailsImpl = customSpringUserDetailsImpl;
this.joseJwtClaimGeneratorUtil = joseJwtClaimGeneratorUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
try {
final String requestTokenHeader = request.getHeader(AUTHORIZATION);
//This will be used once we have token.
String jwtToken = getTokenWithBearer(requestTokenHeader).get(TOKEN);
String username = getTokenWithBearer(requestTokenHeader).get(USERNAME);
// Once we get the token then only validate security context for currently authenticated principal, or an authentication request token.
if (SecurityContextHolder.getContext().getAuthentication() == null && !StringUtils.isEmpty(jwtToken)) {
UserDetails employeeDetails = customSpringUserDetailsImpl.loadUserByUsername(username);
//Validate token and employee details.
if (joseJwtClaimGeneratorUtil.validateTokenTimestamp(jwtToken, employeeDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
employeeDetails, null, employeeDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//If no exception till here. User is validated add the authenticated principle.
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
} catch (ParseException e) {
throw new ServletException("ServletException caused by : Payload of JWS object not being a valid JSON object", e.getCause());
} catch (JOSEException e) {
throw new ServletException("JOSE exception for token: ", e.getCause());
}
chain.doFilter(request, response);
}
private Map getTokenWithBearer(String requestTokenHeader) throws ServletException, JOSEException {
Map tokenWithUsername = new HashMap<>();
if (requestTokenHeader != null && requestTokenHeader.startsWith(BEARER_TOKEN)) {
try {
tokenWithUsername.put(TOKEN, requestTokenHeader.substring(BEARER_TOKEN.length()));
tokenWithUsername.put(USERNAME, joseJwtClaimGeneratorUtil.getUsernameFromToken(requestTokenHeader.substring(BEARER_TOKEN.length())));
} catch (IllegalArgumentException | ParseException e) {
throw new ServletException(e.getMessage());
}
}
return tokenWithUsername;
}
}
package com.example.demo.auth.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomJoseJwtEntryPoint customJoseJwtEntryPoint;
private final UserDetailsService jwtUserDetailsService;
private final RequestResponseJwtChainFilter requestResponseJwtChainFilter;
public WebSecurityConfig(CustomJoseJwtEntryPoint customJoseJwtEntryPoint, @Qualifier("customSpringUserDetailsImpl") UserDetailsService jwtUserDetailsService, RequestResponseJwtChainFilter requestResponseJwtChainFilter) {
this.customJoseJwtEntryPoint = customJoseJwtEntryPoint;
this.jwtUserDetailsService = jwtUserDetailsService;
this.requestResponseJwtChainFilter = requestResponseJwtChainFilter;
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// Can use other Encoders but using BCryptPasswordEncoder here
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable()
.authorizeRequests().antMatchers("/authenticate/token").permitAll().
anyRequest().authenticated().and().
exceptionHandling().authenticationEntryPoint(customJoseJwtEntryPoint).and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.addFilterBefore(requestResponseJwtChainFilter, UsernamePasswordAuthenticationFilter.class);
}
}
package com.example.demo.controller;
import com.example.demo.nimbus.jose.NimbusJoseJwtRequest;
import com.example.demo.nimbus.jose.NimbusJoseJwtResponse;
import com.example.demo.user.CustomSpringUserDetailsImpl;
import com.example.demo.util.JoseJwtClaimGeneratorUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
@RestController
public class InitialJwtAuthenticationController {
private final AuthenticationManager authenticationManager;
private final JoseJwtClaimGeneratorUtil joseJwtClaimGeneratorUtil;
private final CustomSpringUserDetailsImpl userDetailsService;
public InitialJwtAuthenticationController(AuthenticationManager authenticationManager, JoseJwtClaimGeneratorUtil joseJwtClaimGeneratorUtil, CustomSpringUserDetailsImpl userDetailsService) {
this.authenticationManager = authenticationManager;
this.joseJwtClaimGeneratorUtil = joseJwtClaimGeneratorUtil;
this.userDetailsService = userDetailsService;
}
@RequestMapping(value = "/authenticate/token", method = RequestMethod.POST)
public ResponseEntity createAuthenticationToken(@RequestBody NimbusJoseJwtRequest authenticationRequest) throws Exception {
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
final UserDetails employeeDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String token = joseJwtClaimGeneratorUtil.generateJwtToken(employeeDetails);
return ResponseEntity.ok(new NimbusJoseJwtResponse(token, LocalDateTime.now().plusMinutes(1).toString()));
}
private void authenticate(String username, String password) throws Exception {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
}
}
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class NimbusJoseJwtController {
@RequestMapping("/company/employees-information")
Map getResponse() {
return returnEmployeeInfo();
}
private Map returnEmployeeInfo() {
Map dummyEmployeesInfo = new HashMap<>();
dummyEmployeesInfo.put("Mira", "Manager");
dummyEmployeesInfo.put("Sam", "Senior Dev");
dummyEmployeesInfo.put("John", "Systems Engineer");
dummyEmployeesInfo.put("Abraham", "HR Administrator");
return dummyEmployeesInfo;
}
}
package com.example.demo.nimbus.jose;
public class NimbusJoseJwtRequest {
private String username;
private String password;
public NimbusJoseJwtRequest() {
}
public NimbusJoseJwtRequest(String username, String password) {
this.setUsername(username);
this.setPassword(password);
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
}
package com.example.demo.nimbus.jose;
public class NimbusJoseJwtResponse {
private final String yourToken;
private final String expires;
public NimbusJoseJwtResponse(String yourToken, String expires) {
this.yourToken = yourToken;
this.expires = expires;
}
public String getYourToken() {
return yourToken;
}
public String getExpires() {
return "Expires after: " + expires;
}
}
package com.example.demo.user;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class CustomSpringUserDetailsImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails employeeDetails;
for (User u : getEmployeesList()) {
if (u.getUsername().equalsIgnoreCase(username)) {
if (u.isAccountNonExpired() && u.isAccountNonLocked()) {
if (u.getUsername().equalsIgnoreCase(username)) {
employeeDetails = new User(u.getUsername(), u.getPassword(),
new ArrayList<>());
return employeeDetails;
} else {
throw new UsernameNotFoundException("Employee not found with username: " + u);
}
}
}
}
return null;
}
//Some dummy company with dummy employees stored at some dummy database.
List getEmployeesList() {
List userList = new ArrayList<>();
User user1 = new User("Mira", "$2a$10$e59rGaFvpijWXLh03j0aZOzBYQNrIRIjlB8sGwLvBL35fecblsW1m", true, true,
true, true, new ArrayList<>());
User user2 = new User("Sam", "$2a$10$e59rGaFvpijWXLh03j0aZOzBYQNrIRIjlB8sGwLvBL35fecblsW1m", false, false,
false, false, new ArrayList<>());
User user3 = new User("John", "$2a$10$e59rGaFvpijWXLh03j0aZOzBYQNrIRIjlB8sGwLvBL35fecblsW1m", new ArrayList<>());
userList.add(user1);
userList.add(user2);
userList.add(user3);
return userList;
}
}
package com.example.demo.util;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
@Component
public class JoseJwtClaimGeneratorUtil {
@Value("${jwt.company-secret-key}")
private String companySecretKey;
//retrieve employeeName from jwt token
public String getUsernameFromToken(String token) throws ParseException, JOSEException {
return getTokenKeyValueForClaims(token).get("sub").toString();
}
private Map getTokenKeyValueForClaims(String token) throws ParseException, JOSEException {
Map claims;
try {
JWSVerifier verifier = new MACVerifier(companySecretKey);
SignedJWT signedJWT = SignedJWT.parse(token);
signedJWT.verify(verifier);
claims = signedJWT.getJWTClaimsSet().getClaims();
} catch (ParseException e) {
throw new ParseException("Payload of JWS object is not a valid JSON object", e.getErrorOffset());
} catch (JOSEException e) {
throw new JOSEException("JOSE exception for token: ", e.getCause());
}
return claims;
}
public String generateJwtToken(UserDetails userDetails) throws Exception {
try {
JWSSigner signer = new MACSigner(companySecretKey);
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject(userDetails.getUsername())
.issuer("your issuer")// On our case it is IBM thingy
.expirationTime(new Date(System.currentTimeMillis() + 60 * 1000)) //Expire in a minute
.audience("demo")
.build();
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
signedJWT.sign(signer);
return signedJWT.serialize();
} catch (Exception e) {
throw new Exception("JWT Signing has failed: ", e.getCause());
}
}
public Boolean validateTokenTimestamp(String token, UserDetails userDetails) throws ParseException, JOSEException {
SimpleDateFormat formatter = new SimpleDateFormat("E MMM dd HH:mm:ss Z yyyy", Locale.ENGLISH);
final Date expiration = formatter.parse(getTokenKeyValueForClaims(token).get("exp").toString());
boolean hasExpired = expiration.before(new Date());
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !hasExpired);
}
}
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class NimbusJoseJwtApplication {
public static void main(String[] args) {
SpringApplication.run(NimbusJoseJwtApplication.class, args);
}
}

If you need to decode, verify and generate JWT. I found this website very helpful. https://jwt.io/
Enjoy coding and learning


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *