From b976fb7562afc7ebc7ff88167bcec21dc0bc0a58 Mon Sep 17 00:00:00 2001 From: u221711 Date: Mon, 17 Jun 2024 10:47:46 +0200 Subject: [PATCH] feat: Add requestToken to CustomClaimController --- playground-backend/pom.xml | 15 +++ .../config/ApplicationConfiguration.java | 102 ++++++++++++++++++ .../config/SwaggerConfiguration.java | 61 +++++++++++ ...eJwtBearerGrantRequestEntityConverter.java | 16 +++ .../config/WebSecurityConfiguration.java | 35 ++++++ .../controller/CustomClaimController.java | 52 ++++++++- .../src/main/resources/application.yaml | 32 +++++- 7 files changed, 308 insertions(+), 5 deletions(-) create mode 100644 playground-backend/src/main/java/ch/sbb/playgroundbackend/config/ApplicationConfiguration.java create mode 100644 playground-backend/src/main/java/ch/sbb/playgroundbackend/config/SwaggerConfiguration.java create mode 100644 playground-backend/src/main/java/ch/sbb/playgroundbackend/config/TokenExchangeJwtBearerGrantRequestEntityConverter.java create mode 100644 playground-backend/src/main/java/ch/sbb/playgroundbackend/config/WebSecurityConfiguration.java diff --git a/playground-backend/pom.xml b/playground-backend/pom.xml index b62982c5..542a669c 100644 --- a/playground-backend/pom.xml +++ b/playground-backend/pom.xml @@ -62,6 +62,21 @@ spring-boot-starter-actuator + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + org.springdoc springdoc-openapi-starter-webmvc-ui diff --git a/playground-backend/src/main/java/ch/sbb/playgroundbackend/config/ApplicationConfiguration.java b/playground-backend/src/main/java/ch/sbb/playgroundbackend/config/ApplicationConfiguration.java new file mode 100644 index 00000000..52b47477 --- /dev/null +++ b/playground-backend/src/main/java/ch/sbb/playgroundbackend/config/ApplicationConfiguration.java @@ -0,0 +1,102 @@ +package ch.sbb.playgroundbackend.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; +import org.springframework.security.oauth2.client.endpoint.DefaultJwtBearerTokenResponseClient; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtClaimValidator; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtIssuerValidator; +import org.springframework.security.oauth2.jwt.JwtTimestampValidator; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +@Configuration +public class ApplicationConfiguration { + + private final OAuth2ResourceServerProperties.Jwt properties; + + // The audience is important because the JWT token is accepted only if the aud claim in the JWT token received by the server is the same as the client ID of the server. + @Value("${spring.security.oauth2.resourceserver.jwt.audience}") + String audience; + + public ApplicationConfiguration(OAuth2ResourceServerProperties properties) { + this.properties = properties.getJwt(); + } + + @Bean + JwtDecoder jwtDecoder() { + NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(properties.getJwkSetUri()).build(); + nimbusJwtDecoder.setJwtValidator(jwtValidator()); + return nimbusJwtDecoder; + } + + @Bean + public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .provider(jwtBearerOAuth2AuthorizedClientProvider()) + .build(); + DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + return authorizedClientManager; + } + + private JwtBearerOAuth2AuthorizedClientProvider jwtBearerOAuth2AuthorizedClientProvider() { + JwtBearerOAuth2AuthorizedClientProvider provider = new JwtBearerOAuth2AuthorizedClientProvider(); + provider.setAccessTokenResponseClient(oAuth2AccessTokenResponseClient()); + return provider; + } + + private DefaultJwtBearerTokenResponseClient oAuth2AccessTokenResponseClient() { + DefaultJwtBearerTokenResponseClient client = new DefaultJwtBearerTokenResponseClient(); + client.setRequestEntityConverter(new TokenExchangeJwtBearerGrantRequestEntityConverter()); + return client; + } + + private OAuth2TokenValidator jwtValidator() { + List> validators = new ArrayList<>(); + String issuerUri = properties.getIssuerUri(); + if (StringUtils.hasText(issuerUri)) { + validators.add(new JwtIssuerValidator(issuerUri)); + } + if (StringUtils.hasText(audience)) { + validators.add(new JwtClaimValidator<>(JwtClaimNames.AUD, audiencePredicate(audience))); + } + validators.add(new JwtTimestampValidator()); + return new DelegatingOAuth2TokenValidator<>(validators); + } + + Predicate audiencePredicate(String audience) { + return aud -> { + if (aud == null) { + return false; + } else if (aud instanceof String) { + return aud.equals(audience); + } else if (aud instanceof List) { + return ((List) aud).contains(audience); + } else { + return false; + } + }; + } + +} diff --git a/playground-backend/src/main/java/ch/sbb/playgroundbackend/config/SwaggerConfiguration.java b/playground-backend/src/main/java/ch/sbb/playgroundbackend/config/SwaggerConfiguration.java new file mode 100644 index 00000000..0bb40401 --- /dev/null +++ b/playground-backend/src/main/java/ch/sbb/playgroundbackend/config/SwaggerConfiguration.java @@ -0,0 +1,61 @@ +package ch.sbb.playgroundbackend.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.OAuthFlow; +import io.swagger.v3.oas.models.security.OAuthFlows; +import io.swagger.v3.oas.models.security.Scopes; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Collections; + +@Configuration +public class SwaggerConfiguration { + + private static final String OAUTH_2 = "oauth2"; + + @Value("${info.app.version}") + private String applicationVersion; + + @Value("${springdoc.swagger-ui.oauth.clientId}") + private String clientId; + + @Value("${spring.security.oauth2.authorizationUrl}") + private String authorizationUrl; + + @Bean + public OpenAPI gleisspiegelOpenAPIConfiguration() { + return new OpenAPI() + .components(new Components().addSecuritySchemes(OAUTH_2, addOAuthSecurityScheme())) + .security(Collections.singletonList(new SecurityRequirement().addList(OAUTH_2))) + .info(apiInfo()); + } + + private Info apiInfo() { + final String versionInformation = StringUtils.isNotBlank(applicationVersion) ? " v " + applicationVersion : ""; + return new Info() + .title("Playground Backend" + versionInformation) + .contact(new Contact() + .name("Team Zug") + .url("https://github.com/SchweizerischeBundesbahnen/DAS")); + } + + private SecurityScheme addOAuthSecurityScheme() { + final Scopes scopes = new Scopes().addString(clientId + "/.default", "Global access"); + + final OAuthFlows flowAuthorizationCode = new OAuthFlows().authorizationCode(new OAuthFlow() + .authorizationUrl(authorizationUrl + "/authorize") + .tokenUrl(authorizationUrl + "/token") + .scopes(scopes)); + + return new SecurityScheme().name(OAUTH_2).type(SecurityScheme.Type.OAUTH2).flows(flowAuthorizationCode); + } + +} diff --git a/playground-backend/src/main/java/ch/sbb/playgroundbackend/config/TokenExchangeJwtBearerGrantRequestEntityConverter.java b/playground-backend/src/main/java/ch/sbb/playgroundbackend/config/TokenExchangeJwtBearerGrantRequestEntityConverter.java new file mode 100644 index 00000000..1a6b133b --- /dev/null +++ b/playground-backend/src/main/java/ch/sbb/playgroundbackend/config/TokenExchangeJwtBearerGrantRequestEntityConverter.java @@ -0,0 +1,16 @@ +package ch.sbb.playgroundbackend.config; + +import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; +import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequestEntityConverter; +import org.springframework.util.MultiValueMap; + +public class TokenExchangeJwtBearerGrantRequestEntityConverter extends JwtBearerGrantRequestEntityConverter { + + @Override + protected MultiValueMap createParameters(JwtBearerGrantRequest jwtBearerGrantRequest) { + MultiValueMap parameters = super.createParameters(jwtBearerGrantRequest); + parameters.add("requested_token_use", "on_behalf_of"); + return parameters; + } + +} diff --git a/playground-backend/src/main/java/ch/sbb/playgroundbackend/config/WebSecurityConfiguration.java b/playground-backend/src/main/java/ch/sbb/playgroundbackend/config/WebSecurityConfiguration.java new file mode 100644 index 00000000..40ea4d28 --- /dev/null +++ b/playground-backend/src/main/java/ch/sbb/playgroundbackend/config/WebSecurityConfiguration.java @@ -0,0 +1,35 @@ +package ch.sbb.playgroundbackend.config; + +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.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfiguration { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authConfig) -> { + authConfig.requestMatchers("/swagger-ui/**").permitAll(); + authConfig.requestMatchers("/v3/api-docs/**").permitAll(); + authConfig.requestMatchers("/actuator/health/*").permitAll(); + authConfig.requestMatchers("/actuator/info").permitAll(); + authConfig.requestMatchers("/customClaim").permitAll(); + authConfig.requestMatchers("/**").authenticated(); + } + ) + // Disable csrf for now as it makes unauthenticated requests return 401/403 + .csrf(AbstractHttpConfigurer::disable) + .oauth2ResourceServer((oauth2) -> + oauth2.jwt(withDefaults()) + ); + return http.build(); + } +} diff --git a/playground-backend/src/main/java/ch/sbb/playgroundbackend/controller/CustomClaimController.java b/playground-backend/src/main/java/ch/sbb/playgroundbackend/controller/CustomClaimController.java index 6aa6b18c..3b99ff41 100644 --- a/playground-backend/src/main/java/ch/sbb/playgroundbackend/controller/CustomClaimController.java +++ b/playground-backend/src/main/java/ch/sbb/playgroundbackend/controller/CustomClaimController.java @@ -5,32 +5,76 @@ import ch.sbb.playgroundbackend.model.azure.TokenIssuanceStartRequest; import ch.sbb.playgroundbackend.model.azure.TokenIssuanceStartResponse; import ch.sbb.playgroundbackend.model.azure.TokenIssuanceStartResponseData; -import ch.sbb.playgroundbackend.service.SferaHandler; +import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.HashMap; import java.util.List; +import java.util.Map; @RestController @RequestMapping("customClaim") +@RequiredArgsConstructor public class CustomClaimController { private static final Logger log = LoggerFactory.getLogger(CustomClaimController.class); - public CustomClaimController() { + private final OAuth2AuthorizedClientManager manager; + private final ClientRegistrationRepository clientRegistrationRepository; + public static final String SFERA_TOKEN_REGISTRATION = "sfera-token"; + private final Map tokenClaimDataMap = new HashMap<>(); + + @GetMapping("requestToken") + String tokenRequest(Authentication authentication, String ru, String train, String role) { + + String userId = (String) ((Jwt) authentication.getPrincipal()).getClaims().get("sub"); + log.info("Received token request for {} with ru={} train={} role={}", userId, ru, train, role); + + tokenClaimDataMap.put(userId, new Claims(ru, train, role)); + + var clientRegistration = clientRegistrationRepository.findByRegistrationId(SFERA_TOKEN_REGISTRATION); + + OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId( + clientRegistration.getRegistrationId()) + .principal(authentication) + .build(); + + OAuth2AuthorizedClient client = manager.authorize(oAuth2AuthorizeRequest); + if (client == null) { + throw new IllegalStateException( + "failed to retrieve sfera-token failed, client is null"); + } + return client.getAccessToken().getTokenValue(); } @PostMapping TokenIssuanceStartResponse tokenIssuanceStartEvent(@RequestBody TokenIssuanceStartRequest body) { - log.info("Received request with body: {}", body); - + log.info("Received tokenUssuanceStartEvent"); + log.info("Client: {}", body.data().authenticationContext().client()); + log.info("ClientServicePrincipal: {}", body.data().authenticationContext().clientServicePrincipal()); + log.info("ResourceServicePrincipal: {}", body.data().authenticationContext().resourceServicePrincipal()); + log.info("User: {}", body.data().authenticationContext().user()); + String userId = body.data().authenticationContext().user().id(); Claims claims = new Claims("1085", "719_2024-06-13", "active"); + if (tokenClaimDataMap.containsKey(userId)) { + claims = tokenClaimDataMap.get(userId); + tokenClaimDataMap.remove(userId); + } + Action action = new Action("microsoft.graph.tokenIssuanceStart.provideClaimsForToken", claims); TokenIssuanceStartResponseData responseData = new TokenIssuanceStartResponseData("microsoft.graph.onTokenIssuanceStartResponseData", List.of(action)); TokenIssuanceStartResponse response = new TokenIssuanceStartResponse(responseData); diff --git a/playground-backend/src/main/resources/application.yaml b/playground-backend/src/main/resources/application.yaml index 6f8665fd..56446978 100644 --- a/playground-backend/src/main/resources/application.yaml +++ b/playground-backend/src/main/resources/application.yaml @@ -1,11 +1,34 @@ +info: + app: + version: '@project.version@' + management: endpoint: health: probes: enabled: true - spring: + security: + oauth2: + authorizationUrl: https://login.microsoftonline.com/2cda5d11-f0ac-46b3-967d-af1b2e1bd01a/oauth2/v2.0 + client: + provider: + azure-active-directory: + issuer-uri: https://login.microsoftonline.com/2cda5d11-f0ac-46b3-967d-af1b2e1bd01a/v2.0 + registration: + sfera-token: + provider: azure-active-directory + client-id: ${CLIENT_ID} + client-secret: ${CLIENT_SECRET} + authorization-grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: api://${SFERA_CLIENT_ID}/.default + resourceserver: + jwt: + jwk-set-uri: https://login.microsoftonline.com/2cda5d11-f0ac-46b3-967d-af1b2e1bd01a/discovery/v2.0/keys + issuer-uri: https://login.microsoftonline.com/2cda5d11-f0ac-46b3-967d-af1b2e1bd01a/v2.0 + audience: ${CLIENT_ID} + profiles: active: ${STAGE:local} cloud: @@ -34,3 +57,10 @@ spring: SSL_KEY_STORE_FORMAT: JKS SSL_KEY_STORE_PASSWORD: ${SOLACE_KEY_STORE_PASSWORD} default-binder: solace + +springdoc: + swagger-ui: + oauth: + use-pkce-with-authorization-code-grant: true + clientId: ${CLIENT_ID} +