I need to setup a legacy Spring-Boot 1.5 project to act as a OAuth2 Resource Server using the exposed JWKS to validate all the incoming JWTs at each request.
The links and articles that I have followed/checked are:
- https://projects.spring.io/spring-security-oauth/docs/oauth2.html
- https://www.baeldung.com/spring-security-oauth2-jws-jwk
- Spring Security OAuth with JWK Example
I have the following dependencies in my pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.22.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>project</groupId>
<artifactId>project-resource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Project Resource</name>
<description>Project Resource</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<!--https://projects.spring.io/spring-security-oauth/docs/oauth2.html-->
<!--https://projects.spring.io/spring-security-oauth/docs/oauth2.html#resource-server-configuration-->
<!--https://github.com/spring-projects/spring-security-oauth/tree/master/tests/annotation/resource-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.5.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
I configured it to act as a Resource Server and use the JWKS uri in the application.yml:
security:
oauth2:
resource:
jwk:
key-set-uri: http://sso.server/.well-known/jwks.json
The main class is as simple as:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
@SpringBootApplication
@EnableResourceServer
public class ProjectResourceApplication {
public static void main(String[] args) {
SpringApplication.run(ProjectResourceApplication.class, args);
}
}
In order to use the APIs from another domain I also had to provide this CorsFilter,
since the simple httpSecurity.cors() didn't work.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CORSFilter extends OncePerRequestFilter {
@Value("${custom.cors.allowOrigin:*}")
private String allowOrigin;
@Value("${custom.cors.allowMethods:GET, POST, PUT, DELETE, OPTIONS}")
private String allowMethods;
@Value("${custom.cors.allowHeaders:Content-Type,Authorization}")
private String allowHeaders;
@Value("${custom.cors.allowCredentials:true}")
private String allowCredentials;
@Value("${custom.cors.maxAge:3600}")
private String maxAge;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
response.addHeader("Access-Control-Allow-Origin", allowOrigin);
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
response.addHeader("Access-Control-Allow-Methods", allowMethods);
response.addHeader("Access-Control-Allow-Headers", allowHeaders);
response.addHeader("Access-Control-Allow-Credentials", allowCredentials);
response.addHeader("Access-Control-Max-Age", maxAge);
response.setStatus(HttpServletResponse.SC_OK);
} else {
filterChain.doFilter(request, response);
}
}
}
The authorization is working fine, meaning that the token is validated correctly with the JWKS, however the Principal is always null.
e.g.:
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
public class PrincipalController {
@GetMapping("/principal")
public Principal getPrincipal(@AuthenticationPrincipal Principal principal) {
return principal;
}
}
Is there something wrong or missing? In addition I would also like to have a custom object as Principal if possible. Does the Rosource Server require to also have info about the client in order to provide the Principal? as found at https://github.com/spring-projects/spring-security-oauth/issues/1012