-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from EntryDSM/feature/2-develop-github-oauth
Feature/2 develop GitHub oauth
- Loading branch information
Showing
24 changed files
with
533 additions
and
47 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
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.4.0" | ||
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") | ||
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.2.0") | ||
} | ||
|
||
kotlin { | ||
compilerOptions { | ||
freeCompilerArgs.addAll("-Xjsr305=strict") | ||
} | ||
} | ||
|
||
noArg { | ||
annotation("jakarta.persistence.Entity") | ||
} | ||
|
||
tasks.test { | ||
useJUnitPlatform() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
...uth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package entry.dsm.gitauth.equusgithubauth.domain.auth.presentation.controller | ||
|
||
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 | ||
import org.springframework.web.bind.annotation.RequestMapping | ||
import org.springframework.web.bind.annotation.RestController | ||
|
||
@RestController | ||
@RequestMapping("/api/github/auth") | ||
class GithubAuthenticationController( | ||
val userInformationService: UserInformationService | ||
) { | ||
|
||
@GetMapping | ||
fun githubAuth(): String { | ||
return "redirect:/oauth2/authorization/github" | ||
} | ||
|
||
// 이 주석 아래 메서드들은 테스트용 메서드 입니다. | ||
@GetMapping("/authenticated/") | ||
fun getGithubUserInfo(@AuthenticationPrincipal oAuth2User: OAuth2User): User { | ||
return userInformationService.execute(oAuth2User) | ||
} | ||
|
||
@GetMapping("/not/authenticated/") | ||
fun notAuthenticated(): String { | ||
return "Not authenticated" | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
...n/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/dto/GithubUserInformation.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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: LocalDateTime, | ||
val updatedAt: LocalDateTime, | ||
val accessToken: String, | ||
val tokenExpiration: LocalDateTime | ||
) |
89 changes: 89 additions & 0 deletions
89
src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
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 | ||
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( | ||
private val authorizedClientService: OAuth2AuthorizedClientService, | ||
private val githubUserValidationService: GithubUserValidationService, | ||
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 { | ||
return try { | ||
val client = getAuthorizedClient(oAuth2User) | ||
|
||
githubUserTokenValidationService.validateAccessToken(client.accessToken.tokenValue) | ||
|
||
validateOrganizationMembership(client, oAuth2User) | ||
|
||
return createGithubUserInformation(oAuth2User, client) | ||
} catch (e: Exception) { | ||
logger.error("GitHub 사용자 정보 취득 중 오류 발생: ${e.message}", e) | ||
throw IllegalStateException("GitHub 사용자 정보를 가져올 수 없습니다.", e) | ||
} | ||
} | ||
|
||
private fun getAuthorizedClient(oAuth2User: OAuth2User): OAuth2AuthorizedClient { | ||
return authorizedClientService.loadAuthorizedClient("github", 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( | ||
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 = parseTimestamp(getRequiredAttributeValue(oAuth2User, "created_at")), | ||
updatedAt = parseTimestamp(getRequiredAttributeValue(oAuth2User, "updated_at")), | ||
accessToken = client.accessToken.tokenValue, | ||
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("사용자 ${oAuth2User.attributes["login"]}의 필수 속성 '$attributeName'이(가) 누락되었습니다.") | ||
} | ||
|
||
private fun getOptionalAttributeValue(oAuth2User: OAuth2User, attributeName: String): String? { | ||
return oAuth2User.attributes[attributeName]?.toString() | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
...entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
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 tokenAuthenticator: TokenAuthenticator | ||
) { | ||
fun validateAccessToken(token: String) { | ||
require(token.isNotBlank()) { "Access token is empty." } | ||
try { | ||
githubClient.getUser(tokenAuthenticator.createAuthorizationHeader(token)) | ||
} catch (ex: Exception) { | ||
throw IllegalArgumentException("Access token is expired or invalid.") | ||
} | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
...tlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserValidationService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
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.stereotype.Service | ||
|
||
@Service | ||
class GithubUserValidationService( | ||
private val githubApiClient: GithubApiClient, | ||
private val tokenAuthenticator: TokenAuthenticator | ||
) { | ||
private val logger = LoggerFactory.getLogger(GithubUserValidationService::class.java) | ||
|
||
companion object { | ||
private const val TARGET_ORGANIZATION = "EntryDSM" | ||
} | ||
|
||
fun validateUserMembership(token: String, username: String): Boolean { | ||
return try { | ||
val authorizationHeader = tokenAuthenticator.createAuthorizationHeader(token) | ||
|
||
val currentUsername = githubApiClient.getUser(authorizationHeader).login | ||
if (currentUsername != username) { | ||
logger.error("Token username mismatch: $currentUsername != $username") | ||
return 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) | ||
false | ||
} | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/User.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = true, 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 | ||
) |
9 changes: 9 additions & 0 deletions
9
.../kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
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<User, UUID> { | ||
fun findByGithubId(githubId: String): User? | ||
} |
Oops, something went wrong.