Skip to content

Spring-security jwt token

Hello! In this post, I try to represent a simple way to secure our applications using spring security and JSON Web Token. Full source code is available in GitHub: https://github.com/kazakovav/spring-security-jwt/tree/master/simple

Initialize project

First of all, create a template at start.spring.io

Then adding additional libraries for the project:

For spring security we need to add spring security dependencies to the application:

implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.security:spring-security-oauth2-resource-server")
implementation("org.springframework.security:spring-security-oauth2-jose")

Also add jjwt for jwt creation capabilities token and bouncy-castle for encription

implementation("io.jsonwebtoken:jjwt-api:$jjwtVersion")
implementation("io.jsonwebtoken:jjwt-impl:$jjwtVersion")
implementation("io.jsonwebtoken:jjwt-jackson:$jjwtVersion")

implementation("org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion")
implementation("org.bouncycastle:bcpkix-jdk15on:$bouncyCastleVersion")

Rest API

For demonstration purposes, we create a controller with two methods “getCurrentUser” and “authenticate”.The “authenticate” method will be used for user login. And “getCurrentUser” could be used for user identification and provide information with storing in the authentication:

@RestController
@RequestMapping("/")
class AuthenticationController(
        private val tokenBuilder: TokenBuilder
) {

    @GetMapping("me")
    fun getCurrentUser(): Jwt {
        val authentication = SecurityContextHolder.getContext().authentication;
        return authentication.principal as Jwt;
    }

    @PostMapping("/login")
    fun authenticate(@RequestBody authenticationRequest: AuthenticationRequest): AuthenticationResponse {
        if ("admin" != authenticationRequest.userName && "adminPassword" != authenticationRequest.password) {
            throw UsernameNotFoundException("User name not found")
        }
        val userName = authenticationRequest.userName
        val authorities = listOf(UserRoles.USER.name, UserRoles.ADMIN.name)

        val jwt = tokenBuilder.buildToken(authorities, userName)
        return AuthenticationResponse(jwt)
    }
}

Building token

For token building are used the “jjwt” library. We can store different information in the token. But be careful and do not store personal user data and secure information. For demo example just authorities and user name are added to token info:

@Component
class TokenBuilderImpl(
        @Value("\\${jwt.token-validity-in-seconds}")
        private val tokenValidityInSeconds: Long,
        private val keyProvider: KeyProvider
) : TokenBuilder {

    override fun buildToken(authorities: List<String>, userName: String): String {
        val expiration = getExpiration()
        val claims: Map<String, Any> = buildClaims(authorities);
        return Jwts.builder()
                .setClaims(claims)
                .setId(UUID.randomUUID().toString())
                .setSubject(userName)
                .signWith(getKey(), SignatureAlgorithm.RS256)
                .setExpiration(expiration)
                .compact()
    }

    private fun getExpiration(): Date =
            Date(System.currentTimeMillis() + tokenValidityInSeconds * 1000)

    private fun getKey() = keyProvider.getKey()

    private fun buildClaims(authorities: List<String>): Map<String, Any> {
        val claims = HashMap<String, Any>()
        claims["authorities"] = authorities
        return claims
    }
}

KeyProvider load key from “pem” file in class-path and convert it to java.security.Key. BouncyCastle library is used for key converting:

@Component
class KeyProviderImpl(private val jwtProperties: JwtProperties) : KeyProvider {

    private lateinit var privateKey: Key

    @PostConstruct
    fun afterPropertiesSet(): Unit {
        log.info("Loaded properties: $jwtProperties")
        privateKey = loadKey()
    }

    private fun loadKey(): Key {
        val factory: KeyFactory = KeyFactory.getInstance(ALGORITHM)
        val resource = ClassPathResource(jwtProperties.pemFilePath)

        InputStreamReader(resource.inputStream).use { keyReader ->
            PemReader(keyReader).use { pemReader ->
                val pemObject = pemReader.readPemObject()
                val content = pemObject.content
                val privateKeySpec = PKCS8EncodedKeySpec(content)
                return factory.generatePrivate(privateKeySpec) as RSAPrivateKey
            }
        }
    }

    override fun getKey(): Key = privateKey

    companion object {
        @Suppress("JAVA_CLASS_ON_COMPANION")
        @JvmStatic
        private val log = LoggerFactory.getLogger(javaClass.enclosingClass)

        @JvmStatic
        val ALGORITHM = "RSA"
    }
}

loadKey() function uses PemReader from BouncyCastle for read and convert it PKCS8EncodedKeySpec.

Execute next command for pem key generation

openssl genpkey -out jwt.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048

And for public key:

openssl rsa -in jwt.pem -pubout > jwt.pub

Copy jwt.pem and jwt.pub to src/main/resources folder

Configuring spring-security

We could create custom filter for token validation and user authorization, for instance, see stackoverflow java example: https://stackoverflow.com/a/41975719

