diff --git a/build.gradle.kts b/build.gradle.kts index 15df67b..f0e308f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,5 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -//buildscript { -// repositories { -// mavenCentral() -// } -//} - buildscript { val springBootVersion = "2.4.2" diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index cf9b422..bcdb7fb 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -8,6 +8,11 @@ plugins { dependencies { api(project(":lib")) api(project(":customer-api")) + api(project(":login-api")) + + implementation("org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.3.4.RELEASE") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") diff --git a/demo/src/main/kotlin/com/example/demo/DemoApplication.kt b/demo/src/main/kotlin/com/example/demo/DemoApplication.kt index bc7cd3b..dc372b1 100644 --- a/demo/src/main/kotlin/com/example/demo/DemoApplication.kt +++ b/demo/src/main/kotlin/com/example/demo/DemoApplication.kt @@ -6,8 +6,13 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.CommandLineRunner import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.Authentication +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController -@SpringBootApplication (scanBasePackages = ["com.example.customerapi"]) +@SpringBootApplication (scanBasePackages = ["com.example.demo", "com.example.loginapi","com.example.customerapi"]) class DemoApplication @Autowired constructor( //customerRepository: CustomerRepository, ) : CommandLineRunner { @@ -23,3 +28,14 @@ class DemoApplication @Autowired constructor( fun main(args: Array) { runApplication(*args) } + + +@RestController +@RequestMapping("/oauth") +@PreAuthorize("isAuthenticated()") +class OAuthController { + @GetMapping + fun getMe(authentication: Authentication) : Any? { + return authentication + } +} \ No newline at end of file diff --git a/demo/src/main/kotlin/com/example/demo/SecurityConfig.kt b/demo/src/main/kotlin/com/example/demo/SecurityConfig.kt new file mode 100644 index 0000000..dd997b1 --- /dev/null +++ b/demo/src/main/kotlin/com/example/demo/SecurityConfig.kt @@ -0,0 +1,27 @@ +package com.example.demo + +import com.example.loginapi.OauthResourceServerSecurity +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer + +/** + * @author sombochea + * @email sombochea@cubetiqs.com + * @date 15/10/19 + * @since 1.0 + */ +@Configuration +@EnableResourceServer +class SecurityConfig : OauthResourceServerSecurity() { + @Throws(Exception::class) + override fun configure(http: HttpSecurity) { + http.exceptionHandling() + .and() + .authorizeRequests() + .antMatchers("/api/**", "/oauth", "/customers") + .access("#oauth2.hasAnyScope('read','write')") + .antMatchers("/actuator/**") + .hasAnyRole("SUPER_ADMIN", "SYS_ADMIN","ACTUATOR") + } +} \ No newline at end of file diff --git a/demo/src/main/resources/application.properties b/demo/src/main/resources/application.properties index 78195c5..63b8028 100644 --- a/demo/src/main/resources/application.properties +++ b/demo/src/main/resources/application.properties @@ -1 +1,2 @@ -spring.data.mongodb.uri=mongodb://192.168.0.202:27017/db-customer-api \ No newline at end of file +spring.data.mongodb.uri=mongodb://192.168.0.202:27017/db-customer-api +spring.main.allow-bean-definition-overriding=true \ No newline at end of file diff --git a/login-api/.gitignore b/login-api/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/login-api/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/login-api/build.gradle.kts b/login-api/build.gradle.kts new file mode 100644 index 0000000..09f42f3 --- /dev/null +++ b/login-api/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id("org.springframework.boot") + id("io.spring.dependency-management") + kotlin("jvm") + kotlin("plugin.spring") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.3.4.RELEASE") + implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") +} + +tasks.withType { + useJUnitPlatform() +} + +tasks.withType { + enabled = true +} + +tasks.withType { + enabled = false +} \ No newline at end of file diff --git a/login-api/gradle/wrapper/gradle-wrapper.jar b/login-api/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/login-api/gradle/wrapper/gradle-wrapper.jar differ diff --git a/login-api/gradle/wrapper/gradle-wrapper.properties b/login-api/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..12d38de --- /dev/null +++ b/login-api/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/login-api/src/main/kotlin/com/example/loginapi/CubeJwtAccessTokenConverter.kt b/login-api/src/main/kotlin/com/example/loginapi/CubeJwtAccessTokenConverter.kt new file mode 100644 index 0000000..d714914 --- /dev/null +++ b/login-api/src/main/kotlin/com/example/loginapi/CubeJwtAccessTokenConverter.kt @@ -0,0 +1,20 @@ +package com.example.loginapi + +import org.springframework.security.oauth2.provider.OAuth2Authentication +import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter +import org.springframework.stereotype.Component + +/** + * @author sombochea + * @email sombochea@cubetiqs.com + * @date 16/10/19 + * @since 1.0 + */ +@Component +class CubeJwtAccessTokenConverter : DefaultAccessTokenConverter() { + override fun extractAuthentication(map: Map?): OAuth2Authentication { + val authentication = super.extractAuthentication(map) + authentication.details = map + return authentication + } +} \ No newline at end of file diff --git a/login-api/src/main/kotlin/com/example/loginapi/LoginApiApplication.kt b/login-api/src/main/kotlin/com/example/loginapi/LoginApiApplication.kt new file mode 100644 index 0000000..4a8c1e4 --- /dev/null +++ b/login-api/src/main/kotlin/com/example/loginapi/LoginApiApplication.kt @@ -0,0 +1,11 @@ +package com.example.loginapi + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class LoginApiApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/login-api/src/main/kotlin/com/example/loginapi/LoginController.kt b/login-api/src/main/kotlin/com/example/loginapi/LoginController.kt new file mode 100644 index 0000000..363238d --- /dev/null +++ b/login-api/src/main/kotlin/com/example/loginapi/LoginController.kt @@ -0,0 +1,20 @@ +package com.example.loginapi + +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/login") +class LoginController { + @GetMapping + fun get(): Any? { + return RestClientUtils.getRestTemplate().getForObject("https://api-clinic.cubetiqs.com/info", Any::class.java) + } + + @PostMapping + fun login( + @RequestParam(value = "username") username: String, + @RequestParam(value = "password") password: String, + ): Any? { + return RestClientUtils.login(username, password) + } +} \ No newline at end of file diff --git a/login-api/src/main/kotlin/com/example/loginapi/OauthResourceServerSecurity.kt b/login-api/src/main/kotlin/com/example/loginapi/OauthResourceServerSecurity.kt new file mode 100644 index 0000000..f01ac9b --- /dev/null +++ b/login-api/src/main/kotlin/com/example/loginapi/OauthResourceServerSecurity.kt @@ -0,0 +1,76 @@ +package com.example.loginapi + +import org.springframework.beans.factory.annotation.Value +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.oauth2.config.annotation.web.configuration.EnableResourceServer +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer +import org.springframework.security.oauth2.provider.token.DefaultTokenServices +import org.springframework.security.oauth2.provider.token.TokenStore +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter +import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore + +/** + * @author sombochea + * @since 1.0 + */ +@Configuration +@EnableResourceServer +open class OauthResourceServerSecurity : + ResourceServerConfigurerAdapter() { + private val jwtAccessTokenConverter: CubeJwtAccessTokenConverter = CubeJwtAccessTokenConverter() + + @Value("\${spring.security.oauth2.resourceserver.jwt.public-key}") + var publicKey: String? = null + + @Value("\${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") + var jwkSetUri: String? = null + + private var tokenStore: TokenStore? = null + + override fun configure(resources: ResourceServerSecurityConfigurer) { + val resourceId = "cubetiq-clinic-dev" + println("Loaded system with resource id: $resourceId") + resources + .tokenStore(tokenStore()) + .resourceId(resourceId) + .stateless(false) + } + + @Throws(Exception::class) + override fun configure(http: HttpSecurity) { + http.exceptionHandling() + .and() + .authorizeRequests() + .antMatchers("/api/**") + .access("#oauth2.hasAnyScope('read','write')") + .antMatchers("/actuator/**") + .hasAnyRole("SUPER_ADMIN", "SYS_ADMIN","ACTUATOR") + } + + @Bean + fun tokenServices(tokenStore: TokenStore?): DefaultTokenServices { + val tokenServices = DefaultTokenServices() + tokenServices.setTokenStore(tokenStore) + return tokenServices + } + + @Bean + fun tokenStore(): TokenStore? { + if (tokenStore == null) { + tokenStore = JwkTokenStore(jwkSetUri, jwtAccessTokenConverter) + } + + return tokenStore + } + + @Bean + fun jwtAccessTokenConverter(): JwtAccessTokenConverter { + val converter = JwtAccessTokenConverter() + converter.accessTokenConverter = jwtAccessTokenConverter + converter.setVerifierKey(publicKey) + return converter + } +} diff --git a/login-api/src/main/kotlin/com/example/loginapi/RestClientUtils.kt b/login-api/src/main/kotlin/com/example/loginapi/RestClientUtils.kt new file mode 100644 index 0000000..a174079 --- /dev/null +++ b/login-api/src/main/kotlin/com/example/loginapi/RestClientUtils.kt @@ -0,0 +1,115 @@ +@file:Suppress("unused", "unused") + +package com.example.loginapi + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import org.springframework.http.* +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap +import org.springframework.web.client.RestTemplate +import java.io.Serializable +import java.nio.charset.StandardCharsets +import java.util.* + +/** + * @author sombochea + * @since 1.0 + */ +object RestClientUtils { + private const val BEAN_NAME = "restTemplate" + + private var restTemplate: RestTemplate? = null + + @JvmStatic + fun setRestTemplate(restTemplate: RestTemplate?) { + RestClientUtils.restTemplate = restTemplate + } + + @JvmStatic + fun getRestTemplate(): RestTemplate { + if (restTemplate == null) { + restTemplate = RestTemplate() + } + return restTemplate ?: throw Exception("rest client service load failed") + } + + fun login(username: String, password: String): OAuthToken? { + val authEndpoint = "https://preprod-api-auth.staging.cubetiqs.com/api/oauth/token" + val httpHeaders = getHttpHeadersConfig() + val body: MultiValueMap = LinkedMultiValueMap() + body.add("grant_type", "password") + body.add("username", username) + body.add("password", password) + + val httpEntity = HttpEntity(body, httpHeaders) + + println(httpEntity) + + return getRestTemplate().postForEntity(authEndpoint, httpEntity, OAuthToken::class.java).body + } + + private fun getHttpHeadersConfig(): HttpHeaders { + val httpHeaders = HttpHeaders() + val client = "cubetiq-clinic-dev" + val secret = "123456" + httpHeaders.contentType = MediaType.APPLICATION_FORM_URLENCODED + val clientDetail = "$client:$secret" + val oauthCodes = Base64.getEncoder().encode(clientDetail.toByteArray(StandardCharsets.US_ASCII)) + httpHeaders["Authorization"] = "Basic " + String(oauthCodes) + httpHeaders["Tenant-ID"] = "TNA-00013067" + httpHeaders["User-Type"] = "INTERNAL" + return httpHeaders + } + + @JsonIgnoreProperties(ignoreUnknown = true) + data class OAuthToken( + @JsonProperty(value = "access_token") + var accessToken: String? = null, + @JsonProperty(value = "token_type") + var tokenType: String? = null, + @JsonProperty(value = "refresh_token") + var refreshToken: String? = null, + @JsonProperty(value = "expires_in") + var expiresIn: Long? = null, + @JsonProperty(value = "scope") + var scope: String? = null, + @JsonProperty(value = "auditor") + var auditor: String? = null, + @JsonProperty(value = "tenant") + var tenant: String? = null, + @JsonProperty(value = "user_id") + var userId: String? = null, + @JsonProperty(value = "username") + var username: String? = null, + @JsonProperty(value = "jti") + var jti: String? = null, + + var passcode: Boolean? = null, + var configs: Map? = null, + + @JsonProperty(value = "current_branch_id") + var currentBranchId: String? = null, + @JsonProperty(value = "current_branch") + var currentBranch: String? = null, + ) : Serializable { + @JsonIgnore + fun addConfig(key: String, value: Any?) = apply { + if (this.configs == null) { + this.configs = mutableMapOf() + } + (this.configs as MutableMap)[key] = value + } + + @JsonIgnore + fun addConfigs(configs: Map?) = apply { + if (this.configs == null) { + this.configs = mutableMapOf() + } + if (configs != null) { + (this.configs as MutableMap).putAll(configs) + } + } + } +} \ No newline at end of file diff --git a/login-api/src/main/kotlin/com/example/loginapi/UserController.kt b/login-api/src/main/kotlin/com/example/loginapi/UserController.kt new file mode 100644 index 0000000..ba94e49 --- /dev/null +++ b/login-api/src/main/kotlin/com/example/loginapi/UserController.kt @@ -0,0 +1,17 @@ +package com.example.loginapi + +import org.springframework.security.core.Authentication +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/users") +class UserController { + @GetMapping("/me") + fun getMe( + authentication: Authentication, + ): Any { + return authentication + } +} \ No newline at end of file diff --git a/login-api/src/main/kotlin/com/example/loginapi/WebSecurityConfig.kt b/login-api/src/main/kotlin/com/example/loginapi/WebSecurityConfig.kt new file mode 100644 index 0000000..a54df71 --- /dev/null +++ b/login-api/src/main/kotlin/com/example/loginapi/WebSecurityConfig.kt @@ -0,0 +1,26 @@ +package com.example.loginapi + +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity +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.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.http.SessionCreationPolicy + +/** + * @author sombochea + * @email sombochea@cubetiqs.com + * @date 15/10/19 + * @since 1.0 + */ +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +class WebSecurityConfig : WebSecurityConfigurerAdapter() { + @Throws(Exception::class) + override fun configure(http: HttpSecurity) { + http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + } +} diff --git a/login-api/src/main/resources/application.yml b/login-api/src/main/resources/application.yml new file mode 100644 index 0000000..b3e14ff --- /dev/null +++ b/login-api/src/main/resources/application.yml @@ -0,0 +1,19 @@ +server: + port: 8015 + +spring: + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: ${JWK_SET_URI:https://preprod-api-auth.staging.cubetiqs.com/.well-known/jwks.json} + public-key: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhLjm/+1Maitij0pV4IVD + gpLZ7IAvlXxKyToTCRusFwsto3T5jZIr5pNFEPJN6XuO/2fHGlcIioRD6pC1xdHu + qoYwImNHjYrS2vRrVboBiMHgOqZ2/Qk2knyNC98vp6sBp8PDSAWSPkWgKPDR2RV0 + sFoPVT+0TCtXPVrdOCPkHDvrg2M4H8NwRtec3bzv3KkIpf2TSuSSHwL9JENaXpJn + 2POnZwjBADa2xIU4K3k9XdYrTDqqlnIfnj/irT8aUCQzyo5vfqy4n9eQjj/lSmhT + L76pnrIEvl0UjnfRfZ9prE6+bS2pF6d4cYXfATwC0lKkIgKjHPoyUnyleJ6qHDyN + CwIDAQAB + -----END PUBLIC KEY----- \ No newline at end of file diff --git a/login-api/src/test/kotlin/com/example/loginapi/LoginApiApplicationTests.kt b/login-api/src/test/kotlin/com/example/loginapi/LoginApiApplicationTests.kt new file mode 100644 index 0000000..ff35873 --- /dev/null +++ b/login-api/src/test/kotlin/com/example/loginapi/LoginApiApplicationTests.kt @@ -0,0 +1,13 @@ +package com.example.loginapi + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class LoginApiApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 110f265..7521a78 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,3 @@ rootProject.name = "sample-modules" - include("demo", "lib", "customer-api") \ No newline at end of file + include("demo", "lib", "customer-api", "login-api") \ No newline at end of file