Use OAuth 2.0 Token Introspection during log in ()

Currently, it uses an endpoint similar to OpenID Connect UserInfo but with some differences. The endpoint does not require the "openid" scope for example. There is an ongoing effort to replace the OAuth 2.0 authorization server with a more standard compliant one which would break the endpoint (since it would require the "openid" scope). It is currently not possible to request the "openid" scope to future-proof since Spring would act differently if that scope is present and assume full OpenID Connect. That leads to requiring an id token to have been issued which the current authorization server does not do.

To get around this the implementation is changed to use a standard compliant Token Introspection endpoint to get access to the subject of the access token (which is the only part that's necessary right now). Since the endpoint is standard compliant it will work with any future authorization server.

It may be necessary to run `docker compose up --build` to get the latest version of the Toker containers.

Reviewed-on: 
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
Co-authored-by: Andreas Svanberg <andreass@dsv.su.se>
Co-committed-by: Andreas Svanberg <andreass@dsv.su.se>
This commit is contained in:
Andreas Svanberg 2025-03-25 08:45:25 +01:00 committed by Nico Athanassiadis
parent 9fa699ed83
commit e95421b8f2
4 changed files with 60 additions and 2 deletions

@ -15,7 +15,7 @@ services:
- JDBC_DATABASE_PASSWORD=scipro
- OAUTH2_AUTHORIZATION_URI=https://oauth2-${VHOST}/authorize
- OAUTH2_TOKEN_URI=https://oauth2-${VHOST}/exchange
- OAUTH2_USER_INFO_URI=https://oauth2-${VHOST}/verify
- OAUTH2_USER_INFO_URI=https://oauth2-${VHOST}/introspect
- OAUTH2_CLIENT_ID=scipro_client
- OAUTH2_CLIENT_SECRET=scipro_secret
- OAUTH2_RESOURCE_SERVER_ID=scipro_api_client

@ -0,0 +1,42 @@
package se.su.dsv.scipro.war;
import java.net.URI;
import java.util.Collections;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;
public class TokenIntrospectionRequestEntityConverter implements Converter<OAuth2UserRequest, RequestEntity<?>> {
private static final MediaType FORM_URL_ENCODED = MediaType.valueOf(
MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"
);
@Override
public RequestEntity<?> convert(OAuth2UserRequest userRequest) {
ClientRegistration clientRegistration = userRequest.getClientRegistration();
URI uri = UriComponentsBuilder.fromUriString(
clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()
)
.build()
.toUri();
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
headers.setAccept(Collections.singletonList(MediaType.ALL));
headers.setContentType(FORM_URL_ENCODED);
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
formParameters.add(OAuth2ParameterNames.TOKEN, userRequest.getAccessToken().getTokenValue());
return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
}
}

@ -14,6 +14,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.web.SecurityFilterChain;
import se.su.dsv.scipro.SciProApplication;
import se.su.dsv.scipro.crosscutting.ForwardPhase2Feedback;
@ -67,6 +68,21 @@ public class WicketConfiguration {
return http.build();
}
// Stop gap measure to switch to Token Introspection instead of OIDC UserInfo
// endpoint. This is necessary because the UserInfo endpoint will in soon require
// the "openid" scope, which is not granted to our clients. Unfortunately we can't
// request the scope because that makes Spring require an id token in the token
// exchange which is not granted at the moment.
//
// Once a new authorization server is in place we can remove this bean and use
// straight up id tokens with "openid" scope.
@Bean
public DefaultOAuth2UserService defaultOAuth2UserService() {
DefaultOAuth2UserService defaultOAuth2UserService = new DefaultOAuth2UserService();
defaultOAuth2UserService.setRequestEntityConverter(new TokenIntrospectionRequestEntityConverter());
return defaultOAuth2UserService;
}
@Bean
public CurrentUserFromSpringSecurity currentUserFromSpringSecurity(
UserService userService,

@ -22,7 +22,7 @@ spring.security.oauth2.resourceserver.opaquetoken.client-secret=${OAUTH2_RESOURC
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=${OAUTH2_RESOURCE_SERVER_INTROSPECTION_URI:http://localhost:59733/introspect}
# Log in via local OAuth 2 authorization server
spring.security.oauth2.client.provider.docker.user-info-uri=${OAUTH2_USER_INFO_URI:http://localhost:59734/verify}
spring.security.oauth2.client.provider.docker.user-info-uri=${OAUTH2_USER_INFO_URI:http://localhost:59734/introspect}
spring.security.oauth2.client.provider.docker.user-name-attribute=sub
spring.security.oauth2.client.provider.docker.token-uri=${OAUTH2_TOKEN_URI:http://localhost:59734/exchange}
spring.security.oauth2.client.provider.docker.authorization-uri=${OAUTH2_AUTHORIZATION_URI:http://localhost:59734/authorize}