> For the complete documentation index, see [llms.txt](https://wisdom.gitbook.io/gyan/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wisdom.gitbook.io/gyan/security/passwordless-login-with-spring-security-6/spring-security-onetimetoken/one-time-token-with-jwt.md).

# One Time Token With Jwt

We often use JWT for the security in spring application. Since by default, one-time token only work with HTML pages.&#x20;

So here is the example of how we can use it with the rest APIs and the JWT token.

* We will be securing our application with JWT token, for that, we will use Ouath2ResourceServer.
* We will be use basic auth for login and issue jwt.
* We will create a separate endpoint for generating token and validating them, and after successful validation a jwt token is issues successfully.

### Classes Needed

1. [AppUser ](/gyan/security/passwordless-login-with-spring-security-6/spring-security-onetimetoken/one-time-token-with-custom-configuration.md#appuser-entity)& [OneTimeToken](/gyan/security/passwordless-login-with-spring-security-6/spring-security-onetimetoken/one-time-token-with-custom-configuration.md#onetimetoken-entity)
2. [CustomUserDetailsService](/gyan/security/passwordless-login-with-spring-security-6/spring-security-onetimetoken/one-time-token-with-custom-configuration.md#userdetailsservice)
3. JDBCTokenGeneratorService&#x20;
4. JwtTokenService
5. OneTimeTokenSuccessHandler
6. SecurityConfiguration & RestController

### JWT Token Generation

* For jwt token generation **`spring-boot-starter-oauth2-resource-server`** is being used.
* It internally used nimbus jose jwt jar.
* JwtEncoder and JwtDecoder beans to jwt security along with public and private keys.

### RSA Keys generation

* To create a public and private key, we will use **OpenSSL.**
* It will generate public and private pem file that we will store at **/src/main/resources/cers .**

```bash
# create rsa key pair
openssl genrsa -out keypair.pem 2048

# extract public key
openssl rsa -in keypair.pem -pubout -out public.pem

# create private key in PKCS#8 format
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out private.pem
```

* After creation of keys, delete keypair.pem file.
* Define path in properties file and create record to hold them.

```properties
rsa.public-key=classpath:certs/public.pem
rsa.private-key=classpath:certs/private.pem
```

```java
@Profile("rest")
@ConfigurationProperties(prefix = "rsa") 
public record RsaKeyProperties(RSAPublicKey publicKey, RSAPrivateKey privateKey) {

}
```

To bind the properties, we need to tell spring by adding this in any configuration or main class.

```
@EnableConfigurationProperties(RsaKeyProperties.class)
```

### JWT encoder and decoder bean

In security configuration class, define jwt encode and decoder bean

```java
private final RsaKeyProperties rsaKeyProperties;

// custom jwt decoder with rsa public key to decode incoming jwt token
@Bean
public JwtDecoder jwtDecoder(){
    return NimbusJwtDecoder.withPublicKey(rsaKeyProperties.publicKey()).build();
}

// custom jwt encoder to encode the tokens generated by ouath2, encodes it with both same public and private keys that we will be using in decode
@Bean
JwtEncoder jwtEncoder() {
    JWK jwk = new RSAKey.Builder(rsaKeyProperties.publicKey()).privateKey(rsaKeyProperties.privateKey()).build();
    JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
    return new NimbusJwtEncoder(jwks);
}
```

### JWT Token Service

```java
@Service
@RequiredArgsConstructor
public class TokenService {

    private final JwtEncoder jwtEncoder;

    /**
     * Method to resolve token, need to pass authentication object,
     * It will generate token with user authorities with 1 hour expiry
     * @param authentication
     * @return
     */
    public String resolveToken(Authentication authentication) {
        Instant issuedAt = Instant.now();

        var authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList();

        var claims = JwtClaimsSet.builder().issuer("self").issuedAt(issuedAt).expiresAt(issuedAt.plus(1, ChronoUnit.HOURS))
                .subject(authentication.getName()).claim("scope",authorities).build();

        return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }

}
```

### JDBCOneTimeTokenService

This class is responsible for generating token and validates them. It uses OneTimeToken entity to store token in database and deleted them after use.

This service is copy of **`JdbcOneTimeTokenService`** defined in package **`org.springframework.security.authentication.ott`**  in spring security.

We also need to schedule a cron to cleanup expired tokens.

```java
package com.app.token.rest;

import com.app.token.repository.OneTimeTokenRepository;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Profile;
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken;
import org.springframework.security.authentication.ott.OneTimeTokenService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.UUID;

/**
 *
 * A implementation of  {@link OneTimeToken} that used to for token persistence.
 *  It is same as JdbcOneTimeTokenService just to increase token expiry time
 *
 */

@Service
@Profile("rest")
@Log4j2
@RequiredArgsConstructor
public class JDBCTokenGeneratorService implements OneTimeTokenService {

    private final OneTimeTokenRepository oneTimeTokenRepository;

    private final Clock clock = Clock.systemUTC();

    @Transactional
    @Override
    @org.springframework.lang.NonNull
    public OneTimeToken generate(@NonNull GenerateOneTimeTokenRequest request) {
        String token = UUID.randomUUID().toString();
        int expiryInMinutes = 15;
        Instant fiveMinutesFromNow = this.clock.instant().plus(Duration.ofMinutes(expiryInMinutes));
        var oneTimeToken = new com.app.token.entity.OneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
        oneTimeToken = this.oneTimeTokenRepository.save(oneTimeToken);
        return oneTimeToken;
    }


    @Override
    public OneTimeToken consume(@NonNull OneTimeTokenAuthenticationToken authenticationToken) {

        List<com.app.token.entity.OneTimeToken> tokens = this.oneTimeTokenRepository.findByTokenValue(authenticationToken.getTokenValue());
        if (CollectionUtils.isEmpty(tokens)) {
            return null;
        }
        com.app.token.entity.OneTimeToken token = tokens.getFirst();
        this.oneTimeTokenRepository.deleteById(token.getId());

        if (isExpired(token)) {
            return null;
        }

        return token;
    }

    private boolean isExpired(OneTimeToken ott) {
        return this.clock.instant().isAfter(ott.getExpiresAt());
    }

}
```

### SecurityConfiguration

This security configuration has to separate filter chain methods.

One is required to do login with username and password, that only executes when request comes from url as /token&#x20;

Another one is used for all other application security and ott.

{% code fullWidth="true" %}

```java
package com.app.token.rest;

import com.app.token.rest.jwt.RsaKeyProperties;
import com.app.token.service.CustomUserDetailsService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.Algorithm;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.authentication.ott.OneTimeTokenService;
import org.springframework.security.config.Customizer;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.io.IOException;
import java.security.AlgorithmConstraints;
import java.util.Map;

/**
 * @Author saurabh vaish
 * @Date 02-02-2025
 */
@Profile("rest")
@Configuration
@EnableWebSecurity
@Log4j2
@RequiredArgsConstructor
public class SecurityConfig {

    private final JDBCTokenGeneratorService jdbcTokenGeneratorService;

    private final String [] permitUrl = {"/error","/exp","/public","/ott/sent","/login/ott","/ott/generate/**","/api/rest/login/ott/consume/**"};

    private final RsaKeyProperties rsaKeyProperties;

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // this filter chain will be used to all other things except /token request.
    // it will work all the request from token generation to ott
    @Bean
    public SecurityFilterChain configure(HttpSecurity security) throws Exception {

        return security.authorizeHttpRequests(request ->
                        request
                                .requestMatchers(permitUrl).permitAll()
                                .anyRequest().authenticated()
                )
                .httpBasic(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                .cors(AbstractHttpConfigurer::disable)
                .oauth2ResourceServer(oauth->oauth.jwt(Customizer.withDefaults()))
                .sessionManagement(sess->sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .exceptionHandling(exceptionHandlingSpec ->new CustomAccessDeniedHandler())
//                .logout(logoutSpec -> logoutSpec.getLogoutSuccessHandler().onLogoutSuccess())
                .oneTimeTokenLogin(ott->
                        ott
                                // default is fine, uses SimpleUrlAuthenticationFailureHandler , that checks token and prepare authentication object
                                // by default only gets username but we can configure Authentication Manger and UserDetailsService to get more details
//                                .authenticationProvider() // with different or custom authentication provider for ott verification

                                // url that specifies how token will get generated with , default one time token service - [InMemory or Jdbc]
                                .tokenGeneratingUrl("/ott/generate") 

                                // specifies the url that process login request, it must be same as the token generated url
                                .loginProcessingUrl("/api/rest/login/ott/consume") 

                                // taking jdbc token service instead of in memory
                                .tokenService(jbdcTokenService()) // used to specify TokenGenerationService - InMemory Or JdbcTokenService , default is {@link InMemoryOneTimeTokenService} with default expiry of 5 minutes

                                // default login page for ott, can be changed to custom one but need to handle csrf
                                // commenting this page as its not needed in rest api, must disable showDefaultSubmitPage
//                                .defaultSubmitPageUrl("/api/rest/login/ott/consume") // token submit page , default is "/login/ott" can be changed to custom one
                                .showDefaultSubmitPage(false)  // sets the value to show submit page or not
//                                .tokenGenerationSuccessHandler()

                                .authenticationFailureHandler(failureHandler()) // specifies the things when token is not valid, defaults to /login?error - SimpleUrlAuthenticationFailureHandler
                                // configures the success handler overrides from default that redirect to previous page, it always redirect to /home
                                .authenticationSuccessHandler(successHandler()) // specifies the success handler - default SavedRequestAwareAuthenticationSuccessHandler
                        )
                .logout(logout->logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler()))
                .build();

    }

    /***
     * security filer just for login, other req will be handled by above filter chain
     * it will be using basic auth , once login is done , req will be redirected to {@link RestLoginController}/{token} endpoint .
     * Where we will generate token
     *
     * @param http
     * @return
     * @throws Exception
     */
    @Order(Ordered.HIGHEST_PRECEDENCE)
    @Bean
    SecurityFilterChain tokenSecurityFilterChain(HttpSecurity http) throws Exception {
        return http.securityMatcher("/api/rest/token")
                .authorizeHttpRequests(req->req.anyRequest().authenticated())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .csrf(AbstractHttpConfigurer::disable)
                .exceptionHandling(ex -> {
                    ex.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint());
                    ex.accessDeniedHandler(new BearerTokenAccessDeniedHandler());
                })
                .httpBasic(Customizer.withDefaults())
                .build();
    }

    // dao authentication manager
    @Bean
    public AuthenticationManager authenticationManager(CustomUserDetailsService customUserDetailsService) throws Exception {
        var provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(customUserDetailsService);

        return new ProviderManager(provider);

    }
    
    // custom one time token generator service
    private OneTimeTokenService jbdcTokenService() {
//        return new JdbcOneTimeTokenService(jdbcOperations); // default JdbcOneTimeTokenService with default expiry of 5 minutes
        return jdbcTokenGeneratorService; // custom OneTimeTokenService with default expiry of 15 minutes
    }

    // ott success handler
    private AuthenticationSuccessHandler successHandler() {
//        return new SimpleUrlAuthenticationSuccessHandler("/home");
        return new RestOneTimeTokenAuthenticationSucessHandler();
    }

    private AuthenticationFailureHandler failureHandler(){
        return (request,response,exception)->{
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");

            var map = Map.of("Unauthorized", exception.getMessage());
            response.getWriter().write(new ObjectMapper().writeValueAsString(map));
        };
    }


    // custom jwt decoder with rsa public key to decode incoming jwt token
    @Bean
    public JwtDecoder jwtDecoder(){
        return NimbusJwtDecoder.withPublicKey(rsaKeyProperties.publicKey()).build();
    }

    // custom jwt encoder to encode the tokens generated by ouath2, encodes it with both same public and private keys that we will be using in decode
    @Bean
    JwtEncoder jwtEncoder() {
        JWK jwk = new RSAKey.Builder(rsaKeyProperties.publicKey()).privateKey(rsaKeyProperties.privateKey()).build();
        JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
        return new NimbusJwtEncoder(jwks);
    }

    private LogoutSuccessHandler logoutSuccessHandler(){
        return (request, response, authentication) ->{
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(new ObjectMapper().writeValueAsString("Logout successfully"));
        };
    }

}

class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        String responseBody = "{\"error\": \"" + accessDeniedException.getLocalizedMessage() + "\"}";
        response.getWriter().write(responseBody);
    }
}


```

{% endcode %}

### OneTimeTokenSuccessHandler

This class will verify user , get email and send generated token with mail or sms

```java
@Component
@Log4j2
@RequiredArgsConstructor
public class OneTimeTokenSuccessHandler implements OneTimeTokenGenerationSuccessHandler {

    private final EmailService emailService;

    private final CustomUserDetailsService userDetailsService;


    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {

        var email = "";
        try{
            email = checkAndGetEmail(oneTimeToken.getUsername());
        }catch (Exception e){
            log.error("Exception getting email for user: {}", oneTimeToken.getUsername());
            sendJsonResponse(response);
            return;
        }

        // creates a logging processing url 
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
                .replacePath(request.getContextPath())
                .replaceQuery(null)
                .fragment(null)
                .path("/api/rest/login/ott/consume")
                .queryParam("token", oneTimeToken.getTokenValue());

        String tokenLink = builder.toUriString();

        System.out.println("Token Link: " + tokenLink );

        var body = """
                Hii %s, Below you will find your secure link to login !
                %s""".formatted(oneTimeToken.getUsername(),magicLink);

        try {
            var sendTo = oneTimeToken.getUsername();
            log.info("Sending One Time Token to username: {}", sendTo);

           emailService.sendMail(email, "One Time Token Login", body,false);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("Exception sending email: {}", e.getMessage());
            sendJsonResponse(response);
        }
    }

    private String checkAndGetEmail(String username) {
        log.info("Retrieving email for user: {}", username);

        var user = (AppUser)userDetailsService.loadUserByUsername(username);
        return user.getEmail();
    }


    public void sendJsonResponse(HttpServletResponse response) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        var map = Map.of("Unauthorized", "Session Timeout");
        response.getWriter().write(new ObjectMapper().writeValueAsString(map));
    }

}
```

### AuthenticationSuccessHandler

```java
public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        clearAuthenticationAttributes(request);
        response.setStatus(HttpServletResponse.SC_OK);
    }

    protected final void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
        }
    }
}
```

### RestController

```java
@RestController
@RequestMapping("/api/rest")
@Log4j2
@RequiredArgsConstructor
public class RestLoginController {

    private final TokenService tokenService;

    @PostMapping("/login/ott/consume")
    public ResponseEntity<String> consumeOtt(Authentication principal){
        System.out.println("principal = " + principal.getName());
        return ResponseEntity.ok("Successfully logged in , token ::" + tokenService.resolveToken(principal));
    }

    @PostMapping("/token")
    public ResponseEntity<String> generateToken(Authentication authentication){
        log.debug("Token requested for user: '{}'", authentication.getName());
        String token = tokenService.resolveToken(authentication);
        log.debug("Token granted: {}", token);
        return ResponseEntity.ok(token);
    }


    @PostMapping("/validate")
    public void validateToken(String token){
        log.info("request to ");
    }

    @GetMapping("/secure/api")
    public ResponseEntity<String> secureApi(Authentication principal){
        return ResponseEntity.ok("secure api = " + principal.getName());
    }

}
```
