-
Notifications
You must be signed in to change notification settings - Fork 4
[FEAT] 로그인 화면 API 연결 #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0960581
4a531e1
de08604
2513f64
73014ed
3a35802
2fd2c9f
ccb686f
ff0eb96
66c62ba
0a16530
6ed7154
07575cd
ac296f0
401ec81
f18c0c3
0b35a22
9065a1b
cc0320c
8121194
51d143a
ff9057a
4f8acbd
38c1a9d
505bea0
9be520c
784ac98
148128f
691d649
76de901
8571dde
f241086
8623ced
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package org.whosin.client.core.auth | ||
|
|
||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.asStateFlow | ||
|
|
||
| object TokenExpiredManager { | ||
| private val _isTokenExpired = MutableStateFlow(false) | ||
| val isTokenExpired: StateFlow<Boolean> = _isTokenExpired.asStateFlow() | ||
|
|
||
| fun setTokenExpired() { | ||
| _isTokenExpired.value = true | ||
| } | ||
|
|
||
| fun reset() { | ||
| _isTokenExpired.value = false | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -21,9 +21,10 @@ import io.ktor.http.encodedPath | |||||||||||||||||||||||||||
| import io.ktor.serialization.kotlinx.json.json | ||||||||||||||||||||||||||||
| import kotlinx.serialization.json.Json | ||||||||||||||||||||||||||||
| import org.whosin.client.BuildKonfig | ||||||||||||||||||||||||||||
| import org.whosin.client.core.auth.TokenExpiredManager | ||||||||||||||||||||||||||||
| import org.whosin.client.core.datastore.TokenManager | ||||||||||||||||||||||||||||
| import org.whosin.client.data.dto.request.ReissueTokenRequestDto | ||||||||||||||||||||||||||||
| import org.whosin.client.data.dto.response.TokenDto | ||||||||||||||||||||||||||||
| import org.whosin.client.data.dto.response.ReissueTokenResponseDto | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| object HttpClientFactory { | ||||||||||||||||||||||||||||
| val BASE_URL = BuildKonfig.BASE_URL | ||||||||||||||||||||||||||||
|
|
@@ -45,10 +46,11 @@ object HttpClientFactory { | |||||||||||||||||||||||||||
| socketTimeoutMillis = 20_000L | ||||||||||||||||||||||||||||
| requestTimeoutMillis = 20_000L | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| install(Auth){ | ||||||||||||||||||||||||||||
| install(Auth) { | ||||||||||||||||||||||||||||
| bearer { | ||||||||||||||||||||||||||||
| loadTokens { | ||||||||||||||||||||||||||||
| val accessToken = tokenManager.getAccessToken() ?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk" | ||||||||||||||||||||||||||||
| val accessToken = tokenManager.getAccessToken() | ||||||||||||||||||||||||||||
| ?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk" | ||||||||||||||||||||||||||||
| val refreshToken = tokenManager.getRefreshToken() ?: "no_token" | ||||||||||||||||||||||||||||
| BearerTokens(accessToken = accessToken, refreshToken = refreshToken) | ||||||||||||||||||||||||||||
|
Comment on lines
+52
to
55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 하드코딩된 JWT 제거 필요 Line 52~55에서 액세스 토큰과 리프레시 토큰이 하드코딩된 문자열로 대체되고 있습니다. 인증이 되지 않은 상태에서도 이 더미 토큰이 모든 요청 헤더에 실리기 때문에, 실서버로 빌드가 나가면 불특정 사용자에게 동일한 권한이 노출되는 치명적인 보안 사고로 이어질 수 있습니다. 또한 정적 분석(gitleaks)도 JWT 유출로 경고하고 있습니다. 토큰이 없으면 - loadTokens {
- val accessToken = tokenManager.getAccessToken()
- ?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk"
- val refreshToken = tokenManager.getRefreshToken() ?: "no_token"
- BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
- }
+ loadTokens {
+ val accessToken = tokenManager.getAccessToken()
+ val refreshToken = tokenManager.getRefreshToken()
+ if (accessToken != null && refreshToken != null) {
+ BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
+ } else {
+ null
+ }
+ }📝 Committable suggestion
Suggested change
🧰 Tools🪛 Gitleaks (8.28.0)[high] 53-53: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data. (jwt) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
@@ -75,7 +77,7 @@ object HttpClientFactory { | |||||||||||||||||||||||||||
| "auth/login", | ||||||||||||||||||||||||||||
| "auth/email", | ||||||||||||||||||||||||||||
| "auth/email/validation", | ||||||||||||||||||||||||||||
| "member/reissue" // 토큰 재발급 요청 자체에는 만료된 액세스 토큰을 보내면 안 됨 | ||||||||||||||||||||||||||||
| "auth/reissue" // 토큰 재발급 요청 | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| val isNoAuthPath = pathWithNoAuth.any { noAuthPath -> | ||||||||||||||||||||||||||||
|
|
@@ -88,26 +90,42 @@ object HttpClientFactory { | |||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| refreshTokens { | ||||||||||||||||||||||||||||
| val rt = tokenManager.getRefreshToken() ?: "no_token" | ||||||||||||||||||||||||||||
| val response = client.post("member/reissue"){ | ||||||||||||||||||||||||||||
| setBody { | ||||||||||||||||||||||||||||
| ReissueTokenRequestDto( | ||||||||||||||||||||||||||||
| refreshToken = rt | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| val rt = tokenManager.getRefreshToken() | ||||||||||||||||||||||||||||
| if (rt.isNullOrEmpty()) { | ||||||||||||||||||||||||||||
| tokenManager.clearToken() | ||||||||||||||||||||||||||||
| TokenExpiredManager.setTokenExpired() | ||||||||||||||||||||||||||||
| return@refreshTokens null | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| val response = client.post("auth/reissue") { | ||||||||||||||||||||||||||||
| setBody(ReissueTokenRequestDto(refreshToken = rt)) | ||||||||||||||||||||||||||||
| markAsRefreshTokenRequest() | ||||||||||||||||||||||||||||
| }.body<ReissueTokenResponseDto>() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (response.success && response.data != null) { | ||||||||||||||||||||||||||||
| tokenManager.saveTokens( | ||||||||||||||||||||||||||||
| accessToken = response.data.accessToken, | ||||||||||||||||||||||||||||
| refreshToken = response.data.refreshToken | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| BearerTokens( | ||||||||||||||||||||||||||||
| accessToken = response.data.accessToken, | ||||||||||||||||||||||||||||
| refreshToken = response.data.refreshToken | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||
| tokenManager.clearToken() | ||||||||||||||||||||||||||||
| TokenExpiredManager.setTokenExpired() | ||||||||||||||||||||||||||||
| null | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| markAsRefreshTokenRequest() | ||||||||||||||||||||||||||||
| }.body<TokenDto>() | ||||||||||||||||||||||||||||
| tokenManager.saveTokens( | ||||||||||||||||||||||||||||
| accessToken = response.accessToken, | ||||||||||||||||||||||||||||
| refreshToken = response.refreshToken | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| val accessToken = response.accessToken | ||||||||||||||||||||||||||||
| val refreshToken = response.refreshToken | ||||||||||||||||||||||||||||
| BearerTokens(accessToken,refreshToken) | ||||||||||||||||||||||||||||
| } catch (e: Exception) { | ||||||||||||||||||||||||||||
| tokenManager.clearToken() | ||||||||||||||||||||||||||||
| TokenExpiredManager.setTokenExpired() | ||||||||||||||||||||||||||||
| null | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| install(Logging){ | ||||||||||||||||||||||||||||
| install(Logging) { | ||||||||||||||||||||||||||||
| logger = object : Logger { | ||||||||||||||||||||||||||||
| override fun log(message: String) { | ||||||||||||||||||||||||||||
| println(message) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package org.whosin.client.data.dto.request | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class EmailValidationRequestDto( | ||
| @SerialName("email") | ||
| val email: String, | ||
| @SerialName("authCode") | ||
| val authCode: String | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package org.whosin.client.data.dto.request | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class EmailVerificationRequestDto( | ||
| @SerialName("email") | ||
| val email: String | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package org.whosin.client.data.dto.request | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class FindPasswordRequestDto( | ||
| @SerialName("email") | ||
| val email: String | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package org.whosin.client.data.dto.request | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class SignupRequestDto( | ||
| @SerialName("email") | ||
| val email: String, | ||
| @SerialName("password") | ||
| val password: String, | ||
| @SerialName("nickName") | ||
| val nickName: String | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package org.whosin.client.data.dto.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class EmailVerificationResponseDto( | ||
| @SerialName("success") | ||
| val success: Boolean, | ||
| @SerialName("status") | ||
| val status: Int, | ||
| @SerialName("message") | ||
| val message: String, | ||
| @SerialName("data") | ||
| val data: String? = null, | ||
| @SerialName("timestamp") | ||
| val timestamp: String? = null | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package org.whosin.client.data.dto.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class FindPasswordResponseDto( | ||
| @SerialName("success") | ||
| val success: Boolean, | ||
| @SerialName("status") | ||
| val status: Int, | ||
| @SerialName("message") | ||
| val message: String, | ||
| @SerialName("data") | ||
| val data: String? = null, | ||
| @SerialName("timestamp") | ||
| val timestamp: String? = null | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package org.whosin.client.data.dto.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class ReissueTokenResponseDto( | ||
| @SerialName("success") | ||
| val success: Boolean, | ||
| @SerialName("status") | ||
| val status: Int, | ||
| @SerialName("message") | ||
| val message: String, | ||
| @SerialName("data") | ||
| val data: TokenDto? = null, | ||
| @SerialName("timestamp") | ||
| val timestamp: String? = null | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package org.whosin.client.data.dto.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class SignupResponseDto( | ||
| @SerialName("success") | ||
| val success: Boolean, | ||
| @SerialName("status") | ||
| val status: Int, | ||
| @SerialName("message") | ||
| val message: String, | ||
| @SerialName("data") | ||
| val data: String? = null, | ||
| @SerialName("timestamp") | ||
| val timestamp: String? = null | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
network_security_config.xml은 버전 관리 대상입니다.
앱의 네트워크 보안 정책을 정의하는 파일을 전역으로 무시하면, 환경마다 다른 설정이 조용히 적용되고 보안 검토도 불가능해집니다. 규정·릴리스 안정성 모두 큰 리스크이니 해당 ignore 항목을 제거하거나, 필요한 경우 별도의 샘플 파일/로컬 경로만 선택적으로 무시해 주세요.
🤖 Prompt for AI Agents