added users, jwt AT and RT
This commit is contained in:
parent
ed13d6b914
commit
134ae88bd3
|
|
@ -26,6 +26,13 @@ dependencies {
|
|||
|
||||
compileOnly("org.projectlombok:lombok:1.18.30")
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.30")
|
||||
|
||||
|
||||
implementation ("org.springframework.boot:spring-boot-starter-security")
|
||||
testImplementation("org.springframework.security:spring-security-test")
|
||||
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
|
||||
implementation("io.jsonwebtoken:jjwt-impl:0.12.3")
|
||||
implementation("io.jsonwebtoken:jjwt-jackson:0.12.3")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.kassaev.notes.config
|
||||
|
||||
import com.kassaev.notes.repository.UserRepository
|
||||
import com.kassaev.notes.service.CustomUserDetailsService
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.AuthenticationProvider
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(JwtProperties::class)
|
||||
class Configuration {
|
||||
|
||||
@Bean
|
||||
fun userDetailsService(userRepository: UserRepository): UserDetailsService =
|
||||
CustomUserDetailsService(userRepository)
|
||||
|
||||
@Bean
|
||||
fun encoder(): PasswordEncoder = BCryptPasswordEncoder()
|
||||
|
||||
@Bean
|
||||
fun authenticationProvider(userRepository: UserRepository): AuthenticationProvider =
|
||||
DaoAuthenticationProvider()
|
||||
.also {
|
||||
it.setUserDetailsService(userDetailsService(userRepository))
|
||||
it.setPasswordEncoder(encoder())
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager =
|
||||
config.authenticationManager
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.kassaev.notes.config
|
||||
|
||||
import com.kassaev.notes.service.CustomUserDetailsService
|
||||
import com.kassaev.notes.service.TokenService
|
||||
import jakarta.servlet.FilterChain
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
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.web.filter.OncePerRequestFilter
|
||||
|
||||
@Component
|
||||
class JwtAuthenticationFilter(
|
||||
private val userDetailsService: CustomUserDetailsService,
|
||||
private val tokenService: TokenService
|
||||
): OncePerRequestFilter() {
|
||||
override fun doFilterInternal(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
filterChain: FilterChain
|
||||
) {
|
||||
val authHeader: String? = request.getHeader("Authorization")
|
||||
|
||||
if(authHeader.doNotContainBearerToken()){
|
||||
filterChain.doFilter(request, response)
|
||||
return
|
||||
}
|
||||
|
||||
val jwtToken = authHeader!!.extractTokenValue()
|
||||
val email = tokenService.extractEmail(jwtToken)
|
||||
|
||||
if (email != null && SecurityContextHolder.getContext().authentication == null){
|
||||
val foundUser = userDetailsService.loadUserByUsername(email)
|
||||
|
||||
if (tokenService.isValid(jwtToken, foundUser)){
|
||||
updateContext(foundUser, request)
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateContext(foundUser: UserDetails, request: HttpServletRequest) {
|
||||
val authToken = UsernamePasswordAuthenticationToken(foundUser, null, foundUser.authorities)
|
||||
authToken.details = WebAuthenticationDetailsSource().buildDetails(request)
|
||||
|
||||
SecurityContextHolder.getContext().authentication = authToken
|
||||
}
|
||||
|
||||
private fun String?.doNotContainBearerToken(): Boolean =
|
||||
this == null || !this.startsWith("Bearer ")
|
||||
|
||||
private fun String.extractTokenValue(): String =
|
||||
this.substringAfter("Bearer ")
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.kassaev.notes.config
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
|
||||
@ConfigurationProperties("jwt")
|
||||
data class JwtProperties(
|
||||
val key: String,
|
||||
val accessTokenExpiration: Long,
|
||||
val refreshTokenExpiration: Long
|
||||
)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.kassaev.notes.config
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.security.authentication.AuthenticationProvider
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfiguration(
|
||||
private val authenticationProvider: AuthenticationProvider
|
||||
) {
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(
|
||||
http: HttpSecurity,
|
||||
jwtAuthenticationFilter: JwtAuthenticationFilter
|
||||
): DefaultSecurityFilterChain =
|
||||
http
|
||||
.csrf { it.disable() }
|
||||
.authorizeHttpRequests {
|
||||
it
|
||||
.requestMatchers("/api/v1.0.0/auth", "/api/v1.0.0/auth/refresh", "/error")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/v1.0.0/user")
|
||||
.permitAll()
|
||||
.requestMatchers("/api/v1.0.0/user**")
|
||||
.hasRole("ADMIN")
|
||||
.anyRequest()
|
||||
.fullyAuthenticated()
|
||||
}
|
||||
.sessionManagement {
|
||||
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
}
|
||||
.authenticationProvider(authenticationProvider)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
|
||||
.build()
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.kassaev.notes.controller.auth
|
||||
|
||||
import com.kassaev.notes.service.AuthenticationService
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1.0.0/auth")
|
||||
class AuthController(
|
||||
private val authenticationService: AuthenticationService
|
||||
) {
|
||||
|
||||
@PostMapping
|
||||
fun authenticate(@RequestBody authRequest: AuthenticationRequest): AuthenticationResponse =
|
||||
authenticationService.authentication(authRequest)
|
||||
|
||||
@PostMapping("/refresh")
|
||||
fun refreshAccessToken(
|
||||
@RequestBody request: RefreshTokenRequest
|
||||
): TokenResponse =
|
||||
authenticationService.refreshAccessToken(request.token)
|
||||
?.mapToTokenResponse()
|
||||
?: throw ResponseStatusException(HttpStatus.FORBIDDEN, "Invalid refresh token!")
|
||||
|
||||
private fun String.mapToTokenResponse(): TokenResponse =
|
||||
TokenResponse(
|
||||
token = this
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.kassaev.notes.controller.auth
|
||||
|
||||
data class AuthenticationRequest(
|
||||
val email: String,
|
||||
val password: String
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.kassaev.notes.controller.auth
|
||||
|
||||
data class AuthenticationResponse(
|
||||
val accessToken: String,
|
||||
val refreshToken: String
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.kassaev.notes.controller.auth
|
||||
|
||||
data class RefreshTokenRequest(
|
||||
val token: String
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.kassaev.notes.controller.auth
|
||||
|
||||
data class TokenResponse(
|
||||
val token: String
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.kassaev.notes.controller
|
||||
package com.kassaev.notes.controller.note
|
||||
|
||||
import com.kassaev.notes.model.Note
|
||||
import com.kassaev.notes.service.NoteService
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.kassaev.notes.controller.user
|
||||
|
||||
import com.kassaev.notes.model.Role
|
||||
import com.kassaev.notes.model.User
|
||||
import com.kassaev.notes.service.UserService
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1.0.0/user")
|
||||
class UserController(
|
||||
private val userService: UserService
|
||||
) {
|
||||
|
||||
@PostMapping
|
||||
fun create(@RequestBody userRequest: UserRequest): UserResponse =
|
||||
userService.createUser(
|
||||
user = userRequest.toModel()
|
||||
)
|
||||
?.toResponse()
|
||||
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot create user.")
|
||||
|
||||
@GetMapping
|
||||
fun listAll(): List<UserResponse> =
|
||||
userService.findAll()
|
||||
.map { it.toResponse() }
|
||||
|
||||
@GetMapping("/{uuid}")
|
||||
fun findByUUID(@PathVariable uuid: UUID): UserResponse =
|
||||
userService.findByUUID(uuid)
|
||||
?.toResponse()
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Cannot find a user.")
|
||||
|
||||
@DeleteMapping("/{uuid}")
|
||||
fun deleteByUUID(@PathVariable uuid: UUID): ResponseEntity<Boolean> {
|
||||
val success = userService.deleteByUUID(uuid)
|
||||
|
||||
return if(success) {
|
||||
ResponseEntity.noContent().build()
|
||||
} else {
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Cannot find a user.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserRequest.toModel(): User =
|
||||
User(
|
||||
id = UUID.randomUUID(),
|
||||
email = this.email,
|
||||
password = this.password,
|
||||
role = Role.USER
|
||||
)
|
||||
|
||||
private fun User.toResponse(): UserResponse =
|
||||
UserResponse(
|
||||
uuid = this.id,
|
||||
email = this.email
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.kassaev.notes.controller.user
|
||||
|
||||
data class UserRequest(
|
||||
val email: String,
|
||||
val password: String
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.kassaev.notes.controller.user
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class UserResponse(
|
||||
val uuid: UUID,
|
||||
val email: String
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.kassaev.notes.model
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class User(
|
||||
val id: UUID,
|
||||
val email: String,
|
||||
val password: String,
|
||||
val role: Role
|
||||
)
|
||||
|
||||
enum class Role{
|
||||
USER, ADMIN
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.kassaev.notes.repository
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
class RefreshTokenRepository {
|
||||
|
||||
private val tokens = mutableMapOf<String, UserDetails>()
|
||||
|
||||
fun findUserDetailsByToken(token: String): UserDetails? =
|
||||
tokens[token]
|
||||
|
||||
fun save(token: String, userDetails: UserDetails) {
|
||||
tokens[token] = userDetails
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.kassaev.notes.repository
|
||||
|
||||
import com.kassaev.notes.model.Role
|
||||
import com.kassaev.notes.model.User
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.util.*
|
||||
|
||||
@Repository
|
||||
class UserRepository(
|
||||
private val encoder: PasswordEncoder
|
||||
) {
|
||||
private val users = mutableListOf(
|
||||
User(
|
||||
id = UUID.randomUUID(),
|
||||
email = "user1@mail.com",
|
||||
password = encoder.encode("pass1"),
|
||||
role = Role.USER
|
||||
),
|
||||
User(
|
||||
id = UUID.randomUUID(),
|
||||
email = "user2@mail.com",
|
||||
password = encoder.encode("pass2"),
|
||||
role = Role.USER
|
||||
),
|
||||
User(
|
||||
id = UUID.randomUUID(),
|
||||
email = "user3@mail.com",
|
||||
password = encoder.encode("pass3"),
|
||||
role = Role.ADMIN
|
||||
),
|
||||
)
|
||||
|
||||
fun save(user: User): Boolean {
|
||||
val updated = user.copy(password = encoder.encode(
|
||||
user.password
|
||||
))
|
||||
return users.add(updated)
|
||||
}
|
||||
|
||||
fun findByEmail(email: String): User? =
|
||||
users.firstOrNull { it.email == email }
|
||||
|
||||
fun findByUUID(uuid: UUID): User? =
|
||||
users.firstOrNull { it.id == uuid }
|
||||
|
||||
fun findAll(): List<User> =
|
||||
users
|
||||
|
||||
fun deleteByUUID(uuid: UUID): Boolean {
|
||||
val foundUser = findByUUID(uuid)
|
||||
|
||||
return foundUser?.let {
|
||||
users.remove(it)
|
||||
} ?: false
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.kassaev.notes.service
|
||||
|
||||
import com.kassaev.notes.config.JwtProperties
|
||||
import com.kassaev.notes.controller.auth.AuthenticationRequest
|
||||
import com.kassaev.notes.controller.auth.AuthenticationResponse
|
||||
import com.kassaev.notes.repository.RefreshTokenRepository
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.Date
|
||||
|
||||
@Service
|
||||
class AuthenticationService(
|
||||
private val authManager: AuthenticationManager,
|
||||
private val userDetailsService: CustomUserDetailsService,
|
||||
private val tokenService: TokenService,
|
||||
private val jwtProperties: JwtProperties,
|
||||
private val refreshTokenRepository: RefreshTokenRepository
|
||||
) {
|
||||
fun authentication(authRequest: AuthenticationRequest): AuthenticationResponse {
|
||||
authManager.authenticate(
|
||||
UsernamePasswordAuthenticationToken(
|
||||
authRequest.email,
|
||||
authRequest.password
|
||||
)
|
||||
)
|
||||
|
||||
val user = userDetailsService.loadUserByUsername(authRequest.email)
|
||||
|
||||
val accessToken = generateAccessToken(user)
|
||||
val refreshToken = generateRefreshToken(user)
|
||||
|
||||
refreshTokenRepository.save(refreshToken, user)
|
||||
|
||||
return AuthenticationResponse(
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateRefreshToken(user: UserDetails) = tokenService.generate(
|
||||
userDetails = user,
|
||||
expirationDate = Date(System.currentTimeMillis() + jwtProperties.refreshTokenExpiration)
|
||||
)
|
||||
|
||||
private fun generateAccessToken(user: UserDetails) = tokenService.generate(
|
||||
userDetails = user,
|
||||
expirationDate = Date(System.currentTimeMillis() + jwtProperties.accessTokenExpiration)
|
||||
)
|
||||
|
||||
fun refreshAccessToken(token: String): String? {
|
||||
val extractedEmail = tokenService.extractEmail(token)
|
||||
|
||||
return extractedEmail?.let { email ->
|
||||
val currentUserDetails = userDetailsService.loadUserByUsername(email)
|
||||
val refreshTokenUserDetails = refreshTokenRepository.findUserDetailsByToken(token)
|
||||
|
||||
if (!tokenService.isExpired(token) && currentUserDetails.username == refreshTokenUserDetails?.username)
|
||||
// TODO: check if this refresh token is the same token in db for this user <refresh token, user>. If so generate new access token and refresh token and update record in with newly created refresh token for this user.
|
||||
generateAccessToken(currentUserDetails)
|
||||
else
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.kassaev.notes.service
|
||||
|
||||
import com.kassaev.notes.repository.UserRepository
|
||||
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
|
||||
|
||||
typealias ApplicationUser = com.kassaev.notes.model.User
|
||||
@Service
|
||||
class CustomUserDetailsService(
|
||||
private val userRepository: UserRepository
|
||||
): UserDetailsService {
|
||||
override fun loadUserByUsername(username: String): UserDetails =
|
||||
userRepository.findByEmail(username)
|
||||
?.mapToUserDetails()
|
||||
?: throw UsernameNotFoundException("Not found!")
|
||||
|
||||
private fun ApplicationUser.mapToUserDetails(): UserDetails =
|
||||
User.builder()
|
||||
.username(this.email)
|
||||
.password(this.password)
|
||||
.roles(this.role.name)
|
||||
.build()
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.kassaev.notes.service
|
||||
|
||||
import com.kassaev.notes.config.JwtProperties
|
||||
import io.jsonwebtoken.Claims
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.security.Keys
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.Date
|
||||
|
||||
@Service
|
||||
class TokenService(
|
||||
jwtProperties: JwtProperties
|
||||
) {
|
||||
private val secretKey = Keys.hmacShaKeyFor(
|
||||
jwtProperties.key.toByteArray()
|
||||
)
|
||||
|
||||
fun generate(
|
||||
userDetails: UserDetails,
|
||||
expirationDate: Date,
|
||||
additionalClaims: Map<String, Any> = emptyMap()
|
||||
): String =
|
||||
Jwts.builder()
|
||||
.claims()
|
||||
.subject(userDetails.username)
|
||||
.issuedAt(Date(System.currentTimeMillis()))
|
||||
.expiration(expirationDate)
|
||||
.add(additionalClaims)
|
||||
.and()
|
||||
.signWith(secretKey)
|
||||
.compact()
|
||||
|
||||
fun extractEmail(token: String): String? =
|
||||
getAllClaims(token)
|
||||
.subject
|
||||
|
||||
fun isExpired(token: String): Boolean =
|
||||
getAllClaims(token)
|
||||
.expiration
|
||||
.before(Date(System.currentTimeMillis()))
|
||||
|
||||
fun isValid(token: String, userDetails: UserDetails): Boolean {
|
||||
val email = extractEmail(token)
|
||||
|
||||
return userDetails.username == email && !isExpired(token)
|
||||
}
|
||||
|
||||
private fun getAllClaims(token: String): Claims {
|
||||
val parser = Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
|
||||
return parser
|
||||
.parseSignedClaims(token)
|
||||
.payload
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.kassaev.notes.service
|
||||
|
||||
import com.kassaev.notes.model.User
|
||||
import com.kassaev.notes.repository.UserRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.UUID
|
||||
|
||||
@Service
|
||||
class UserService(
|
||||
private val userRepository: UserRepository
|
||||
) {
|
||||
fun createUser(user: User): User? {
|
||||
val found = userRepository.findByEmail(user.email)
|
||||
|
||||
return if (found == null) {
|
||||
userRepository.save(user)
|
||||
user
|
||||
} else null
|
||||
}
|
||||
|
||||
fun findByUUID(uuid: UUID): User? =
|
||||
userRepository.findByUUID(uuid)
|
||||
|
||||
fun findAll(): List<User> =
|
||||
userRepository.findAll()
|
||||
|
||||
fun deleteByUUID(uuid: UUID): Boolean =
|
||||
userRepository.deleteByUUID(uuid)
|
||||
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
jwt:
|
||||
key: ${JWT_KEY}
|
||||
access-token-expiration: 3600000
|
||||
refresh-token-expiration: 86400000
|
||||
Loading…
Reference in New Issue