Implement Windows AD Authentication, Local DB Authorization with JWT Response Token using Spring Security

Himanshu Pratap
4 min readOct 6, 2021

--

In the previous article here, i have implemented custom authentication such that

  • Check if the user exist in local database user table and if the user exist, then authenticate it to external windows AD server.
  • Upon successful call to login API, a session was created on server and session id was returned in response header.
  • Subsequent call to other APIs were done with session id token.

One of the issue faced in above implementation was that frontend ajax tools like axios, fetch and super agent were not able to extract session id token from response header. Refer this.

So, instead of session based authentication , I have tried to implement stateless authentication using jwt token.

Steps are as follows:

  1. Add following dependencies to pom.xml file.
  <dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>

2. Edit application.properties file. Add jwt seceret key and database connectivity details

jwt.secret=mysecretkey12345spring.jpa.show-sql = true
spring.jpa.generate-ddl=false
spring.sql.init.mode=never
#DATABASE CONFIGURATION spring.datasource.url=jdbc:postgresql://localhost/pgdb
spring.datasource.username=pguser
spring.datasource.password=pguser
spring.datasource.driver-class-name=org.postgresql.Driver

3. Create User’s entity class , repo interface and service class

@Entity
public class Myuser {
@Id
private String username;
private String authority;
// constructor, getter & setter
}
@Repository
public interface MyuserRepo extends JpaRepository<Myuser, String> {}
@Service
@Transactional
public class MyuserService {

private final MyuserRepo myuserRepo;
public MyuserService(MyuserRepo myuserRepo) {
this.myuserRepo = myuserRepo;
}
public boolean findOne(String username) {
Optional<Myuser> myuser = myuserRepo.findById(username);
return myuser.isPresent();
}

public Myuser loadUserByUsername(String username) throws UsernameNotFoundException {
return myuserRepo.findById(username).get();
}
public List<Myuser> findAll() {
return myuserRepo.findAll();
}
}

4. Create following jwt related files

a. Create class for Authentication Request and Authentication Response

public class AuthenticationRequest implements Serializable {
private String username;
private String password;
//constructors and getter setter methods
}public class AuthenticationResponse implements Serializable {
private final String jwt;
//constructors and getter setter methods
}

b. Create jwt util class

@Service
public class JwtUtil {
@Value("${jwt.secret}") // read key from application.properties file
private String SECRET_KEY;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(Myuser userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
}
public Boolean validateToken(String token, Myuser userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}

}

c. Create jwt filter class

@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private MyuserService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");String username = null;
String jwt = null;
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null & SecurityContextHolder.getContext().getAuthentication() == null) {
Myuser userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {

// get user authority from local db
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_".concat(userDetails.getAuthority())));

UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, grantedAuthorities);

usernamePasswordAuthenticationToken
.setDetails( new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(request, response);
}
}

d. Create JwtAuthenticationEntryPoint class

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable
{
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException
{
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}

5. Create following config classes

a. CustomAuthenticationProvider class that checks for existence of user in local db, if present, check for authentication.

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

private final MyuserService myuserService;

public CustomAuthenticationProvider(MyuserService myuserService) {
super();
this.myuserService = myuserService;
}
@Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider(){

ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(
"example.examplegroup.co.in","ldap://192.168.1.25");

// to parse AD failed credentails error message due to account - expiry,lock, credentialis - expiry,lock
activeDirectoryLdapAuthenticationProvider.setConvertSubErrorCodesToExceptions(true);
return activeDirectoryLdapAuthenticationProvider;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

String username = authentication.getName();
boolean ifPresent = myuserService.findOne(username);

if(ifPresent) {
return activeDirectoryLdapAuthenticationProvider().authenticate(authentication);
}
else throw new UsernameNotFoundException("User not found.");
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

b. Web security Configuration class

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private JwtRequestFilter jwtRequestFilter;
private CustomAuthenticationProvider customAuthenticationProvider;
public WebSecurityConfig(JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtRequestFilter jwtRequestFilter,
CustomAuthenticationProvider customAuthenticationProvider) {
super();
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtRequestFilter = jwtRequestFilter;
this.customAuthenticationProvider = customAuthenticationProvider;
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);
}


@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and().csrf().disable()
.headers().frameOptions().deny()
.and()
.authorizeRequests()
.antMatchers("/api/authenticate").permitAll()
.anyRequest().authenticated()
.and() .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

http.addFilterBefore(jwtRequestFilter,UsernamePasswordAuthenticationFilter.class);

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

}

c. Create cors configuration class

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final long MAX_AGE_SECS = 3600;@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://myserver2:3000", "http://myserver3:3000")
.allowedMethods("GET", "POST")
.maxAge(MAX_AGE_SECS)
.allowCredentials(true);
}
}

6. Controller class for following REST APIs

  • login api that performs authentication and responds with jwt token if successful.
  • API thats check returns “hello world” if the associated jwt token in payload is valid.
@RestController
@RequestMapping("/api")
public class AuthenticationResource {

private final AuthenticationManager authenticationManager;
private final JwtUtil jwtTokenUtil;
private final MyuserService myuserService;

public AuthenticationResource(AuthenticationManager authenticationManager, JwtUtil jwtTokenUtil,
MyuserService myuserService) {
super();
this.authenticationManager = authenticationManager;
this.jwtTokenUtil = jwtTokenUtil;
this.myuserService = myuserService;
}
@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
);
}
catch (BadCredentialsException e) {
throw new Exception("Incorrect username or password", e);
}
final Myuser userDetails = myuserService.loadUserByUsername(authenticationRequest.getUsername());final String jwt = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(jwt));
}
@GetMapping("/hello")
public String sayHello(){
return "Hello World";
}

Thats it on server side.

On React JS fronted side,

  • Make authentication call as follows
const onLogin= () => {onst AUTH_TOKEN_KEY = 'authenticationToken';
const loginUrl = "http://myserver1:8080/api/authenticate";
axios.post(loginUrl, { username, password })
.then(res => {
const bearerToken = res.data.jwt;
if (bearerToken) {
localStorage.setItem(AUTH_TOKEN_KEY, bearerToken);
setBearerId(bearerToken);
setIsAuthenticated(true);
}
})
.catch(err => console.log(err.message));
}
  • Make endpoint api call with bearer id as follows
const endpointUrl = 'http://myserver1:8080/api/hello';const authHeader = "Bearer "+ localStorage.getItem('authenticationToken');axios
.get(endpointUrl,{ headers: { 'Authorization': authHeader } } )
.then(res => console.log(res.data))
.catch(err => console.log(err.message));
}

Resources:

--

--