From 8a8166455a4382402c4af3fefedaa287c186e477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Tue, 5 Nov 2024 09:04:43 +0900 Subject: [PATCH 01/21] chore : (#2) Update DB scheme --- scheme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scheme.md b/scheme.md index fddd625..b51ac7c 100644 --- a/scheme.md +++ b/scheme.md @@ -3,8 +3,8 @@ ```mermaid erDiagram USERS { - BIGINT id PK "고유 ID" - BIGINT github_id "GitHub 사용자 ID" + UUID id PK "고유 ID" + VARCHAR github_id "GitHub 사용자 ID" VARCHAR username "GitHub 사용자 이름" VARCHAR email "사용자 이메일" VARCHAR profile_url "GitHub 프로필 URL" From 37c2060f1483fa19e872021c997fea8b587c3632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Tue, 5 Nov 2024 09:10:10 +0900 Subject: [PATCH 02/21] feat : (#2) Update dependencies --- build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bfba86d..d9630e3 100644 --- a/build.gradle +++ b/build.gradle @@ -22,13 +22,16 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - + implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.jetbrains.kotlin:kotlin-reflect' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' + runtimeOnly 'com.mysql:mysql-connector-j' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } + kotlin { compilerOptions { freeCompilerArgs.addAll '-Xjsr305=strict' From 4abc38a225d355caf11eb54d4f58c50d62d7be8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Tue, 5 Nov 2024 09:10:43 +0900 Subject: [PATCH 03/21] feat : (#2) set application properties --- src/main/resources/application.properties | 1 - src/main/resources/application.yaml | 34 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yaml diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 46e4cb0..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=Equus-Github-Auth diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..2e1142a --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,34 @@ +spring: + datasource: + url: jdbc:mysql://${DB_HOST:localhost}:3306/${DB_NAME} + username: ${DB_USERNAME:root} + password: ${DB_PASSWORD:root_password} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: ${JPA_HIBERNATE_DDL_AUTO:create} + show-sql: ${JPA_SHOW_SQL:true} + properties: + hibernate: + format_sql: ${JPA_FORMAT_SQL:true} + open-in-view: false + + + security: + oauth2: + client: + registration: + github: + client-id: ${ GITHUB_CLIENT_ID } + client-secret: ${ GITHUB_CLIENT_SECRET } + scope: read:user, user:email + redirect-uri: http://localhost:8080/login/oauth2/code/github + authorization-grant-type: authorization_code + client-name: EntryDSM GitHub OAuth + provider: + github: + authorization-uri: https://github.com/login/oauth/authorize + token-uri: https://github.com/login/oauth/access_token + user-info-uri: https://api.github.com/user + user-name-attribute: id + From d0879b1eea6ae549abcd4f9dc5710493bc0dbe7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Tue, 5 Nov 2024 19:50:06 +0900 Subject: [PATCH 04/21] feat : (#2) Add lombok dependency --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index d9630e3..d6dda41 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.jetbrains.kotlin:kotlin-reflect' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' runtimeOnly 'com.mysql:mysql-connector-j' From 2defb23b012315c9604fed90b08554e9d5ccf9f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Tue, 5 Nov 2024 23:28:29 +0900 Subject: [PATCH 05/21] fix : (#3) Switch build file from Groovy to Kotlin script --- build.gradle | 44 -------------------------------------------- build.gradle.kts | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 44 deletions(-) delete mode 100644 build.gradle create mode 100644 build.gradle.kts diff --git a/build.gradle b/build.gradle deleted file mode 100644 index d6dda41..0000000 --- a/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.jvm' version '1.9.25' - id 'org.jetbrains.kotlin.plugin.spring' version '1.9.25' - id 'org.springframework.boot' version '3.3.5' - id 'io.spring.dependency-management' version '1.1.6' -} - -group = 'entry.dsm.gitauth' -version = '0.0.1-SNAPSHOT' - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } -} - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.springframework.boot:spring-boot-starter' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.jetbrains.kotlin:kotlin-reflect' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' - runtimeOnly 'com.mysql:mysql-connector-j' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' -} - - -kotlin { - compilerOptions { - freeCompilerArgs.addAll '-Xjsr305=strict' - } -} - -tasks.named('test') { - useJUnitPlatform() -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..c2567e8 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("org.jetbrains.kotlin.jvm") version "1.9.25" + id("org.jetbrains.kotlin.plugin.spring") version "1.9.25" + id("org.springframework.boot") version "3.3.5" + id("io.spring.dependency-management") version "1.1.6" + id("org.jetbrains.kotlin.plugin.jpa") version "1.9.25" +} + +group = "entry.dsm.gitauth" +version = "0.0.1-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-oauth2-client") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.projectlombok:lombok") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + runtimeOnly("com.mysql:mysql-connector-j") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + } +} + +noArg { + annotation("jakarta.persistence.Entity") +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file From f49805ebeb7e81e507097f342465dc3a2896b3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Wed, 6 Nov 2024 08:11:51 +0900 Subject: [PATCH 06/21] feat : (#2) Add User Entity --- .../domain/user/entity/User.kt | 46 +++++++++++++++++++ .../user/entity/repository/UserRepository.kt | 6 +++ 2 files changed, 52 insertions(+) create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt new file mode 100644 index 0000000..b6ed632 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt @@ -0,0 +1,46 @@ +package entry.dsm.gitauth.equusgithubauth.domain.user.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import java.time.LocalDateTime +import java.util.* + +@Entity +@Table(name = "users") +data class User( + @Id + @Column(name = "id", nullable = false) + val id: UUID = UUID.randomUUID(), + + @Column(name = "github_id", nullable = false, unique = true) + val githubId: String, + + @Column(name = "username", nullable = false) + val username: String, + + @Column(name = "email", nullable = false, unique = true) + val email: String, + + @Column(name = "profile_url") + val profileUrl: String? = null, + + @Column(name = "avatar_url") + val avatarUrl: String? = null, + + @Column(name = "created_at", nullable = false) + val createdAt: LocalDateTime = LocalDateTime.now(), + + @Column(name = "updated_at") + var updatedAt: LocalDateTime = LocalDateTime.now(), + + @Column(name = "is_member_of_org", nullable = false) + val isMemberOfOrg: Boolean = false, + + @Column(name = "access_token") + var accessToken: String? = null, + + @Column(name = "token_expiration") + var tokenExpiration: LocalDateTime? = null +) diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt new file mode 100644 index 0000000..38eb70d --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt @@ -0,0 +1,6 @@ +package entry.dsm.gitauth.equusgithubauth.domain.user.entity.repository + +import org.springframework.data.jpa.repository.JpaRepository +import entry.dsm.gitauth.equusgithubauth.domain.user.entity.User + +interface UserRepository : JpaRepository \ No newline at end of file From 3c5636181b7629444c6e1b24dca8e4410905bc1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Wed, 6 Nov 2024 16:44:40 +0900 Subject: [PATCH 07/21] feat : (#2) update database scheme(remove is_member_of_org colum) --- scheme.md | 1 - 1 file changed, 1 deletion(-) diff --git a/scheme.md b/scheme.md index b51ac7c..a6bd9be 100644 --- a/scheme.md +++ b/scheme.md @@ -11,7 +11,6 @@ erDiagram VARCHAR avatar_url "GitHub 아바타 이미지 URL" TIMESTAMP created_at "생성 시간" TIMESTAMP updated_at "갱신 시간" - BOOLEAN is_member_of_org "조직 소속 여부" VARCHAR access_token "내부 애플리케이션 액세스 토큰" TIMESTAMP token_expiration "토큰 만료 시간" } From 5598ff03f0c86e97466641c3bcc1f19b0c9c209c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Sun, 8 Dec 2024 01:42:27 +0900 Subject: [PATCH 08/21] feat : (#2) Repair existing code --- .../GithubAuthenticationController.kt | 25 ++++++ .../presentation/dto/GithubUserInformation.kt | 13 +++ .../domain/auth/service/GithubUserService.kt | 80 +++++++++++++++++++ .../global/config/ApplicationConfig.kt | 15 ++++ .../oauth/GithubAuthenticationConfig.kt | 23 ++++++ .../GithubAuthenticationFailureHandler.kt | 16 ++++ .../GithubAuthenticationSuccessHandler.kt | 16 ++++ .../global/security/SecurityConfig.kt | 26 ++++++ src/main/resources/application.yaml | 5 +- 9 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/dto/GithubUserInformation.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/config/ApplicationConfig.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubAuthenticationConfig.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/handler/GithubAuthenticationFailureHandler.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/handler/GithubAuthenticationSuccessHandler.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/security/SecurityConfig.kt diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt new file mode 100644 index 0000000..b6dc66f --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt @@ -0,0 +1,25 @@ +package entry.dsm.gitauth.equusgithubauth.domain.auth.presentation.controller + +import entry.dsm.gitauth.equusgithubauth.domain.auth.presentation.dto.GithubUserInformation +import entry.dsm.gitauth.equusgithubauth.domain.auth.service.GithubUserService +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.oauth2.core.user.OAuth2User +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/github/auth") +class GithubAuthenticationController( + val githubUserService: GithubUserService +) { + @GetMapping("/authenticated/") + fun getGithubUserInfo(@AuthenticationPrincipal oAuth2User: OAuth2User): GithubUserInformation { + return githubUserService.getGithubUserInformation(oAuth2User) + } + + @GetMapping("/not/authenticated/") + fun notAuthenticated(): String { + return "Not authenticated" + } +} \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/dto/GithubUserInformation.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/dto/GithubUserInformation.kt new file mode 100644 index 0000000..880e079 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/dto/GithubUserInformation.kt @@ -0,0 +1,13 @@ +package entry.dsm.gitauth.equusgithubauth.domain.auth.presentation.dto + +data class GithubUserInformation( + val githubId: String, + val username: String, + val email: String?, + val profileUrl: String, + val avatarUrl: String, + val createdAt: String, + val updatedAt: String, + val accessToken: String, + val tokenExpiration: String +) \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt new file mode 100644 index 0000000..98e4f24 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt @@ -0,0 +1,80 @@ +package entry.dsm.gitauth.equusgithubauth.domain.auth.service + +import entry.dsm.gitauth.equusgithubauth.domain.auth.presentation.dto.GithubUserInformation +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.RequestEntity +import org.springframework.http.ResponseEntity +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService +import org.springframework.security.oauth2.core.user.OAuth2User +import org.springframework.stereotype.Service +import org.springframework.web.client.RestTemplate + +@Service +class GithubUserService( + private val authorizedClientService: OAuth2AuthorizedClientService, + private val restTemplate: RestTemplate +) { + fun getGithubUserInformation(oAuth2User: OAuth2User): GithubUserInformation { + val client = getAuthorizedClient(oAuth2User) + validateAccessToken(client.accessToken.tokenValue) + return createGithubUserInformation(oAuth2User, client) + } + + private fun getAuthorizedClient(oAuth2User: OAuth2User): OAuth2AuthorizedClient { + return authorizedClientService.loadAuthorizedClient("github", oAuth2User.name) + ?: throw IllegalArgumentException("인증된 클라이언트를 찾을 수 없음") + } + + private fun validateAccessToken(token: String) { + token.takeIf { it.isNotBlank() } ?: throw IllegalArgumentException("토큰이 비어있음") + if (!isTokenActive(token)) { + throw IllegalArgumentException("토큰이 만료되었거나 잘못된 토큰입니다.") + } + } + + private fun isTokenActive(token: String): Boolean { + return try { + val request = buildGithubApiRequest(token) + val response: ResponseEntity = restTemplate.exchange(request, String::class.java) + response.statusCode == HttpStatus.OK + } catch (ex: Exception) { + false + } + } + + private fun buildGithubApiRequest(token: String): RequestEntity { + val url = "https://api.github.com/user" + val headers = HttpHeaders().apply { + set("Authorization", "Bearer $token") + } + return RequestEntity.get(url).headers(headers).build() + } + + private fun createGithubUserInformation( + oAuth2User: OAuth2User, + client: OAuth2AuthorizedClient + ): GithubUserInformation { + return GithubUserInformation( + githubId = getRequiredAttributeValue(oAuth2User, "id"), + username = getRequiredAttributeValue(oAuth2User, "login"), + email = getOptionalAttributeValue(oAuth2User, "email"), + profileUrl = getRequiredAttributeValue(oAuth2User, "html_url"), + avatarUrl = getRequiredAttributeValue(oAuth2User, "avatar_url"), + createdAt = getRequiredAttributeValue(oAuth2User, "created_at"), + updatedAt = getRequiredAttributeValue(oAuth2User, "updated_at"), + accessToken = client.accessToken.tokenValue, + tokenExpiration = client.accessToken.expiresAt.toString() + ) + } + + private fun getRequiredAttributeValue(oAuth2User: OAuth2User, attributeName: String): String { + return oAuth2User.attributes[attributeName]?.toString() + ?: throw IllegalStateException("필수 속성이 존재하지 않음") + } + + private fun getOptionalAttributeValue(oAuth2User: OAuth2User, attributeName: String): String? { + return oAuth2User.attributes[attributeName]?.toString() + } +} \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/config/ApplicationConfig.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/config/ApplicationConfig.kt new file mode 100644 index 0000000..1043249 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/config/ApplicationConfig.kt @@ -0,0 +1,15 @@ +package entry.dsm.gitauth.equusgithubauth.global.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.client.RestTemplate + +@Configuration +class ApplicationConfig { + + @Bean + fun restTemplate(): RestTemplate { + return RestTemplate() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubAuthenticationConfig.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubAuthenticationConfig.kt new file mode 100644 index 0000000..35c2a60 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubAuthenticationConfig.kt @@ -0,0 +1,23 @@ +package entry.dsm.gitauth.equusgithubauth.global.oauth + +import entry.dsm.gitauth.equusgithubauth.global.oauth.handler.GithubAuthenticationFailureHandler +import entry.dsm.gitauth.equusgithubauth.global.oauth.handler.GithubAuthenticationSuccessHandler +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity + +@Configuration +class GithubAuthenticationConfig{ + @Bean + fun githubAuthenticationSuccessHandler() = GithubAuthenticationSuccessHandler() + + @Bean + fun githubAuthenticationFailureHandler() = GithubAuthenticationFailureHandler() + + fun configureOAuth2Login(http: HttpSecurity) { + http.oauth2Login { + it.successHandler(githubAuthenticationSuccessHandler()) + it.failureHandler(githubAuthenticationFailureHandler()) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/handler/GithubAuthenticationFailureHandler.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/handler/GithubAuthenticationFailureHandler.kt new file mode 100644 index 0000000..f7a8e85 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/handler/GithubAuthenticationFailureHandler.kt @@ -0,0 +1,16 @@ +package entry.dsm.gitauth.equusgithubauth.global.oauth.handler + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.core.AuthenticationException +import org.springframework.security.web.authentication.AuthenticationFailureHandler + +class GithubAuthenticationFailureHandler : AuthenticationFailureHandler { + override fun onAuthenticationFailure( + request: HttpServletRequest?, + response: HttpServletResponse?, + exception: AuthenticationException? + ) { + response?.sendRedirect("/api/github/auth/not/authenticated") + } +} \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/handler/GithubAuthenticationSuccessHandler.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/handler/GithubAuthenticationSuccessHandler.kt new file mode 100644 index 0000000..f1afb18 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/handler/GithubAuthenticationSuccessHandler.kt @@ -0,0 +1,16 @@ +package entry.dsm.gitauth.equusgithubauth.global.oauth.handler + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.core.Authentication +import org.springframework.security.web.authentication.AuthenticationSuccessHandler + +class GithubAuthenticationSuccessHandler : AuthenticationSuccessHandler { + override fun onAuthenticationSuccess( + request: HttpServletRequest?, + response: HttpServletResponse?, + authentication: Authentication? + ) { + response?.sendRedirect("/api/github/auth/authenticated/") + } +} \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/security/SecurityConfig.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/security/SecurityConfig.kt new file mode 100644 index 0000000..8cd30fd --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/security/SecurityConfig.kt @@ -0,0 +1,26 @@ +package entry.dsm.gitauth.equusgithubauth.global.security + +import entry.dsm.gitauth.equusgithubauth.global.oauth.GithubAuthenticationConfig +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.web.SecurityFilterChain + +@Configuration +@EnableWebSecurity +class SecurityConfig { + + @Bean + fun securityFilterChain(http: HttpSecurity, githubAuthenticationConfig: GithubAuthenticationConfig): SecurityFilterChain { + http + .authorizeHttpRequests { auth -> + auth.requestMatchers("/login").permitAll() + auth.anyRequest().authenticated() + } + + githubAuthenticationConfig.configureOAuth2Login(http) + + return http.build() + } +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 2e1142a..9121141 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -19,8 +19,8 @@ spring: client: registration: github: - client-id: ${ GITHUB_CLIENT_ID } - client-secret: ${ GITHUB_CLIENT_SECRET } + client-id: ${GITHUB_CLIENT_ID} + client-secret: ${GITHUB_CLIENT_SECRET} scope: read:user, user:email redirect-uri: http://localhost:8080/login/oauth2/code/github authorization-grant-type: authorization_code @@ -31,4 +31,3 @@ spring: token-uri: https://github.com/login/oauth/access_token user-info-uri: https://api.github.com/user user-name-attribute: id - From a58e6669edd745a6f186bd487e086b76bcfa8d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Sun, 8 Dec 2024 01:51:15 +0900 Subject: [PATCH 09/21] feat : Add github login endpoint --- .../controller/GithubAuthenticationController.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt index b6dc66f..8651b73 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt @@ -13,6 +13,13 @@ import org.springframework.web.bind.annotation.RestController class GithubAuthenticationController( val githubUserService: GithubUserService ) { + + @GetMapping + fun githubAuth(): String { + return "redirect:/oauth2/authorization/github" + } + + // 이 주석 아래 메서드들은 테스트용 메서드 입니다. @GetMapping("/authenticated/") fun getGithubUserInfo(@AuthenticationPrincipal oAuth2User: OAuth2User): GithubUserInformation { return githubUserService.getGithubUserInformation(oAuth2User) From e18ca715791b6aee65b10189b3ebb54c281b0323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Sun, 8 Dec 2024 01:58:02 +0900 Subject: [PATCH 10/21] feat : (#2) Repair existing code --- .../domain/auth/service/GithubUserService.kt | 54 ++++++----------- .../GithubUserTokenValidationService.kt | 58 +++++++++++++++++++ .../service/GithubUserValidationService.kt | 56 ++++++++++++++++++ .../oauth/GithubAuthenticationConfig.kt | 23 -------- .../global/oauth/GithubOAuth2LoginConfig.kt | 42 ++++++++++++++ .../global/security/SecurityConfig.kt | 20 ++++--- 6 files changed, 188 insertions(+), 65 deletions(-) create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserValidationService.kt delete mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubAuthenticationConfig.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubOAuth2LoginConfig.kt diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt index 98e4f24..2a988dc 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt @@ -1,55 +1,39 @@ package entry.dsm.gitauth.equusgithubauth.domain.auth.service import entry.dsm.gitauth.equusgithubauth.domain.auth.presentation.dto.GithubUserInformation -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpStatus -import org.springframework.http.RequestEntity -import org.springframework.http.ResponseEntity +import org.slf4j.LoggerFactory import org.springframework.security.oauth2.client.OAuth2AuthorizedClient import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService import org.springframework.security.oauth2.core.user.OAuth2User import org.springframework.stereotype.Service -import org.springframework.web.client.RestTemplate @Service class GithubUserService( private val authorizedClientService: OAuth2AuthorizedClientService, - private val restTemplate: RestTemplate + private val githubUserValidationService: GithubUserValidationService, + private val githubUserTokenValidationService: GithubUserTokenValidationService ) { + private val logger = LoggerFactory.getLogger(GithubUserService::class.java) + fun getGithubUserInformation(oAuth2User: OAuth2User): GithubUserInformation { val client = getAuthorizedClient(oAuth2User) - validateAccessToken(client.accessToken.tokenValue) - return createGithubUserInformation(oAuth2User, client) - } - - private fun getAuthorizedClient(oAuth2User: OAuth2User): OAuth2AuthorizedClient { - return authorizedClientService.loadAuthorizedClient("github", oAuth2User.name) - ?: throw IllegalArgumentException("인증된 클라이언트를 찾을 수 없음") - } + githubUserTokenValidationService.validateAccessToken(client.accessToken.tokenValue) - private fun validateAccessToken(token: String) { - token.takeIf { it.isNotBlank() } ?: throw IllegalArgumentException("토큰이 비어있음") - if (!isTokenActive(token)) { - throw IllegalArgumentException("토큰이 만료되었거나 잘못된 토큰입니다.") - } - } - - private fun isTokenActive(token: String): Boolean { return try { - val request = buildGithubApiRequest(token) - val response: ResponseEntity = restTemplate.exchange(request, String::class.java) - response.statusCode == HttpStatus.OK - } catch (ex: Exception) { - false + if (!githubUserValidationService.validateUserMembership(client.accessToken.tokenValue, oAuth2User.attributes["login"].toString())) { + throw IllegalArgumentException("User is not a member of the organization.") + } + logger.info("Successfully validated user membership for: ${oAuth2User.attributes["login"]}") + createGithubUserInformation(oAuth2User, client) + } catch (e: Exception) { + logger.error("Error occurred while getting GitHub user information for ${oAuth2User.attributes["login"]}: ${e.message}", e) + throw IllegalStateException("Failed to get GitHub user information.", e) } } - private fun buildGithubApiRequest(token: String): RequestEntity { - val url = "https://api.github.com/user" - val headers = HttpHeaders().apply { - set("Authorization", "Bearer $token") - } - return RequestEntity.get(url).headers(headers).build() + private fun getAuthorizedClient(oAuth2User: OAuth2User): OAuth2AuthorizedClient { + return authorizedClientService.loadAuthorizedClient("github", oAuth2User.name) + ?: throw IllegalArgumentException("No authorized client found for the user ${oAuth2User.name}.") } private fun createGithubUserInformation( @@ -71,10 +55,10 @@ class GithubUserService( private fun getRequiredAttributeValue(oAuth2User: OAuth2User, attributeName: String): String { return oAuth2User.attributes[attributeName]?.toString() - ?: throw IllegalStateException("필수 속성이 존재하지 않음") + ?: throw IllegalStateException("Required attribute '$attributeName' is missing for user ${oAuth2User.attributes["login"]}.") } private fun getOptionalAttributeValue(oAuth2User: OAuth2User, attributeName: String): String? { return oAuth2User.attributes[attributeName]?.toString() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt new file mode 100644 index 0000000..cdbcc5b --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt @@ -0,0 +1,58 @@ +package entry.dsm.gitauth.equusgithubauth.domain.auth.service + +import org.slf4j.LoggerFactory +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.RequestEntity +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Service +import org.springframework.web.client.RestTemplate +import java.lang.Thread.sleep + +@Service +class GithubUserTokenValidationService( + private val restTemplate: RestTemplate +) { + private val logger = LoggerFactory.getLogger(GithubUserTokenValidationService::class.java) + + fun validateAccessToken(token: String) { + token.takeIf { it.isNotBlank() } ?: throw IllegalArgumentException("Access token is empty.") + val isTokenValid = retry(3, 2000) { isTokenActive(token) } // Add delay between retries + if (!isTokenValid!!) { + throw IllegalArgumentException("Access token is expired or invalid.") + } + } + + private fun retry(times: Int, delay: Long = 0L, block: () -> Boolean?): Boolean? { + repeat(times - 1) { + val result = try { + block() + } catch (e: Exception) { + logger.error("Retry failed: ${e.message}") + null + } + if (result == true) return result + Thread.sleep(delay) + } + return block() + } + + private fun isTokenActive(token: String): Boolean { + return try { + val request = buildGithubApiRequest(token) + val response: ResponseEntity = restTemplate.exchange(request, String::class.java) + response.statusCode == HttpStatus.OK + } catch (ex: Exception) { + logger.error("Error occurred while validating GitHub access token: ${ex.message}", ex) + false + } + } + + private fun buildGithubApiRequest(token: String): RequestEntity { + val url = "https://api.github.com/user" + val headers = HttpHeaders().apply { + set("Authorization", "Bearer $token") + } + return RequestEntity.get(url).headers(headers).build() + } +} diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserValidationService.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserValidationService.kt new file mode 100644 index 0000000..bed2460 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserValidationService.kt @@ -0,0 +1,56 @@ +package entry.dsm.gitauth.equusgithubauth.domain.auth.service + +import org.slf4j.LoggerFactory +import org.springframework.http.HttpHeaders +import org.springframework.http.RequestEntity +import org.springframework.stereotype.Service +import org.springframework.web.client.RestTemplate +import org.springframework.web.client.exchange + +@Service +class GithubUserValidationService( + private val restTemplate: RestTemplate +) { + private val logger = LoggerFactory.getLogger(GithubUserValidationService::class.java) + private val TARGET_ORGANIZATION = "EntryDSM" + + fun validateUserMembership(token: String, username: String): Boolean { + return try { + val userUrl = "https://api.github.com/user" + + val headers = HttpHeaders().apply { + set(HttpHeaders.AUTHORIZATION, "Bearer $token") + } + + val userRequest = RequestEntity.get(userUrl) + .headers(headers) + .build() + + val userResponse = restTemplate.exchange>(userRequest) + val currentUsername = userResponse.body?.get("login")?.toString() + + if (currentUsername != username) { + logger.error("Token username mismatch: $currentUsername != $username") + return false + } + + val organizationsUrl = "https://api.github.com/users/$username/orgs" + val orgsRequest = RequestEntity.get(organizationsUrl) + .headers(headers) + .build() + + val orgsResponse = restTemplate.exchange>>(orgsRequest) + + val isMemberOfOrg = orgsResponse.body?.any { + it["login"]?.toString() == TARGET_ORGANIZATION + } ?: false + + logger.info("Membership status for $username in $TARGET_ORGANIZATION: $isMemberOfOrg") + + isMemberOfOrg + } catch (e: Exception) { + logger.error("Error validating GitHub user membership for $username", e) + false + } + } +} diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubAuthenticationConfig.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubAuthenticationConfig.kt deleted file mode 100644 index 35c2a60..0000000 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubAuthenticationConfig.kt +++ /dev/null @@ -1,23 +0,0 @@ -package entry.dsm.gitauth.equusgithubauth.global.oauth - -import entry.dsm.gitauth.equusgithubauth.global.oauth.handler.GithubAuthenticationFailureHandler -import entry.dsm.gitauth.equusgithubauth.global.oauth.handler.GithubAuthenticationSuccessHandler -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.security.config.annotation.web.builders.HttpSecurity - -@Configuration -class GithubAuthenticationConfig{ - @Bean - fun githubAuthenticationSuccessHandler() = GithubAuthenticationSuccessHandler() - - @Bean - fun githubAuthenticationFailureHandler() = GithubAuthenticationFailureHandler() - - fun configureOAuth2Login(http: HttpSecurity) { - http.oauth2Login { - it.successHandler(githubAuthenticationSuccessHandler()) - it.failureHandler(githubAuthenticationFailureHandler()) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubOAuth2LoginConfig.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubOAuth2LoginConfig.kt new file mode 100644 index 0000000..81891a2 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubOAuth2LoginConfig.kt @@ -0,0 +1,42 @@ +package entry.dsm.gitauth.equusgithubauth.global.oauth + +import entry.dsm.gitauth.equusgithubauth.global.oauth.handler.GithubAuthenticationFailureHandler +import entry.dsm.gitauth.equusgithubauth.global.oauth.handler.GithubAuthenticationSuccessHandler +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.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver + +@Configuration +class GithubOAuth2LoginConfig { + + @Bean + fun githubAuthenticationSuccessHandler() = GithubAuthenticationSuccessHandler() + + @Bean + fun githubAuthenticationFailureHandler() = GithubAuthenticationFailureHandler() + + fun configure( + http: HttpSecurity, + clientRegistrationRepository: ClientRegistrationRepository + ) { + http.oauth2Login { oauth -> + oauth + .successHandler(githubAuthenticationSuccessHandler()) + .failureHandler(githubAuthenticationFailureHandler()) + .defaultSuccessUrl("/api/github/auth/authenticated/", true) + .failureUrl("/api/github/auth/not/authenticated") + .authorizationEndpoint { authorizationEndpoint -> + val defaultResolver = DefaultOAuth2AuthorizationRequestResolver( + clientRegistrationRepository, + "/oauth2/authorization" + ) + defaultResolver.setAuthorizationRequestCustomizer { builder -> + builder.scope("read:org") + } + authorizationEndpoint.authorizationRequestResolver(defaultResolver) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/security/SecurityConfig.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/security/SecurityConfig.kt index 8cd30fd..cd91d4a 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/security/SecurityConfig.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/security/SecurityConfig.kt @@ -1,25 +1,31 @@ package entry.dsm.gitauth.equusgithubauth.global.security -import entry.dsm.gitauth.equusgithubauth.global.oauth.GithubAuthenticationConfig 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.oauth2.client.registration.ClientRegistrationRepository import org.springframework.security.web.SecurityFilterChain +import entry.dsm.gitauth.equusgithubauth.global.oauth.GithubOAuth2LoginConfig @Configuration @EnableWebSecurity -class SecurityConfig { - +class SecurityConfig( + private val githubOAuth2LoginConfig: GithubOAuth2LoginConfig +) { @Bean - fun securityFilterChain(http: HttpSecurity, githubAuthenticationConfig: GithubAuthenticationConfig): SecurityFilterChain { + fun securityFilterChain( + http: HttpSecurity, + clientRegistrationRepository: ClientRegistrationRepository + ): SecurityFilterChain { http .authorizeHttpRequests { auth -> - auth.requestMatchers("/login").permitAll() - auth.anyRequest().authenticated() + auth + .requestMatchers("/", "/login", "/oauth2/**", "/error").permitAll() + .anyRequest().authenticated() } - githubAuthenticationConfig.configureOAuth2Login(http) + githubOAuth2LoginConfig.configure(http, clientRegistrationRepository) return http.build() } From bd50db71709def9bc5b7408ef8f23a57baa4835c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Sun, 8 Dec 2024 01:58:59 +0900 Subject: [PATCH 11/21] style : (#2) Delete unnecessary import statements --- .../domain/auth/service/GithubUserTokenValidationService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt index cdbcc5b..21d9655 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt @@ -7,7 +7,6 @@ import org.springframework.http.RequestEntity import org.springframework.http.ResponseEntity import org.springframework.stereotype.Service import org.springframework.web.client.RestTemplate -import java.lang.Thread.sleep @Service class GithubUserTokenValidationService( From af4778e34e7be7679b4d1c225e4c6970b14f0a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Sun, 8 Dec 2024 11:39:01 +0900 Subject: [PATCH 12/21] feat : (#2) Refactor GitHub authentication to use User entity and service --- .../GithubAuthenticationController.kt | 10 ++-- .../presentation/dto/GithubUserInformation.kt | 8 +-- .../domain/auth/service/GithubUserService.kt | 54 +++++++++++++------ .../domain/user/entity/User.kt | 6 ++- .../user/service/UserInformationService.kt | 32 +++++++++++ 5 files changed, 85 insertions(+), 25 deletions(-) create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/service/UserInformationService.kt diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt index 8651b73..3ea8869 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt @@ -1,7 +1,7 @@ package entry.dsm.gitauth.equusgithubauth.domain.auth.presentation.controller -import entry.dsm.gitauth.equusgithubauth.domain.auth.presentation.dto.GithubUserInformation -import entry.dsm.gitauth.equusgithubauth.domain.auth.service.GithubUserService +import entry.dsm.gitauth.equusgithubauth.domain.user.entity.User +import entry.dsm.gitauth.equusgithubauth.domain.user.service.UserInformationService import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.oauth2.core.user.OAuth2User import org.springframework.web.bind.annotation.GetMapping @@ -11,7 +11,7 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/github/auth") class GithubAuthenticationController( - val githubUserService: GithubUserService + val userInformationService: UserInformationService ) { @GetMapping @@ -21,8 +21,8 @@ class GithubAuthenticationController( // 이 주석 아래 메서드들은 테스트용 메서드 입니다. @GetMapping("/authenticated/") - fun getGithubUserInfo(@AuthenticationPrincipal oAuth2User: OAuth2User): GithubUserInformation { - return githubUserService.getGithubUserInformation(oAuth2User) + fun getGithubUserInfo(@AuthenticationPrincipal oAuth2User: OAuth2User): User { + return userInformationService.execute(oAuth2User) } @GetMapping("/not/authenticated/") diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/dto/GithubUserInformation.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/dto/GithubUserInformation.kt index 880e079..4cd2583 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/dto/GithubUserInformation.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/dto/GithubUserInformation.kt @@ -1,13 +1,15 @@ package entry.dsm.gitauth.equusgithubauth.domain.auth.presentation.dto +import java.time.LocalDateTime + data class GithubUserInformation( val githubId: String, val username: String, val email: String?, val profileUrl: String, val avatarUrl: String, - val createdAt: String, - val updatedAt: String, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, val accessToken: String, - val tokenExpiration: String + val tokenExpiration: LocalDateTime ) \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt index 2a988dc..4b37ef4 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt @@ -6,6 +6,9 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService import org.springframework.security.oauth2.core.user.OAuth2User import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter @Service class GithubUserService( @@ -14,26 +17,35 @@ class GithubUserService( private val githubUserTokenValidationService: GithubUserTokenValidationService ) { private val logger = LoggerFactory.getLogger(GithubUserService::class.java) + private val timestampFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME fun getGithubUserInformation(oAuth2User: OAuth2User): GithubUserInformation { - val client = getAuthorizedClient(oAuth2User) - githubUserTokenValidationService.validateAccessToken(client.accessToken.tokenValue) - return try { - if (!githubUserValidationService.validateUserMembership(client.accessToken.tokenValue, oAuth2User.attributes["login"].toString())) { - throw IllegalArgumentException("User is not a member of the organization.") - } - logger.info("Successfully validated user membership for: ${oAuth2User.attributes["login"]}") + val client = getAuthorizedClient(oAuth2User) + + githubUserTokenValidationService.validateAccessToken(client.accessToken.tokenValue) + + validateOrganizationMembership(client, oAuth2User) + createGithubUserInformation(oAuth2User, client) } catch (e: Exception) { - logger.error("Error occurred while getting GitHub user information for ${oAuth2User.attributes["login"]}: ${e.message}", e) - throw IllegalStateException("Failed to get GitHub user information.", e) + logger.error("GitHub 사용자 정보 취득 중 오류 발생: ${e.message}", e) + throw IllegalStateException("GitHub 사용자 정보를 가져올 수 없습니다.", e) } } private fun getAuthorizedClient(oAuth2User: OAuth2User): OAuth2AuthorizedClient { return authorizedClientService.loadAuthorizedClient("github", oAuth2User.name) - ?: throw IllegalArgumentException("No authorized client found for the user ${oAuth2User.name}.") + ?: throw IllegalArgumentException("사용자 ${oAuth2User.name}에 대한 인증된 클라이언트를 찾을 수 없습니다.") + } + + private fun validateOrganizationMembership(client: OAuth2AuthorizedClient, oAuth2User: OAuth2User) { + val username = oAuth2User.attributes["login"].toString() + if (!githubUserValidationService.validateUserMembership(client.accessToken.tokenValue, username)) { + logger.warn("조직 멤버십 검증 실패: $username") + throw IllegalArgumentException("사용자가 조직의 멤버가 아닙니다.") + } + logger.info("조직 멤버십 검증 성공: $username") } private fun createGithubUserInformation( @@ -46,19 +58,31 @@ class GithubUserService( email = getOptionalAttributeValue(oAuth2User, "email"), profileUrl = getRequiredAttributeValue(oAuth2User, "html_url"), avatarUrl = getRequiredAttributeValue(oAuth2User, "avatar_url"), - createdAt = getRequiredAttributeValue(oAuth2User, "created_at"), - updatedAt = getRequiredAttributeValue(oAuth2User, "updated_at"), + createdAt = parseTimestamp(getRequiredAttributeValue(oAuth2User, "created_at")), + updatedAt = parseTimestamp(getRequiredAttributeValue(oAuth2User, "updated_at")), accessToken = client.accessToken.tokenValue, - tokenExpiration = client.accessToken.expiresAt.toString() + tokenExpiration = parseTokenExpirationTime(client) + ) + } + + private fun parseTimestamp(timestamp: String): LocalDateTime { + return LocalDateTime.parse( + timestamp.replace("Z", ""), + timestampFormatter ) } + private fun parseTokenExpirationTime(client: OAuth2AuthorizedClient): LocalDateTime { + return client.accessToken.expiresAt?.atZone(ZoneId.systemDefault())?.toLocalDateTime() + ?: throw IllegalStateException("토큰 만료 시간이 누락되었습니다.") + } + private fun getRequiredAttributeValue(oAuth2User: OAuth2User, attributeName: String): String { return oAuth2User.attributes[attributeName]?.toString() - ?: throw IllegalStateException("Required attribute '$attributeName' is missing for user ${oAuth2User.attributes["login"]}.") + ?: throw IllegalStateException("사용자 ${oAuth2User.attributes["login"]}의 필수 속성 '$attributeName'이(가) 누락되었습니다.") } private fun getOptionalAttributeValue(oAuth2User: OAuth2User, attributeName: String): String? { return oAuth2User.attributes[attributeName]?.toString() } -} +} \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt index b6ed632..b043785 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt @@ -4,9 +4,11 @@ import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.Id import jakarta.persistence.Table +import lombok.Builder import java.time.LocalDateTime import java.util.* +@Builder @Entity @Table(name = "users") data class User( @@ -20,8 +22,8 @@ data class User( @Column(name = "username", nullable = false) val username: String, - @Column(name = "email", nullable = false, unique = true) - val email: String, + @Column(name = "email", nullable = true, unique = true) + val email: String?, @Column(name = "profile_url") val profileUrl: String? = null, diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/service/UserInformationService.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/service/UserInformationService.kt new file mode 100644 index 0000000..20bd4e8 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/service/UserInformationService.kt @@ -0,0 +1,32 @@ +package entry.dsm.gitauth.equusgithubauth.domain.user.service + +import entry.dsm.gitauth.equusgithubauth.domain.auth.presentation.dto.GithubUserInformation +import entry.dsm.gitauth.equusgithubauth.domain.auth.service.GithubUserService +import entry.dsm.gitauth.equusgithubauth.domain.user.entity.User +import entry.dsm.gitauth.equusgithubauth.domain.user.entity.repository.UserRepository +import org.springframework.security.oauth2.core.user.OAuth2User +import org.springframework.stereotype.Service + +@Service +class UserInformationService( + private val userRepository: UserRepository, + private val GithubUserService: GithubUserService +) { + fun execute(oAuth2User: OAuth2User) : User { + val userInfo = GithubUserService.getGithubUserInformation(oAuth2User) + + val user = User( + githubId = userInfo.githubId, + username = userInfo.username, + email = userInfo.email, + profileUrl = userInfo.profileUrl, + avatarUrl = userInfo.avatarUrl, + createdAt = userInfo.createdAt, + updatedAt = userInfo.updatedAt, + accessToken = userInfo.accessToken, + tokenExpiration = userInfo.tokenExpiration + ) + + return userRepository.save(user) + } +} \ No newline at end of file From b6474d0f6ca7140da2424cf9e4fde8ca5b50ecad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Sun, 8 Dec 2024 14:28:05 +0900 Subject: [PATCH 13/21] refactor: (#2) improve package structure --- .../global/{security => config}/SecurityConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/{security => config}/SecurityConfig.kt (95%) diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/security/SecurityConfig.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/config/SecurityConfig.kt similarity index 95% rename from src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/security/SecurityConfig.kt rename to src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/config/SecurityConfig.kt index cd91d4a..75c3e0e 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/security/SecurityConfig.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/config/SecurityConfig.kt @@ -1,4 +1,4 @@ -package entry.dsm.gitauth.equusgithubauth.global.security +package entry.dsm.gitauth.equusgithubauth.global.config import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration From 94f2b7fc4d94033e498602c4b1eef702bbd1e3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Thu, 19 Dec 2024 10:47:50 +0900 Subject: [PATCH 14/21] chore : (#2) Update Readme --- readme.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 404ff91..9adba19 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ ```mermaid sequenceDiagram participant User as User - participant App as Equus-Github-Auth System + participant App as Casper-Internship participant GitHub as GitHub User ->> App: Login Request @@ -21,5 +21,4 @@ sequenceDiagram else User is not a member of EntryDSM App ->> User: Login Fail end -``` - +``` \ No newline at end of file From d0dd478f87267e6140a71813dd3df1982eb94606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Thu, 19 Dec 2024 12:00:16 +0900 Subject: [PATCH 15/21] feat :: (#2) springboot version up for spring cloud openfeign --- build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index c2567e8..589e7c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { id("org.jetbrains.kotlin.jvm") version "1.9.25" id("org.jetbrains.kotlin.plugin.spring") version "1.9.25" - id("org.springframework.boot") version "3.3.5" + id("org.springframework.boot") version "3.4.0" id("io.spring.dependency-management") version "1.1.6" id("org.jetbrains.kotlin.plugin.jpa") version "1.9.25" } @@ -31,6 +31,7 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") runtimeOnly("com.mysql:mysql-connector-j") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.2.0") } kotlin { From e33758cc11a91c6f84e5a9a1c570a4ec9e17d9da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Thu, 19 Dec 2024 12:03:07 +0900 Subject: [PATCH 16/21] feat : (#2) Refactoring using spring cloud openfeign --- .../EquusGithubAuthApplication.kt | 2 + .../domain/auth/service/GithubUserService.kt | 5 ++- .../service/GithubUserValidationService.kt | 42 ++++++------------- .../user/entity/repository/UserRepository.kt | 4 +- .../user/service/UserInformationService.kt | 3 ++ .../presentation/controller/GithubClient.kt | 20 +++++++++ .../dto/GithubOrganizationResponse.kt | 5 +++ .../presentation/dto/GithubUserResponse.kt | 5 +++ .../external/service/TokenAuthenticator.kt | 10 +++++ .../global/oauth/GithubOAuth2LoginConfig.kt | 2 - 10 files changed, 64 insertions(+), 34 deletions(-) create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/controller/GithubClient.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/dto/GithubOrganizationResponse.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/dto/GithubUserResponse.kt create mode 100644 src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/service/TokenAuthenticator.kt diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/EquusGithubAuthApplication.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/EquusGithubAuthApplication.kt index fad5db1..14fb10e 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/EquusGithubAuthApplication.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/EquusGithubAuthApplication.kt @@ -2,7 +2,9 @@ package entry.dsm.gitauth.equusgithubauth import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.cloud.openfeign.EnableFeignClients +@EnableFeignClients @SpringBootApplication class EquusGithubAuthApplication diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt index 4b37ef4..0e7238a 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt @@ -1,6 +1,7 @@ package entry.dsm.gitauth.equusgithubauth.domain.auth.service import entry.dsm.gitauth.equusgithubauth.domain.auth.presentation.dto.GithubUserInformation +import entry.dsm.gitauth.equusgithubauth.domain.user.entity.repository.UserRepository import org.slf4j.LoggerFactory import org.springframework.security.oauth2.client.OAuth2AuthorizedClient import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService @@ -14,7 +15,7 @@ import java.time.format.DateTimeFormatter class GithubUserService( private val authorizedClientService: OAuth2AuthorizedClientService, private val githubUserValidationService: GithubUserValidationService, - private val githubUserTokenValidationService: GithubUserTokenValidationService + private val githubUserTokenValidationService: GithubUserTokenValidationService, ) { private val logger = LoggerFactory.getLogger(GithubUserService::class.java) private val timestampFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME @@ -27,7 +28,7 @@ class GithubUserService( validateOrganizationMembership(client, oAuth2User) - createGithubUserInformation(oAuth2User, client) + return createGithubUserInformation(oAuth2User, client) } catch (e: Exception) { logger.error("GitHub 사용자 정보 취득 중 오류 발생: ${e.message}", e) throw IllegalStateException("GitHub 사용자 정보를 가져올 수 없습니다.", e) diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserValidationService.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserValidationService.kt index bed2460..203dc67 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserValidationService.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserValidationService.kt @@ -1,52 +1,36 @@ package entry.dsm.gitauth.equusgithubauth.domain.auth.service +import entry.dsm.gitauth.equusgithubauth.domain.auth.client.GithubApiClient +import entry.dsm.gitauth.equusgithubauth.global.external.github.presentation.dto.GithubOrganizationResponse +import entry.dsm.gitauth.equusgithubauth.global.external.service.TokenAuthenticator import org.slf4j.LoggerFactory -import org.springframework.http.HttpHeaders -import org.springframework.http.RequestEntity import org.springframework.stereotype.Service -import org.springframework.web.client.RestTemplate -import org.springframework.web.client.exchange @Service class GithubUserValidationService( - private val restTemplate: RestTemplate + private val githubApiClient: GithubApiClient, + private val tokenAuthenticator: TokenAuthenticator ) { private val logger = LoggerFactory.getLogger(GithubUserValidationService::class.java) - private val TARGET_ORGANIZATION = "EntryDSM" + + companion object { + private const val TARGET_ORGANIZATION = "EntryDSM" + } fun validateUserMembership(token: String, username: String): Boolean { return try { - val userUrl = "https://api.github.com/user" - - val headers = HttpHeaders().apply { - set(HttpHeaders.AUTHORIZATION, "Bearer $token") - } - - val userRequest = RequestEntity.get(userUrl) - .headers(headers) - .build() - - val userResponse = restTemplate.exchange>(userRequest) - val currentUsername = userResponse.body?.get("login")?.toString() + val authorizationHeader = tokenAuthenticator.createAuthorizationHeader(token) + val currentUsername = githubApiClient.getUser(authorizationHeader).login if (currentUsername != username) { logger.error("Token username mismatch: $currentUsername != $username") return false } - val organizationsUrl = "https://api.github.com/users/$username/orgs" - val orgsRequest = RequestEntity.get(organizationsUrl) - .headers(headers) - .build() - - val orgsResponse = restTemplate.exchange>>(orgsRequest) - - val isMemberOfOrg = orgsResponse.body?.any { - it["login"]?.toString() == TARGET_ORGANIZATION - } ?: false + val isMemberOfOrg = githubApiClient.getUserOrganizations(authorizationHeader, username) + .any { org: GithubOrganizationResponse -> org.login == TARGET_ORGANIZATION } logger.info("Membership status for $username in $TARGET_ORGANIZATION: $isMemberOfOrg") - isMemberOfOrg } catch (e: Exception) { logger.error("Error validating GitHub user membership for $username", e) diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt index 38eb70d..79ff55f 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt @@ -3,4 +3,6 @@ package entry.dsm.gitauth.equusgithubauth.domain.user.entity.repository import org.springframework.data.jpa.repository.JpaRepository import entry.dsm.gitauth.equusgithubauth.domain.user.entity.User -interface UserRepository : JpaRepository \ No newline at end of file +interface UserRepository : JpaRepository { + fun findByGithubId(githubId: String): User? +} \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/service/UserInformationService.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/service/UserInformationService.kt index 20bd4e8..773a2d7 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/service/UserInformationService.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/service/UserInformationService.kt @@ -27,6 +27,9 @@ class UserInformationService( tokenExpiration = userInfo.tokenExpiration ) + if (userRepository.findByGithubId(userInfo.githubId) != null) { + return userRepository.findByGithubId(userInfo.githubId)!! + } return userRepository.save(user) } } \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/controller/GithubClient.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/controller/GithubClient.kt new file mode 100644 index 0000000..97030e6 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/controller/GithubClient.kt @@ -0,0 +1,20 @@ +package entry.dsm.gitauth.equusgithubauth.domain.auth.client + +import entry.dsm.gitauth.equusgithubauth.global.external.github.presentation.dto.GithubOrganizationResponse +import entry.dsm.gitauth.equusgithubauth.global.external.github.presentation.dto.GithubUserResponse +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.PathVariable + +@FeignClient(name = "githubApiClient", url = "https://api.github.com") +interface GithubApiClient { + @GetMapping("/user") + fun getUser(@RequestHeader("Authorization") authorization: String): GithubUserResponse + + @GetMapping("/users/{username}/orgs") + fun getUserOrganizations( + @RequestHeader("Authorization") authorization: String, + @PathVariable username: String + ): List +} diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/dto/GithubOrganizationResponse.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/dto/GithubOrganizationResponse.kt new file mode 100644 index 0000000..5f7148b --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/dto/GithubOrganizationResponse.kt @@ -0,0 +1,5 @@ +package entry.dsm.gitauth.equusgithubauth.global.external.github.presentation.dto + +data class GithubOrganizationResponse( + val login: String, +) \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/dto/GithubUserResponse.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/dto/GithubUserResponse.kt new file mode 100644 index 0000000..7e6caeb --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/dto/GithubUserResponse.kt @@ -0,0 +1,5 @@ +package entry.dsm.gitauth.equusgithubauth.global.external.github.presentation.dto + +data class GithubUserResponse( + val login: String, +) \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/service/TokenAuthenticator.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/service/TokenAuthenticator.kt new file mode 100644 index 0000000..5411ebe --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/service/TokenAuthenticator.kt @@ -0,0 +1,10 @@ +package entry.dsm.gitauth.equusgithubauth.global.external.service + +import org.springframework.stereotype.Component + +@Component +class TokenAuthenticator { + fun createAuthorizationHeader(token: String): String { + return "Bearer $token" + } +} \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubOAuth2LoginConfig.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubOAuth2LoginConfig.kt index 81891a2..ea9f422 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubOAuth2LoginConfig.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubOAuth2LoginConfig.kt @@ -25,8 +25,6 @@ class GithubOAuth2LoginConfig { oauth .successHandler(githubAuthenticationSuccessHandler()) .failureHandler(githubAuthenticationFailureHandler()) - .defaultSuccessUrl("/api/github/auth/authenticated/", true) - .failureUrl("/api/github/auth/not/authenticated") .authorizationEndpoint { authorizationEndpoint -> val defaultResolver = DefaultOAuth2AuthorizationRequestResolver( clientRegistrationRepository, From 10961a322c5bf4f2e14f511c7802dce7838e298d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= <161235128+kangeunchan@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:06:16 +0900 Subject: [PATCH 17/21] fix : (#2) Change primary key type in UserRepository from Long to UUID to prevent runtime errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 도경 <125863754+rudeh2926@users.noreply.github.com> --- .../domain/user/entity/repository/UserRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt index 79ff55f..8641746 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt @@ -3,6 +3,6 @@ package entry.dsm.gitauth.equusgithubauth.domain.user.entity.repository import org.springframework.data.jpa.repository.JpaRepository import entry.dsm.gitauth.equusgithubauth.domain.user.entity.User -interface UserRepository : JpaRepository { +interface UserRepository : JpaRepository { fun findByGithubId(githubId: String): User? } \ No newline at end of file From e900a217250feb5f72be95cd6d5d573d1e77ff72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Fri, 20 Dec 2024 08:59:16 +0900 Subject: [PATCH 18/21] feat : (#2) Changed to use github api client --- .../GithubUserTokenValidationService.kt | 51 +++---------------- .../{GithubClient.kt => GithubApiClient.kt} | 0 2 files changed, 6 insertions(+), 45 deletions(-) rename src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/controller/{GithubClient.kt => GithubApiClient.kt} (100%) diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt index 21d9655..c6b99f2 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt @@ -1,57 +1,18 @@ package entry.dsm.gitauth.equusgithubauth.domain.auth.service -import org.slf4j.LoggerFactory -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpStatus -import org.springframework.http.RequestEntity -import org.springframework.http.ResponseEntity +import entry.dsm.gitauth.equusgithubauth.domain.auth.client.GithubApiClient import org.springframework.stereotype.Service -import org.springframework.web.client.RestTemplate @Service class GithubUserTokenValidationService( - private val restTemplate: RestTemplate + private val githubClient: GithubApiClient ) { - private val logger = LoggerFactory.getLogger(GithubUserTokenValidationService::class.java) - fun validateAccessToken(token: String) { - token.takeIf { it.isNotBlank() } ?: throw IllegalArgumentException("Access token is empty.") - val isTokenValid = retry(3, 2000) { isTokenActive(token) } // Add delay between retries - if (!isTokenValid!!) { - throw IllegalArgumentException("Access token is expired or invalid.") - } - } - - private fun retry(times: Int, delay: Long = 0L, block: () -> Boolean?): Boolean? { - repeat(times - 1) { - val result = try { - block() - } catch (e: Exception) { - logger.error("Retry failed: ${e.message}") - null - } - if (result == true) return result - Thread.sleep(delay) - } - return block() - } - - private fun isTokenActive(token: String): Boolean { - return try { - val request = buildGithubApiRequest(token) - val response: ResponseEntity = restTemplate.exchange(request, String::class.java) - response.statusCode == HttpStatus.OK + require(token.isNotBlank()) { "Access token is empty." } + try { + githubClient.getUser("Bearer $token") } catch (ex: Exception) { - logger.error("Error occurred while validating GitHub access token: ${ex.message}", ex) - false - } - } - - private fun buildGithubApiRequest(token: String): RequestEntity { - val url = "https://api.github.com/user" - val headers = HttpHeaders().apply { - set("Authorization", "Bearer $token") + throw IllegalArgumentException("Access token is expired or invalid.") } - return RequestEntity.get(url).headers(headers).build() } } diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/controller/GithubClient.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/controller/GithubApiClient.kt similarity index 100% rename from src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/controller/GithubClient.kt rename to src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/controller/GithubApiClient.kt From 3ac718b5d58a90c0f6acea99612efe7160d36cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Fri, 20 Dec 2024 09:02:56 +0900 Subject: [PATCH 19/21] feat : (#2) Remove builder annotation in user data class --- .../dsm/gitauth/equusgithubauth/domain/user/entity/User.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt index b043785..ab3f848 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt @@ -4,11 +4,9 @@ import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.Id import jakarta.persistence.Table -import lombok.Builder import java.time.LocalDateTime import java.util.* -@Builder @Entity @Table(name = "users") data class User( From 2c3e9237b796e75ea5f9abf55d200393f78571d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Fri, 20 Dec 2024 09:04:58 +0900 Subject: [PATCH 20/21] fix : (#2) Unresolved reference: UUID --- .../domain/user/entity/repository/UserRepository.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt index 8641746..409e3d4 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt @@ -2,6 +2,7 @@ package entry.dsm.gitauth.equusgithubauth.domain.user.entity.repository import org.springframework.data.jpa.repository.JpaRepository import entry.dsm.gitauth.equusgithubauth.domain.user.entity.User +import java.util.* interface UserRepository : JpaRepository { fun findByGithubId(githubId: String): User? From fe5fa8e8a66caa6aca877efaa525a317ffdbd26b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=80=EC=B0=AC?= Date: Fri, 20 Dec 2024 09:16:47 +0900 Subject: [PATCH 21/21] feat : (#2) Change GithubUserTokenValidationService use TokenAuthenticator --- .../domain/auth/service/GithubUserTokenValidationService.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt index c6b99f2..880b7a7 100644 --- a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt @@ -1,16 +1,18 @@ package entry.dsm.gitauth.equusgithubauth.domain.auth.service import entry.dsm.gitauth.equusgithubauth.domain.auth.client.GithubApiClient +import entry.dsm.gitauth.equusgithubauth.global.external.service.TokenAuthenticator import org.springframework.stereotype.Service @Service class GithubUserTokenValidationService( - private val githubClient: GithubApiClient + private val githubClient: GithubApiClient, + private val tokenAuthenticator: TokenAuthenticator ) { fun validateAccessToken(token: String) { require(token.isNotBlank()) { "Access token is empty." } try { - githubClient.getUser("Bearer $token") + githubClient.getUser(tokenAuthenticator.createAuthorizationHeader(token)) } catch (ex: Exception) { throw IllegalArgumentException("Access token is expired or invalid.") }