Skip to content

Commit

Permalink
Enable conditional oauth authentication for terraform-boot
Browse files Browse the repository at this point in the history
  • Loading branch information
jinyangyang222 committed Sep 7, 2023
1 parent ca7e865 commit df0d71a
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 3 deletions.
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<br/>\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 | |
| 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
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<springdoc.version>2.1.0</springdoc.version>
<checkstyle-maven-plugin.version>3.3.0</checkstyle-maven-plugin.version>
<logbook.version>3.1.0</logbook.version>
<nimbusds.oidc.sdk.version>10.7</nimbusds.oidc.sdk.version>
</properties>
<dependencies>
<dependency>
Expand Down Expand Up @@ -69,6 +70,15 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>${nimbusds.oidc.sdk.version}</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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";

}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(Duration.ofSeconds(60)),
new JwtIssuerValidator(issuerUri));
jwtDecoder.setJwtValidator(withClockSkew);
return jwtDecoder;
}

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

}
Loading

0 comments on commit df0d71a

Please sign in to comment.