I am trying to learning spring security and created a sample project where an angular based UI is authenticated using OIDC with PKCE through springboot. I am authenticating users with Auth0 following this tutorial https://docs.spring.io/spring-authorization-server/reference/guides/how-to-social-login.html from spring. I am able to login correctly, but the access and id tokens that is generated always fail the signature validation. Is there anything I am missing in my configure files that may cause the signature to not fail?
application.yaml
server:
port: 9000
auth:
client: <insert auth0 address here>
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${auth.client}
jwk-set-uri: ${auth.client}/.well-known/jwks.json
client:
registration:
my-client:
provider: test
authorization-grant-type: authorization_code
redirect-uri: http://localhost:9000/login/oauth2/code/my-client
client-authentication-method: none
client-id: xxxx
client-secret: yyyyyy
scope:
- openid
- profile
- email
- read.data
provider:
test:
issuer-uri: ${auth.client}/
authorization-uri: ${auth.client}/authorize
token-uri: ${auth.client}/oauth/token
user-info-uri: ${auth.client}/userinfo
user-name-attribute: sub
jwk-set-uri: ${auth.client}/.well-known/jwks.json
authorizationserver:
client:
- my-client:
registration:
client-id: "public-client"
client-authentication-methods:
- "none"
authorization-grant-types:
- "authorization_code"
redirect-uris:
- "http://localhost:4200"
scopes:
- "openid"
- "profile"
- "offline_access"
require-authorization-consent: true
require-proof-key: true
token:
access-token-time-to-live: 600s
SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http
// Redirect to the OAuth 2.0 Login endpoint when not authenticated
// from the authorization endpoint
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/my-client"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
return http.cors(Customizer.withDefaults()).build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2Login(Customizer.withDefaults());
return http.cors(Customizer.withDefaults()).build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedOrigin("http://127.0.0.1:4200");
config.addAllowedOrigin("http://localhost:4200");
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return source;
}
}
Here is an example access token
eyJraWQiOiJmOGI2OWFmNi1jYmE1LTQ1YzAtOGY4NC1kNWMzMDI3ZjQzZDUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhdXRoMHw2MjA1YzJjYzY5NDVmOTAwNmI1Yzg1YWMiLCJhdWQiOiJwdWJsaWMtY2xpZW50IiwibmJmIjoxNzA1MTU3ODIwLCJzY29wZSI6WyJvcGVuaWQiLCJwcm9maWxlIiwib2ZmbGluZV9hY2Nlc3MiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDAwIiwiZXhwIjoxNzA1MTU4NDIwLCJpYXQiOjE3MDUxNTc4MjAsImp0aSI6ImNjMzRlZTRkLTNmYzQtNGRiZi05YTI2LTBlNmNiNjg5YzMwZiJ9.TUNQLWUYnKjn8XCfyQRtkDLXbt4PBKheP9u60t8by_5C6318NRUkfxJlAh2ye5Gv2ZMWQDjXbTOznB0W5IsMA8x67oq49QRpmHYW_Gd23968DGo00XHAt4GEQaR7COh-Tm7yRwp3ihIwGPWDffYT0bTUrCwzEH8OpYu26CSXzyTa7Cc7VcuTnf1qlRysI2j-jwyIkTM-V8JppffIcuTz-r0dRbxsi4tYGNEvmRghY-QNuApc_BekVr0pOq9rMblaUOMz2zOcoCN0O1ty1XXb-6hLWUemOO3Q7Zdj4N-s6cx42EbFv6L_fsj7oYCYq-Pzt-hzLCDRQfTpYf4-TD9ccw
Here is the results from the local host jwk endpoint
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "f8b69af6-cba5-45c0-8f84-d5c3027f43d5",
"n": "s6z7mFu60eiD5zAgpiz6rL2ZsdayRf5xM95BHloWD_xz9n56l6oBX82gXpkF6x4rY9f6OKcCXTFEM37Bu_xvVxrkyAfkZ0XGvJYQv6glrbJWkV7Qfl3H7dWmjO57wWWLn7_Hr9imx-T-y9VuPAKKkK_c2L-yzd8zj1dYf1hmzKnOwK46mIDZ86Q28udNz-pHjcj3RxTjd4f1u4YKh4a66A8EJRTDC4iJ6wjutbggl3Aywb1RNksp1HlmEl2ifHm51Jw4guPEUSo2KnZajN8qBqUBmLMq1rcHOIM6IMsvFsrto-ZESwvgKJobcGIVFystPbEljA0pEbtK2UZTVWCi0Q"
}
]
}
Here is the jwk endpoint from auth0
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"n": "nGXHKrVaXqj1jOgZ37FD4BIVYlhvL-CGflccHEczP7vtYfArkNOVjW4V4DY1zQNHd9da5WKdVuuEzjsAC_E74u7u6zTJVhLC4reYCPqClkK-fxLRlRSeGTRbEjXxk4hJZ2bdEnzfM_RK8mcxLgSDF4jar45atWHvebsaI8LdzxnaOLqA2aQbKQbhPnGnngdR6kdZTWSBq-Kkmh73r4hZ0Nm9cKgA7q9v06Uq1XGiEqgDlCn3_dSDaUaZKJvrDSOMtftv71O15mDrC8bJLufF_oROOTrSqk-9LMBnjO5vUSK4A4i-JjR4hQnAZvryVW6RyqUiEkaU6lWR_miwAqZytw",
"e": "AQAB",
"kid": "MMBwle23wTQWIvx-CW55l",
"x5t": "OmYvzIheoHg5Ik2y2c7iXbxxpwU",
"x5c": [
"MIIDDTCCAfWgAwIBAgIJC0XZSRME3+GKMA0GCSqGSIb3DQEBCwUAMCQxIjAgBgNVBAMTGWRldi03d2EwaHptcC51cy5hdXRoMC5jb20wHhcNMjIwMjA4MjI0NzE3WhcNMzUxMDE4MjI0NzE3WjAkMSIwIAYDVQQDExlkZXYtN3dhMGh6bXAudXMuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnGXHKrVaXqj1jOgZ37FD4BIVYlhvL+CGflccHEczP7vtYfArkNOVjW4V4DY1zQNHd9da5WKdVuuEzjsAC/E74u7u6zTJVhLC4reYCPqClkK+fxLRlRSeGTRbEjXxk4hJZ2bdEnzfM/RK8mcxLgSDF4jar45atWHvebsaI8LdzxnaOLqA2aQbKQbhPnGnngdR6kdZTWSBq+Kkmh73r4hZ0Nm9cKgA7q9v06Uq1XGiEqgDlCn3/dSDaUaZKJvrDSOMtftv71O15mDrC8bJLufF/oROOTrSqk+9LMBnjO5vUSK4A4i+JjR4hQnAZvryVW6RyqUiEkaU6lWR/miwAqZytwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTUixf7gM1eEMvzY05IVpq33NpOMDAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBABxW2jufvaRzLMFqkRZloAGTiEqpeIDbuoBtd2rzyirsiD2z6z7e4x5ERwBxRVki1AQsqxsezCrqc0EeE4mDxqWvxEJd9wAQKRSgptmMiCC+3ACtHKm6sAGvzpCJt5dAFvqQc1RiPV9nOTHG4VbWbx3EK6jCocMQu/oJfDmGe+V5A/Uz43bLRVJxXG/QqiF6T6paRM+l0j6nxP3WU30vX5NyCILEYabnUHOENMmEaVoarGogwqIv/mb5Rd+RQ14Ab1PYBtvjFF4sSX1oxmAJR+NpZx646JabUg3OmFRUdZKPaS/Sqn7YwGuBtlyMTA1ek/SqLxSoDN91rFlXpFITupI="
],
"alg": "RS256"
},
{
"kty": "RSA",
"use": "sig",
"n": "yad3J942bXm02rmBBQ5zuekX7j7cxUugPCnfJukUxQurzpkmxAcfCaXhzzwNjqlRdzd8BVyTwzGfBxdBKJCcor-_6a_D2B8p9B_5-3aCIzrf0zWer9gs_K1E-FXynHnGxXtx3RuddsQXyc423bIFKbq3TJcpVZrubMQPAkJfnIxaVx9_iB4obZYxUW70Gd8dUHcChExBgZZWipy0Tn5FIQUpNxT2_oR7PGqWG2DQW3f08Om21dQha-RiCVMSSQ_8nXjEsYR6r7R8EjCQhukv7B-PsX4vgpjVo8tOCzvfuTeLlwWTP07zGUw0ZTTWm4_v1GTIoYY-5YqU7iuu_AjAOw",
"e": "AQAB",
"kid": "hmQrSAlxu9ngNAlLkPeqY",
"x5t": "gUenOFsCzw4z9VJVIrjuqI4T_UM",
"x5c": [
"MIIDDTCCAfWgAwIBAgIJbHLvlGpDKY0SMA0GCSqGSIb3DQEBCwUAMCQxIjAgBgNVBAMTGWRldi03d2EwaHptcC51cy5hdXRoMC5jb20wHhcNMjIwMjA4MjI0NzE3WhcNMzUxMDE4MjI0NzE3WjAkMSIwIAYDVQQDExlkZXYtN3dhMGh6bXAudXMuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyad3J942bXm02rmBBQ5zuekX7j7cxUugPCnfJukUxQurzpkmxAcfCaXhzzwNjqlRdzd8BVyTwzGfBxdBKJCcor+/6a/D2B8p9B/5+3aCIzrf0zWer9gs/K1E+FXynHnGxXtx3RuddsQXyc423bIFKbq3TJcpVZrubMQPAkJfnIxaVx9/iB4obZYxUW70Gd8dUHcChExBgZZWipy0Tn5FIQUpNxT2/oR7PGqWG2DQW3f08Om21dQha+RiCVMSSQ/8nXjEsYR6r7R8EjCQhukv7B+PsX4vgpjVo8tOCzvfuTeLlwWTP07zGUw0ZTTWm4/v1GTIoYY+5YqU7iuu/AjAOwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRSZQM0uQyFnmwU3W+DYUc7z4nQyjAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAF3Psiqk32Dq10WEtT5T2UIxczim6pKGv+edH5yPxDLlfJjnFNK7PMMQS7whyYrMzMZoJv4VUlNvQ3hkg1ICvYzwGDM4kNgRP7TTOQqgomCslpUnqsLNjVwBJBqC0XpMcmldcUXbYaXwUN73PiEkjFDNjjezQnJlO7o3CFySDqaahKevHXoTbex5RjwnrjziC/pVyjlruaEFZUhrAsi+jTQ8NUkPWj2APtU3WhMAYGAvccd9CXl3CSQXRUVtd8yTV7z8YNdG3MAx/JSrpB/m2ghrRUhML7Jn8io4HY0iP4fbQ46FAjLAdWDDDf+IupqEysOegdnuok6dY61qv750C4A="
],
"alg": "RS256"
}
]
}
If you don't pass in an audience
parameter to the authorize
endpoint, Auth0 will return an opaque access token rather than a JWT. You can configure Spring Security pass in an audience parameter as follows:
package com.example.apigateway;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.web.SecurityFilterChain;
import java.util.function.Consumer;
@Configuration
public class SecurityConfiguration {
@Value("${okta.oauth2.audience}")
private String audience;
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfiguration(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
.authorizationRequestResolver(
authorizationRequestResolver(this.clientRegistrationRepository)
)
)
);
return http.build();
}
private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
ClientRegistrationRepository clientRegistrationRepository) {
DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
new DefaultOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, "/oauth2/authorization");
authorizationRequestResolver.setAuthorizationRequestCustomizer(
authorizationRequestCustomizer());
return authorizationRequestResolver;
}
private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
return customizer -> customizer
.additionalParameters(params -> params.put("audience", audience));
}
}
See this issue if you need a WebFlux version.