From df0d71afa1e9f3749ca29cbb6c75d95e099e6a66 Mon Sep 17 00:00:00 2001 From: jinyangyang222 Date: Thu, 7 Sep 2023 15:01:35 +0800 Subject: [PATCH] Enable conditional oauth authentication for terraform-boot --- README.md | 44 ++++- pom.xml | 10 ++ .../boot/models/response/ResultType.java | 3 +- .../oauth2/config/Oauth2Constants.java | 29 ++++ .../oauth2/config/Oauth2OpenApiConfig.java | 52 ++++++ .../config/Oauth2WebSecurityConfig.java | 161 ++++++++++++++++++ .../OauthOpaqueTokenIntrospector.java | 47 +++++ .../resources/application-oauth.properties | 28 +++ 8 files changed, 371 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/config/Oauth2Constants.java create mode 100644 src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/config/Oauth2OpenApiConfig.java create mode 100644 src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/config/Oauth2WebSecurityConfig.java create mode 100644 src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/introspector/OauthOpaqueTokenIntrospector.java create mode 100644 src/main/resources/application-oauth.properties diff --git a/README.md b/README.md index 0b2663f..6f0f2b3 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,46 @@ A spring-boot-based project which aims to provide a RESTful API for Terraform CL Server can be compiled and started as below +If you have a fully configured OAuth instance running on your local system, then you can use the below way to +start the application.The variables and their descriptions that need to be passed during oauth startup are recorded in +the Available Configurations table. + +### From Command Line + +1.Start with oauth + ```shell ./mvmw clean install -DskipTests -java -jar target/terraform-boot-*.jar +$ java -jar target/terraform-boot-*.jar\ +--spring.profiles.active=oauth \ +--authorization-token-type=${token-type} \ +--authorization-server-endpoint=${server-endpoint} \ +--authorization-api-client-id=${client-id} \ +--authorization-api-client-secret=${client-secret} \ +--authorization-swagger-ui-client-id=${swagger-ui-cleint-id} ``` +2.Start without oauth + +```shell +./mvmw clean install -DskipTests +$ java -jar target/terraform-boot-*.jar +``` + +### From IDE + +1.Start with oauth + + 1.Set values for the authorization related variables in the application-oauth-properties configuration file, + and specify the configuration file for loading oauth in the application-properties configuration file, + start the main application. + 2.Or the oauth related variables configuration can be added to IDE and the main application can be executed directly + to launch the application. + +2.Start without oauth + + Simply start the main application. + API can be accessed using the following URLs ```html @@ -54,4 +89,9 @@ The below property names can be changed in the following ways | terraform_binary_path | TERRAFORM_BINARY_PATH | Terraform available on syspath | The path to the terraform binary | | terraform.root.module.directory | TERRAFORM_ROOT_MODULE_DIRECTORY | /tmp on Linux
\AppData\Local\Temp on Windows | The path to the parent directory where all terraform module directories will be stored at as subdirs | | log.terraform.stdout.stderr | LOG_TERRAFORM_STDOUT_STDERR | true | Controls if the command execution output must be logged. If disabled, the output is only returned in the API response | -| terraform.log.level | TERRAFORM_LOG_LEVEL | INFO | Controls the log level of the terraform binary. Allowed values are INFO, DEBUG, TRACE, WARN and ERROR | | \ No newline at end of file +| terraform.log.level | TERRAFORM_LOG_LEVEL | INFO | Controls the log level of the terraform binary. Allowed values are INFO, DEBUG, TRACE, WARN and ERROR | +| authorization-token-type | AUTHORIZATION_TOKEN_TYPE | OpaqueToken | Authorization server authentication Type, allowed values: OpaqueToken or JWT +| authorization-server-endpoint | AUTHORIZATION_SERVER_ENDPOINT | | The endpoint value of the authorization server +| authorization-api-client-id | AUTHORIZATION_API_CLIENT_ID | | The ID value of the authorization server API client +| authorization-api-client-secret | AUTHORIZATION_API_CLIENT_SECRET | | The secret value of the authorization server API client +| authorization-swagger-ui-client-id| AUTHORIZATION_SWAGGER_UI_CLIENT_ID| | The ID value of the authorization server swagger-ui client \ No newline at end of file diff --git a/pom.xml b/pom.xml index 847f58f..8b93b51 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ 2.1.0 3.3.0 3.1.0 + 10.7 @@ -69,6 +70,15 @@ + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + com.nimbusds + oauth2-oidc-sdk + ${nimbusds.oidc.sdk.version} + diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/models/response/ResultType.java b/src/main/java/org/eclipse/xpanse/terraform/boot/models/response/ResultType.java index fa91e08..0ec613d 100644 --- a/src/main/java/org/eclipse/xpanse/terraform/boot/models/response/ResultType.java +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/models/response/ResultType.java @@ -18,7 +18,8 @@ public enum ResultType { UNPROCESSABLE_ENTITY("Unprocessable Entity"), TERRAFORM_EXECUTION_FAILED("Terraform Execution Failed"), UNSUPPORTED_ENUM_VALUE("Unsupported Enum Value"), - SERVICE_UNAVAILABLE("Service Unavailable"); + SERVICE_UNAVAILABLE("Service Unavailable"), + UNAUTHORIZED("Unauthorized"); private final String value; diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/config/Oauth2Constants.java b/src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/config/Oauth2Constants.java new file mode 100644 index 0000000..cf54243 --- /dev/null +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/config/Oauth2Constants.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: Huawei Inc. + * + */ + +package org.eclipse.xpanse.terraform.boot.security.oauth2.config; + +/** + * Constants for OAuth2 Authorization. + */ +public class Oauth2Constants { + + /** + * Auth token type: JWT. + */ + public static final String AUTH_TYPE_JWT = "JWT"; + + /** + * Auth token type: OpaqueToken. + */ + public static final String AUTH_TYPE_TOKEN = "OpaqueToken"; + + /** + * Mandatory scope to request the profile of the user. + */ + public static final String OPENID_SCOPE = "openid"; + +} diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/config/Oauth2OpenApiConfig.java b/src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/config/Oauth2OpenApiConfig.java new file mode 100644 index 0000000..966e146 --- /dev/null +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/config/Oauth2OpenApiConfig.java @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: Huawei Inc. + * + */ + +package org.eclipse.xpanse.terraform.boot.security.oauth2.config; + +import static org.eclipse.xpanse.terraform.boot.security.oauth2.config.Oauth2Constants.OPENID_SCOPE; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.OAuthFlow; +import io.swagger.v3.oas.annotations.security.OAuthFlows; +import io.swagger.v3.oas.annotations.security.OAuthScope; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + + +/** + * Configuration springdoc security OAuth2. + */ +@Profile("oauth") +@OpenAPIDefinition( + info = @Info( + title = "Terraform-Boot API", + description = "RESTful Services to interact with Terraform-Boot runtime", + version = "${app.version}" + ), + security = @SecurityRequirement(name = "OAuth2 Flow", + scopes = {OPENID_SCOPE}) +) +@SecurityScheme( + name = "OAuth2 Flow", + type = SecuritySchemeType.OAUTH2, + flows = @OAuthFlows(authorizationCode = + @OAuthFlow( + authorizationUrl = "${springdoc.oAuthFlow.authorizationUrl}", + tokenUrl = "${springdoc.oAuthFlow.tokenUrl}", + scopes = { + @OAuthScope(name = OPENID_SCOPE, + description = "mandatory must be selected.") + } + ) + ) +) +@Configuration +public class Oauth2OpenApiConfig { +} diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/config/Oauth2WebSecurityConfig.java b/src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/config/Oauth2WebSecurityConfig.java new file mode 100644 index 0000000..840fbf0 --- /dev/null +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/config/Oauth2WebSecurityConfig.java @@ -0,0 +1,161 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: Huawei Inc. + * + */ + +package org.eclipse.xpanse.terraform.boot.security.oauth2.config; + + +import static org.eclipse.xpanse.terraform.boot.security.oauth2.config.Oauth2Constants.AUTH_TYPE_JWT; +import static org.eclipse.xpanse.terraform.boot.security.oauth2.config.Oauth2Constants.AUTH_TYPE_TOKEN; +import static org.springframework.web.cors.CorsConfiguration.ALL; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletResponse; +import java.io.PrintWriter; +import java.time.Duration; +import java.util.Collections; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.xpanse.terraform.boot.models.response.Response; +import org.eclipse.xpanse.terraform.boot.models.response.ResultType; +import org.eclipse.xpanse.terraform.boot.security.oauth2.introspector.OauthOpaqueTokenIntrospector; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.MediaType; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +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.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtDecoders; +import org.springframework.security.oauth2.jwt.JwtIssuerValidator; +import org.springframework.security.oauth2.jwt.JwtTimestampValidator; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +/** + * Configuration applied on all web endpoints defined for this + * application. Any configuration on specific resources is applied + * in addition to these global rules. + */ +@Slf4j +@Profile("oauth") +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) +public class Oauth2WebSecurityConfig { + + @Value("${authorization-token-type:JWT}") + private String authTokenType; + + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + private String issuerUri; + + @Value("${spring.security.oauth2.resourceserver.opaquetoken.introspection-uri}") + private String introspectionUri; + + @Value("${spring.security.oauth2.resourceserver.opaquetoken.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.resourceserver.opaquetoken.client-secret}") + private String clientSecret; + + /** + * Configures basic security handler per HTTP session. + * + * @param http security configuration + */ + @Bean + public SecurityFilterChain apiFilterChain(HttpSecurity http) + throws Exception { + // accept cors requests and allow preflight checks + http.cors(httpSecurityCorsConfigurer -> httpSecurityCorsConfigurer.configurationSource( + corsConfigurationSource())); + + http.authorizeHttpRequests(arc -> { + arc.requestMatchers(AntPathRequestMatcher.antMatcher("/swagger-ui/**")).permitAll(); + arc.requestMatchers(AntPathRequestMatcher.antMatcher("/v3/**")).permitAll(); + arc.requestMatchers(AntPathRequestMatcher.antMatcher("/error")).permitAll(); + arc.anyRequest().authenticated(); + }); + + http.csrf(AbstractHttpConfigurer::disable); + + http.headers(headersConfigurer -> headersConfigurer.addHeaderWriter( + new XFrameOptionsHeaderWriter( + XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))); + + // set custom exception handler + http.exceptionHandling(exceptionHandlingConfigurer -> + exceptionHandlingConfigurer.authenticationEntryPoint( + (httpRequest, httpResponse, authException) -> { + httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpResponse.setCharacterEncoding("UTF-8"); + httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); + ObjectMapper objectMapper = new ObjectMapper(); + Response responseModel = Response.errorResponse(ResultType.UNAUTHORIZED, + Collections.singletonList(ResultType.UNAUTHORIZED.toValue())); + String resBody = objectMapper.writeValueAsString(responseModel); + PrintWriter printWriter = httpResponse.getWriter(); + printWriter.print(resBody); + printWriter.flush(); + printWriter.close(); + } + )); + + if (StringUtils.equalsIgnoreCase(AUTH_TYPE_TOKEN, authTokenType)) { + // Config custom OpaqueTokenIntrospector + http.oauth2ResourceServer(oauth2 -> + oauth2.opaqueToken(opaque -> + opaque.introspector( + new OauthOpaqueTokenIntrospector(introspectionUri, + clientId, clientSecret)) + ) + ); + } + + if (StringUtils.equalsIgnoreCase(AUTH_TYPE_JWT, authTokenType)) { + // Config custom JwtAuthenticationConverter + http.oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwtDecoder()) + ); + } + return http.build(); + } + + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowCredentials(true); + configuration.addAllowedHeader(ALL); + configuration.addAllowedMethod(ALL); + configuration.addAllowedOriginPattern(ALL); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + @ConditionalOnProperty("authorization-token-type=JWT") + JwtDecoder jwtDecoder() { + NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri); + OAuth2TokenValidator withClockSkew = new DelegatingOAuth2TokenValidator<>( + new JwtTimestampValidator(Duration.ofSeconds(60)), + new JwtIssuerValidator(issuerUri)); + jwtDecoder.setJwtValidator(withClockSkew); + return jwtDecoder; + } + +} \ No newline at end of file diff --git a/src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/introspector/OauthOpaqueTokenIntrospector.java b/src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/introspector/OauthOpaqueTokenIntrospector.java new file mode 100644 index 0000000..1daf867 --- /dev/null +++ b/src/main/java/org/eclipse/xpanse/terraform/boot/security/oauth2/introspector/OauthOpaqueTokenIntrospector.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: Huawei Inc. + * + */ + +package org.eclipse.xpanse.terraform.boot.security.oauth2.introspector; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; + +/** + * Customize the OAuth2AuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector. + */ +@Slf4j +public class OauthOpaqueTokenIntrospector implements OpaqueTokenIntrospector { + + private final OpaqueTokenIntrospector opaqueTokenIntrospector; + + /** + * Constructor. + * + * @param introspectionUri The url of IAM server to verify token. + * @param clientId The id of api client created in IAM server. + * @param clientSecret The secret of api client created in IAM server. + */ + public OauthOpaqueTokenIntrospector(String introspectionUri, + String clientId, + String clientSecret) { + opaqueTokenIntrospector = + new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret); + } + + /** + * Verify token. + * + * @param token The token of current user. + * @return OAuth2AuthenticatedPrincipal + */ + public OAuth2AuthenticatedPrincipal introspect(String token) { + return this.opaqueTokenIntrospector.introspect(token); + } + +} \ No newline at end of file diff --git a/src/main/resources/application-oauth.properties b/src/main/resources/application-oauth.properties new file mode 100644 index 0000000..3c1f321 --- /dev/null +++ b/src/main/resources/application-oauth.properties @@ -0,0 +1,28 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Huawei Inc. +# +# +# spring security oauth2 config +logging.level.org.springframework.web=info +logging.level.org.springframework.security=debug +# set authorization-token-type: JWT or OpaqueToken +authorization-token-type=OpaqueToken +# set authorization server endpoint and client configs +authorization-server-endpoint= +authorization-api-client-id= +authorization-api-client-secret= +authorization-swagger-ui-client-id= +# spring security oauth2 config when using token type JWT +spring.security.oauth2.resourceserver.jwt.issuer-uri=${authorization-server-endpoint} +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${authorization-server-endpoint}/oauth/v2/keys +# spring security oauth2 config when using token type OpaqueToken +spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=${authorization-server-endpoint}/oauth/v2/introspect +spring.security.oauth2.resourceserver.opaquetoken.client-id=${authorization-api-client-id} +spring.security.oauth2.resourceserver.opaquetoken.client-secret=${authorization-api-client-secret} +# springdoc openapi security oauth2 config +springdoc.show-login-endpoint=true +springdoc.swagger-ui.oauth.use-pkce-with-authorization-code-grant=true +springdoc.swagger-ui.oauth.clientId=${authorization-swagger-ui-client-id} +springdoc.oAuthFlow.authorizationUrl=${authorization-server-endpoint}/oauth/v2/authorize +springdoc.oAuthFlow.tokenUrl=${authorization-server-endpoint}/oauth/v2/token \ No newline at end of file