But instead of reinventing the wheel, we could use spring-security-oauth2-resourceserver for this purpose. Sample configuration:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration(private val objectMapper: ObjectMapper
) : WebSecurityConfigurerAdapter() {

    @Throws(Exception::class)
    override fun configure(http: HttpSecurity) {
        http {
            csrf {
                disable()
            }
            authorizeRequests {
                authorize("login", permitAll)
                authorize("logout", permitAll)
                authorize("/version", permitAll)
                authorize(anyRequest, authenticated)
            }

            oauth2ResourceServer {
                jwt {
                    jwtAuthenticationConverter = jwtCustomAuthenticationConverter()
                }
            }
        }
    }

    @Bean
    fun jwtCustomAuthenticationConverter(): Converter<Jwt, AbstractAuthenticationToken>? {
        val jwtAuthenticationConverter = JwtAuthenticationConverter()
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter())
        return jwtAuthenticationConverter
    }

    @Bean
    fun jwtGrantedAuthoritiesConverter(): Converter<Jwt, Collection<GrantedAuthority>?>? {
        return CustomJwtRolesConverter()
    }

}

By default spring security extract scopes from JWT token by org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter.

And parses it like scope, i.e. it will be present like SCOPE_ADMIN. SCOPE_USER. If we want to use role model then we need to create a custom converter:

class CustomJwtRolesConverter : Converter<Jwt, Collection<GrantedAuthority>?> {

    override fun convert(jwt: Jwt): Collection<GrantedAuthority> {
        val authorities = jwt.claims[AUTHORITIES_KEY] as JSONArray

        return authorities.stream().map { role: Any ->
            SimpleGrantedAuthority(ROLE_PREFIX + role)
        }.collect(Collectors.toList())
    }

    companion object {
        const val AUTHORITIES_KEY = "authorities"
        const val ROLE_PREFIX = "ROLE_"
    }
}

Lets test application

Build and run application.

For testing are used IntelliJ IDEA embedded rest client.

Login user:

###
POST <http://localhost:8080/login>
Content-Type: application/json

{
  "userName": "admin",
  "password": "admin"
}

> {% client.global.set("auth_token", response.body.token); %}

Login result:

POST <http://localhost:8080/login>

HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 14 Oct 2020 06:27:33 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "token": "eyJhbGciOiJSUzI1NiJ9.eyJhdXRob3JpdGllcyI6WyJVU0VSIiwiQURNSU4iXSwianRpIjoiNzMyM2M1ODAtYjgyZC00NjBkLTkzM2UtNzU4MzI0NjAzYjA4Iiwic3ViIjoiYWRtaW4iLCJleHAiOjE2MDI3NDMyNTN9.a7RM9t00KYrsMR1dUT_fK3kcU4KueA_yOsUYsVlsp8PdMdUtUetpPa26inzJP8FFrecjrN1f49syIKTpgwe4n0gWy07xQ5w4WaK0Nxq17IfjBU1In5Yq4PeNmgi96jKFSCrzNL6oFkCfUbGTTQiK4OHzmhyWeQl78zVfM6LGnPFejJlE8GuevPKYbBVYb4Xxauj2TVljXad3UACKkIGsHZoHX5_pGhOYE7Hrrj-M_w4Qc-WpyGuMc35TVxpnOzee5jta_UlUSEzgA1KSUSmBx23bw-JlbUfmVdkn_JrztS1oGxOEjH3rmGZgpEOPbM9CkA7HEP3olYKUX9UoePuLUw"
}

Response code: 200; Time: 547ms; Content length: 520 bytes

The token is stored in the environment variable auth_token. After it could be used by inserting in the header – Authorization: Bearer {{auth_token}} or could be used by copy and past to the header manually.

Obtain information about current user:

###
GET  <http://localhost:8080/me>
Content-Type: application/json
Authorization: Bearer {{auth_token}}

Result:

GET <http://localhost:8080/me>

HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 14 Oct 2020 06:31:41 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "tokenValue": "eyJhbGciOiJSUzI1NiJ9.eyJhdXRob3JpdGllcyI6WyJVU0VSIiwiQURNSU4iXSwianRpIjoiNzMyM2M1ODAtYjgyZC00NjBkLTkzM2UtNzU4MzI0NjAzYjA4Iiwic3ViIjoiYWRtaW4iLCJleHAiOjE2MDI3NDMyNTN9.a7RM9t00KYrsMR1dUT_fK3kcU4KueA_yOsUYsVlsp8PdMdUtUetpPa26inzJP8FFrecjrN1f49syIKTpgwe4n0gWy07xQ5w4WaK0Nxq17IfjBU1In5Yq4PeNmgi96jKFSCrzNL6oFkCfUbGTTQiK4OHzmhyWeQl78zVfM6LGnPFejJlE8GuevPKYbBVYb4Xxauj2TVljXad3UACKkIGsHZoHX5_pGhOYE7Hrrj-M_w4Qc-WpyGuMc35TVxpnOzee5jta_UlUSEzgA1KSUSmBx23bw-JlbUfmVdkn_JrztS1oGxOEjH3rmGZgpEOPbM9CkA7HEP3olYKUX9UoePuLUw",
  "issuedAt": "2020-10-15T06:27:32Z",
  "expiresAt": "2020-10-15T06:27:33Z",
  "headers": {
    "alg": "RS256"
  },
  "claims": {
    "sub": "admin",
    "exp": "2020-10-15T06:27:33Z",
    "iat": "2020-10-15T06:27:32Z",
    "authorities": [
      "USER",
      "ADMIN"
    ],
    "jti": "7323c580-b82d-460d-933e-758324603b08"
  },
  "id": "7323c580-b82d-460d-933e-758324603b08",
  "subject": "admin",
  "audience": null,
  "notBefore": null,
  "issuer": null
}

Response code: 200; Time: 116ms; Content length: 888 bytes

Thanks! Happy coding!

Leave a Reply

Your email address will not be published.