nurgasemetey
nurgasemetey

Reputation: 770

Spring Boot OAuth2 Resource Server: How library can verify JWT token without public key?

I have following spring boot app with minimal configuration

application.properties

server.port=8081
spring.security.oauth2.resourceserver.jwt.issuer-uri = http://localhost:8080/auth/realms/master

pom.xml

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.nurgasemetey</groupId>
    <artifactId>springboot-keycloak</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-keycloak</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>

controller

@RestController
@RequestMapping("/users")
public class UsersController {
    @GetMapping("/status/check")
    public String status(@AuthenticationPrincipal Jwt principal) {
        return "working";
    }
}

It seems that Spring Boot Oauth2 doesn't use public key, as I see in code:

OAuth2ResourceServerProperties

/**
         * JSON Web Key URI to use to verify the JWT token.
         */
        private String jwkSetUri;

        /**
         * JSON Web Algorithm used for verifying the digital signatures.
         */
        private String jwsAlgorithm = "RS256";

        /**
         * URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0
         * Authorization Server Metadata endpoint defined by RFC 8414.
         */
        private String issuerUri;

        /**
         * Location of the file containing the public key used to verify a JWT.
         */
        private Resource publicKeyLocation;

But I didn't give publicKeyLocation, but app can verify without public key.

Under the hood it uses JwtIssuerValidator and JwtTimestampValidator validators.

On other hand, with express-jwt, it requires public key for offline verification

const express = require('express');
const jwt = require('express-jwt');
const app = express();
const secret = 'secret';
const fs = require('fs');
var publicKey = fs.readFileSync('public.pub');


app.get('/protected', jwt({ secret: publicKey, algorithms: ['RS256'] }), (req, res) => {
  res.send('protected');
})
app.listen(3000, () => console.log('server started'));

How the Spring Boot Oauth verifies without public key?

Upvotes: 1

Views: 4467

Answers (1)

nurgasemetey
nurgasemetey

Reputation: 770

Self answer.

Firstly, it seems that http://localhost:8080/auth/realms/master exposes public key. As said in this Generate JWT Token in Keycloak and get public key to verify the JWT token on a third party platform - Stack Overflow and in this comment to this question by @Thomas Kåsene

Secondly, I digged spring boot oauth2 code and stumbled to this code in

ReactiveOAuth2ResourceServerJwkConfiguration

@Bean
        @Conditional(IssuerUriCondition.class)
        ReactiveJwtDecoder jwtDecoderByIssuerUri() {
            return ReactiveJwtDecoders.fromIssuerLocation(this.properties.getIssuerUri());
        }

JwtDecoderProviderConfigurationUtils

private static Map<String, Object> getConfiguration(String issuer, URI... uris) {
        String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " +
                "\"" + issuer + "\"";
        for (URI uri : uris) {
            try {
                RequestEntity<Void> request = RequestEntity.get(uri).build();
                ResponseEntity<Map<String, Object>> response = rest.exchange(request, typeReference);
                Map<String, Object> configuration = response.getBody();

                if (configuration.get("jwks_uri") == null) {
                    throw new IllegalArgumentException("The public JWK set URI must not be null");
                }

                return configuration;
            } catch (IllegalArgumentException e) {
                throw e;
            } catch (RuntimeException e) {
                if (!(e instanceof HttpClientErrorException &&
                        ((HttpClientErrorException) e).getStatusCode().is4xxClientError())) {
                    throw new IllegalArgumentException(errorMessage, e);
                }
                // else try another endpoint
            }
        }
        throw new IllegalArgumentException(errorMessage);
    }

which seems to fetch public key from issuer-uri given in application.properties. After it fetched it verifies jwt tokens with fetched public key.

To test

Close your jwt provider, keycloak in my case and run spring boot application, then it gives

Caused by: java.lang.IllegalArgumentException: Unable to resolve the Configuration with the provided Issuer of "http://localhost:8080/auth/realms/master"

Upvotes: 1

Related Questions