Skip to content

Commit

Permalink
feat: Add requestToken to CustomClaimController
Browse files Browse the repository at this point in the history
  • Loading branch information
Grodien committed Jun 17, 2024
1 parent d614747 commit b976fb7
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 5 deletions.
15 changes: 15 additions & 0 deletions playground-backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId> <!-- Require this because this project uses WebClient。 -->
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

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

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Jwt> jwtValidator() {
List<OAuth2TokenValidator<Jwt>> 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<Object> 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;
}
};
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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<String, String> createParameters(JwtBearerGrantRequest jwtBearerGrantRequest) {
MultiValueMap<String, String> parameters = super.createParameters(jwtBearerGrantRequest);
parameters.add("requested_token_use", "on_behalf_of");
return parameters;
}

}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Claims> 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);
Expand Down
32 changes: 31 additions & 1 deletion playground-backend/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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}

0 comments on commit b976fb7

Please sign in to comment.