diff --git a/build.gradle b/build.gradle deleted file mode 100644 index bfba86d..0000000 --- a/build.gradle +++ /dev/null @@ -1,40 +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.jetbrains.kotlin:kotlin-reflect' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' - 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..589e7c7 --- /dev/null +++ b/build.gradle.kts @@ -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() +} \ No newline at end of file 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 diff --git a/scheme.md b/scheme.md index fddd625..a6bd9be 100644 --- a/scheme.md +++ b/scheme.md @@ -3,15 +3,14 @@ ```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" VARCHAR avatar_url "GitHub 아바타 이미지 URL" TIMESTAMP created_at "생성 시간" TIMESTAMP updated_at "갱신 시간" - BOOLEAN is_member_of_org "조직 소속 여부" VARCHAR access_token "내부 애플리케이션 액세스 토큰" TIMESTAMP token_expiration "토큰 만료 시간" } 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/presentation/controller/GithubAuthenticationController.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt new file mode 100644 index 0000000..3ea8869 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/controller/GithubAuthenticationController.kt @@ -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" + } +} \ 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..4cd2583 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/presentation/dto/GithubUserInformation.kt @@ -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 +) \ 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..0e7238a --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserService.kt @@ -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() + } +} \ 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..880b7a7 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserTokenValidationService.kt @@ -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.") + } + } +} 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..203dc67 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/auth/service/GithubUserValidationService.kt @@ -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 + } + } +} 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..ab3f848 --- /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 = 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 +) 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..409e3d4 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/entity/repository/UserRepository.kt @@ -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 { + 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 new file mode 100644 index 0000000..773a2d7 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/domain/user/service/UserInformationService.kt @@ -0,0 +1,35 @@ +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 + ) + + 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/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/config/SecurityConfig.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/config/SecurityConfig.kt new file mode 100644 index 0000000..75c3e0e --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/config/SecurityConfig.kt @@ -0,0 +1,32 @@ +package entry.dsm.gitauth.equusgithubauth.global.config + +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( + private val githubOAuth2LoginConfig: GithubOAuth2LoginConfig +) { + @Bean + fun securityFilterChain( + http: HttpSecurity, + clientRegistrationRepository: ClientRegistrationRepository + ): SecurityFilterChain { + http + .authorizeHttpRequests { auth -> + auth + .requestMatchers("/", "/login", "/oauth2/**", "/error").permitAll() + .anyRequest().authenticated() + } + + githubOAuth2LoginConfig.configure(http, clientRegistrationRepository) + + return http.build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/controller/GithubApiClient.kt b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/controller/GithubApiClient.kt new file mode 100644 index 0000000..97030e6 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/external/github/presentation/controller/GithubApiClient.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 new file mode 100644 index 0000000..ea9f422 --- /dev/null +++ b/src/main/kotlin/entry/dsm/gitauth/equusgithubauth/global/oauth/GithubOAuth2LoginConfig.kt @@ -0,0 +1,40 @@ +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()) + .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/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/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..9121141 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,33 @@ +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