Hello!
In this post, we will improve the application from the previous post – https://akazakov.dev/?p=39. All code could be found at Github https://github.com/kazakovav/spring-security-jwt/tree/master/generate-jwk
In modern applications are used microservices architecture. One common approach is when microservices are connected like resource services – they are only validating user permissions. For demonstration token-based security in microservices architectures, we will divide the application into 2 services – authentication service and resource service.
Authentication service will provide user authentication and token creation
The resource server will provide some protected resources, for demonstration, it only returns parsed token value.
Like in the previous post initialize two applications via start.spring.io. After that, we copy all the code associated with token generation and user authentication to auth-service and all code associated with token validation and “/me” endpoint to resource-service.
The resulting directory tree in resource-service:

and for auth-service:

Resource service
Resource service contains only one controller, which provides “/me” endpoint:
@RestController @RequestMapping("/") class AuthenticationController( private val tokenBuilder: TokenBuilder ) { @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) } }
the main difference is in the config file application.yml:
app.version: v1 spring: security: oauth2: resourceserver: jwt: jwk-set-uri: <http://localhost:8484/.well-known/jwks.json>
We set URI for loading JWK set for token validation. All work for token validation and key loading is made by spring-security.
Authentication service
The main difference from the application in the previous post is a key generation on server startup. For this, we modify KeyProvider and KeyProviderImpl.
In KeyProvider we just add two new functions:
import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey interface KeyProvider { fun getPrivateKey(): RSAPrivateKey fun getPublicKey(): RSAPublicKey fun getKeyId(): String }
and in implementation, we remove code that loads the key from the “.pem” file and adds code for a keyPair generation. The full source of KeyProviderImpl:
@Component class KeyProviderImpl(private val jwtProperties: JwtProperties) : KeyProvider { private lateinit var keyPair: KeyPair private lateinit var keyId: UUID @PostConstruct fun afterPropertiesSet(): Unit { log.info("Loaded properties: $jwtProperties") keyPair = generateKeyPair(); keyId = UUID.randomUUID(); } private fun generateKeyPair(): KeyPair { Security.addProvider(BouncyCastleProvider()) val keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM, "BC") keyPairGenerator.initialize(KEY_SIZE, SecureRandom()) return keyPairGenerator.generateKeyPair() } override fun getPrivateKey(): RSAPrivateKey = keyPair.private as RSAPrivateKey override fun getPublicKey(): RSAPublicKey = keyPair.public as RSAPublicKey override fun getKeyId(): String = keyId.toString() companion object { @Suppress("JAVA_CLASS_ON_COMPANION") @JvmStatic private val log = LoggerFactory.getLogger(javaClass.enclosingClass) @JvmStatic val ALGORITHM = "RSA" @JvmStatic val KEY_SIZE = 2048 } }
Instead of loading the key from the “.pem” file, the application generates key pairs during startup. The benefits of this approach are:
- No needed copying public key to all microservices in our system
- Key pair could be generated periodically, that improves application security
- We could switch to another authentication service, i.e. “Keycloak” (https://www.keycloak.org), with minimal changes
Also, we need to provide an endpoint for the JWK key set, see JwkSetRestController below:
@RestController @RequestMapping("/.well-known/") class JwkSetRestController(private val jwkSetResolver: JwkSetResolver) { @GetMapping("jwks.json") fun getKeys(): MutableMap<String, Any>? { return jwkSetResolver.getJwkSet().toJSONObject(true); } }
JwkSetResolver uses “nimbus” for key set creating from RsaPublicKey:
@Service class JwkSetResolverImpl(private val keyProvider: KeyProvider) : JwkSetResolver { private lateinit var jwkSet: JWKSet @PostConstruct fun afterPropertiesSet(): Unit { jwkSet = buildJwkSet() } private fun buildJwkSet(): JWKSet { val builder: RSAKey.Builder = RSAKey.Builder(keyProvider.getPublicKey()) .keyUse(KeyUse.SIGNATURE) .algorithm(JWSAlgorithm.RS256) .keyID(keyProvider.getKeyId()) return JWKSet(builder.build()) } override fun getJwkSet(): JWKSet = jwkSet companion object { @Suppress("JAVA_CLASS_ON_COMPANION") @JvmStatic private val log = LoggerFactory.getLogger(javaClass.enclosingClass) } }
Of course, we need to provide key id in our token, change TokenBuilder:
@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) .setHeaderParam("kid", keyProvider.getKeyId()) .signWith(getKey(), SignatureAlgorithm.RS256) .setExpiration(expiration) .compact() } private fun getExpiration(): Date = Date(System.currentTimeMillis() + tokenValidityInSeconds * 1000) private fun getKey() = keyProvider.getPrivateKey() private fun buildClaims(authorities: List<String>): Map<String, Any> { val claims = HashMap<String, Any>() claims["authorities"] = authorities return claims } }
The main difference from the previous version is setting key id param to the token header:
setHeaderParam("kid", keyProvider.getKeyId())
Let’s test our application. Run both applications and run test requests from IntelliJ Idea:
### POST <http://localhost:8484/login> Content-Type: application/json { "userName": "admin", "password": "adminPassword" } > {% client.global.set("auth_token", response.body.token); %} ### GET <http://localhost:8080/me> Content-Type: application/json Authorization: Bearer {{auth_token}}
Result of the last request:
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, 28 Oct 2020 06:23:52 GMT Keep-Alive: timeout=60 Connection: keep-alive { "tokenValue": "eyJraWQiOiJkMzhiMzk2OS05MWU0LTQwNzMtYmUwZC01MTVhOTNjOGRhYzQiLCJhbGciOiJSUzI1NiJ9.eyJhdXRob3JpdGllcyI6WyJVU0VSIiwiQURNSU4iXSwianRpIjoiYTBiNDQ2MTAtMDNjOC00NGYxLThjZmUtMmEyODg2ODU3NGY4Iiwic3ViIjoiYWRtaW4iLCJleHAiOjE2MDM4NjYyNTR9.NKb7QtDC2vHnq0uPfW1w5khaNqg23rspkxwcXRYFAH836u-4u0BHwaiKAOZIuJb8-tb8zK3vZW_b3jMQaHJh562m6kNdDDOSnmn1zV7LbfnFwLXA-hGNI2tbQHazZquJViTTqHd_dxoB1Wt1V3Bu_YkqlHL8nIfG--cqStqxqh0xWopyyT2iPnB22n-e1xdrs4OziKzSXUzf3MYLFby1vK0JR2qB06IOq7S7UCqwTmI9-P14VuA_z3fCEvqg1jc09DMlzAWxfMyVpnuY_gu9pXqZgmQw_knMlx6lar6lkYCJNhzy1iM9no0oJ5nVxBjduEwH9IWn9rt2X8sdb4G7BQ", "issuedAt": "2020-10-28T06:24:13Z", "expiresAt": "2020-10-28T06:24:14Z", "headers": { "kid": "d38b3969-91e4-4073-be0d-515a93c8dac4", "alg": "RS256" }, "claims": { "sub": "admin", "exp": "2020-10-28T06:24:14Z", "iat": "2020-10-28T06:24:13Z", "authorities": [ "USER", "ADMIN" ], "jti": "a0b44610-03c8-44f1-8cfe-2a28868574f8" }, "id": "a0b44610-03c8-44f1-8cfe-2a28868574f8", "issuer": null, "subject": "admin", "audience": null, "notBefore": null } Response code: 200; Time: 340ms; Content length: 993 bytes
If we restart services, keys and key-id will be generated soon. And we couldn’t verify our previous token.
The result after restarting services, with the previous version of token:
GET [<http://localhost:8080/me>](<http://localhost:8080/me>) HTTP/1.1 401 WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Signed JWT rejected: Invalid signature", error_uri="[<https://tools.ietf.org/html/rfc6750#section-3.1>](<https://tools.ietf.org/html/rfc6750#section-3.1>)" 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-Length: 0 Date: Tue, 27 Oct 2020 06:56:02 GMT Keep-Alive: timeout=60 Connection: keep-alive
Thanks! Happy coding!
[…] this post, we switch our resource service from the previous post(https://akazakov.dev/?p=47) to the keycloak authorization server (https://www.keycloak.org). All source code is available at […]