Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/deploy-to-dev-ec2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,15 @@ jobs:
KAKAO_CLIENT_SECRET="${{ secrets.KAKAO_CLIENT_SECRET }}" \
KAKAO_REDIRECT_URI="${{ secrets.KAKAO_REDIRECT_URI }}" \
KAKAO_APP_ID="${{ secrets.KAKAO_APP_ID }}" \
APPLE_CLIENT_ID="${{ secrets.APPLE_CLIENT_ID }}" \
APPLE_TEAM_ID="${{ secrets.APPLE_TEAM_ID }}" \
APPLE_KEY_ID="${{ secrets.APPLE_KEY_ID }}" \
APPLE_PRIVATE_KEY="${{ secrets.APPLE_PRIVATE_KEY }}" \
IAM_ACCESS_KEY="${{ secrets.IAM_ACCESS_KEY }}" \
IAM_SECRET_KEY="${{ secrets.IAM_SECRET_KEY }}" \
BUCKET_NAME="${{ secrets.BUCKET_NAME }}" \
PROFILE_IMAGE_BASE_URL="${{ secrets.PROFILE_IMAGE_BASE_URL }}" \
KMS_KEY_ID="${{ secrets.KMS_KEY_ID }}" \
MAIL_ADDRESS="${{ secrets.MAIL_ADDRESS }}" \
MAIL_PASSWORD="${{ secrets.MAIL_PASSWORD }}" \
OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" \
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/deploy-to-prod-ec2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,15 @@ jobs:
KAKAO_CLIENT_SECRET="${{ secrets.KAKAO_CLIENT_SECRET }}" \
KAKAO_REDIRECT_URI="${{ secrets.KAKAO_REDIRECT_URI }}" \
KAKAO_APP_ID="${{ secrets.KAKAO_APP_ID }}" \
APPLE_CLIENT_ID="${{ secrets.APPLE_CLIENT_ID }}" \
APPLE_TEAM_ID="${{ secrets.APPLE_TEAM_ID }}" \
APPLE_KEY_ID="${{ secrets.APPLE_KEY_ID }}" \
APPLE_PRIVATE_KEY="${{ secrets.APPLE_PRIVATE_KEY }}" \
IAM_ACCESS_KEY="${{ secrets.IAM_ACCESS_KEY }}" \
IAM_SECRET_KEY="${{ secrets.IAM_SECRET_KEY }}" \
BUCKET_NAME="${{ secrets.BUCKET_NAME }}" \
PROFILE_IMAGE_BASE_URL="${{ secrets.PROFILE_IMAGE_BASE_URL }}" \
KMS_KEY_ID="${{ secrets.KMS_KEY_ID }}" \
MAIL_ADDRESS="${{ secrets.MAIL_ADDRESS }}" \
MAIL_PASSWORD="${{ secrets.MAIL_PASSWORD }}" \
OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" \
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ dependencies {
implementation platform('software.amazon.awssdk:bom:2.39.4')
implementation 'software.amazon.awssdk:s3'

// AWS KMS
implementation 'software.amazon.awssdk:kms'

// BadWordFiltering 라이브러리
implementation 'io.github.vaneproject:badwordfiltering:1.0.0'
implementation 'com.modernmt.text:profanity-filter:1.0.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.devkor.ifive.nadab.domain.auth.api.dto.request.NaverNativeLoginRequest;
import com.devkor.ifive.nadab.domain.auth.api.dto.request.GoogleNativeLoginRequest;
import com.devkor.ifive.nadab.domain.auth.api.dto.request.KakaoNativeLoginRequest;
import com.devkor.ifive.nadab.domain.auth.api.dto.request.AppleNativeLoginRequest;
import com.devkor.ifive.nadab.domain.auth.api.dto.response.AuthorizationUrlResponse;
import com.devkor.ifive.nadab.domain.auth.api.dto.request.SocialLoginRequest;
import com.devkor.ifive.nadab.domain.auth.api.dto.response.TokenResponse;
Expand Down Expand Up @@ -256,6 +257,7 @@ public ResponseEntity<ApiResponseDto<TokenResponse>> login(
responseCode = "409",
description = """
이메일 중복
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_APPLE - 애플 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_NAVER - 네이버 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_GOOGLE - 구글 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_KAKAO - 카카오 계정으로 이미 가입됨
Expand Down Expand Up @@ -320,8 +322,8 @@ public ResponseEntity<ApiResponseDto<TokenResponse>> oauth2Login(
@ApiResponse(
responseCode = "409",
description = """
이메일 중복
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_NAVER - 네이버 계정으로 이미 가입됨
이메일 중복 (다른 제공자로 이미 가입된 경우)
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_APPLE - 애플 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_GOOGLE - 구글 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_KAKAO - 카카오 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_BASIC - 일반 계정으로 이미 가입됨
Expand Down Expand Up @@ -384,9 +386,9 @@ public ResponseEntity<ApiResponseDto<TokenResponse>> naverNativeLogin(
@ApiResponse(
responseCode = "409",
description = """
이메일 중복
이메일 중복 (다른 제공자로 이미 가입된 경우)
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_APPLE - 애플 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_NAVER - 네이버 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_GOOGLE - 구글 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_KAKAO - 카카오 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_BASIC - 일반 계정으로 이미 가입됨
""",
Expand Down Expand Up @@ -444,10 +446,10 @@ public ResponseEntity<ApiResponseDto<TokenResponse>> googleNativeLogin(
@ApiResponse(
responseCode = "409",
description = """
이메일 중복
이메일 중복 (다른 제공자로 이미 가입된 경우)
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_APPLE - 애플 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_NAVER - 네이버 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_GOOGLE - 구글 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_KAKAO - 카카오 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_BASIC - 일반 계정으로 이미 가입됨
""",
content = @Content
Expand All @@ -469,6 +471,74 @@ public ResponseEntity<ApiResponseDto<TokenResponse>> kakaoNativeLogin(
);
}

@PostMapping("/apple/native-login")
@PermitAll
@Operation(
summary = "애플 Native SDK 로그인 (iOS 전용)",
description = """
iOS 앱에서 Sign in with Apple SDK로 받은 Authorization Code를 사용하여 로그인을 완료합니다.<br>
Access Token과 signupStatus는 응답 바디(JSON)로 반환되며, Refresh Token은 HttpOnly 쿠키로 자동 설정됩니다.<br>
기존 회원은 바로 로그인 처리되며, 신규 사용자는 자동으로 회원가입 후 로그인됩니다.<br>
<br>
중요: iOS 클라이언트 설정<br>
- request.requestedScopes = [.email] 설정 필수<br>
- email scope를 포함해야 첫 로그인 시 이메일을 받을 수 있습니다<br>
<br>
신규 가입자(signupStatus: PROFILE_INCOMPLETE)는 온보딩 과정에서 약관 동의(POST /terms/consent) 후 닉네임을 입력해야 합니다.<br>
<br>
**signupStatus:**<br>
- PROFILE_INCOMPLETE: 프로필 입력 필요 (신규 가입자, 약관 동의 + 닉네임 입력 필요)<br>
- COMPLETED: 가입 완료 (모든 필수 정보 입력 완료)<br>
- WITHDRAWN: 회원 탈퇴 (14일 내 복구 가능)
""",
responses = {
@ApiResponse(
responseCode = "200",
description = "로그인 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class), mediaType = "application/json")
),
@ApiResponse(
responseCode = "400",
description = "ErrorCode: VALIDATION_FAILED - Authorization Code가 누락된 경우",
content = @Content
),
@ApiResponse(
responseCode = "401",
description = """
Authorization Code 검증 실패
- ErrorCode: AUTH_OAUTH2_TOKEN_FAILED - 애플 토큰 발급 실패(유효하지 않은 코드, 만료된 코드)
- ErrorCode: AUTH_OAUTH2_USERINFO_FAILED - 애플 ID Token 파싱 실패
""",
content = @Content
),
@ApiResponse(
responseCode = "409",
description = """
이메일 중복 (다른 제공자로 이미 가입된 경우)
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_NAVER - 네이버 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_GOOGLE - 구글 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_KAKAO - 카카오 계정으로 이미 가입됨
- ErrorCode: AUTH_EMAIL_ALREADY_REGISTERED_WITH_BASIC - 일반 계정으로 이미 가입됨
""",
content = @Content
)
}
)
public ResponseEntity<ApiResponseDto<TokenResponse>> appleNativeLogin(
@RequestBody @Valid AppleNativeLoginRequest request,
HttpServletResponse response
) {
// 애플 Authorization Code로 토큰 교환 및 로그인
TokenBundle tokenBundle = nativeAuthService.executeAppleLogin(request.code());

// Refresh Token을 HttpOnly 쿠키에 저장
cookieManager.addRefreshTokenCookie(response, tokenBundle.refreshToken());

return ApiResponseEntity.ok(
new TokenResponse(tokenBundle.accessToken(), tokenBundle.signupStatus())
);
}

@PostMapping("/refresh")
@PermitAll
@Operation(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.devkor.ifive.nadab.domain.auth.api.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;

/**
* 애플 Native SDK 로그인 요청 DTO
* - iOS 앱에서 Sign in with Apple SDK로 받은 Authorization Code 전달
*/
@Schema(description = "애플 Native SDK 로그인 요청")
public record AppleNativeLoginRequest(
@Schema(description = "애플 SDK로부터 받은 Authorization Code", example = "c1234567890abcdef")
@NotBlank(message = "애플 Authorization Code는 필수입니다")
String code
) {
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
package com.devkor.ifive.nadab.domain.auth.application;

import com.devkor.ifive.nadab.domain.auth.application.TokenService.TokenBundle;
import com.devkor.ifive.nadab.domain.auth.core.entity.ProviderType;
import com.devkor.ifive.nadab.domain.auth.core.entity.SocialAccount;
import com.devkor.ifive.nadab.domain.auth.core.entity.SocialAuthRefreshToken;
import com.devkor.ifive.nadab.domain.auth.core.repository.SocialAccountRepository;
import com.devkor.ifive.nadab.domain.auth.infra.oauth.OAuth2Provider;
import com.devkor.ifive.nadab.domain.auth.infra.oauth.OAuth2UserInfo;
import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.AppleOAuth2Client;
import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.GoogleOAuth2Client;
import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.KakaoOAuth2Client;
import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.NaverOAuth2Client;
import com.devkor.ifive.nadab.domain.auth.infra.oauth.client.dto.AppleTokenResponse;
import com.devkor.ifive.nadab.domain.user.core.entity.User;
import com.devkor.ifive.nadab.global.security.crypto.DataCryptoService;
import com.devkor.ifive.nadab.global.security.crypto.EncryptedPayload;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
* Native SDK 소셜 로그인 서비스
* - Android/iOS 앱에서 각 SDK로 받은 토큰으로 로그인 처리
Expand All @@ -26,8 +36,11 @@ public class NativeAuthService {
private final NaverOAuth2Client naverOAuth2Client;
private final GoogleOAuth2Client googleOAuth2Client;
private final KakaoOAuth2Client kakaoOAuth2Client;
private final AppleOAuth2Client appleOAuth2Client;
private final SocialAccountService socialAccountService;
private final SocialAccountRepository socialAccountRepository;
private final TokenService tokenService;
private final DataCryptoService dataCryptoService;

// 네이버 Native SDK 로그인 처리
public TokenBundle executeNaverLogin(String naverAccessToken) {
Expand Down Expand Up @@ -79,4 +92,33 @@ public TokenBundle executeKakaoLogin(String kakaoAccessToken) {
// 4. JWT 토큰 발급
return tokenService.issueTokens(user.getId());
}

// 애플 Native SDK 로그인 처리 (iOS)
public TokenBundle executeAppleLogin(String code) {
// 1. Authorization Code → Access Token, Refresh Token, ID Token 교환
AppleTokenResponse appleTokens = appleOAuth2Client.fetchTokensForNative(code);

// 2. ID Token 파싱 및 사용자 정보 추출
OAuth2UserInfo userInfo = appleOAuth2Client.parseIdToken(appleTokens.getIdToken());

// 3. 사용자 조회 또는 생성
User user = socialAccountService.getOrCreateUser(
OAuth2Provider.APPLE,
userInfo.providerId(),
userInfo.email()
);

// 4. Refresh Token 암호화 저장
if (appleTokens.getRefreshToken() != null) {
ProviderType providerType = ProviderType.APPLE;
SocialAccount socialAccount = socialAccountRepository
.findByProviderTypeAndProviderUserId(providerType, userInfo.providerId())
.orElseThrow(() -> new IllegalStateException("SocialAccount 조회 실패"));
EncryptedPayload encrypted = dataCryptoService.encrypt(appleTokens.getRefreshToken().getBytes(UTF_8));
socialAccount.updateRefreshToken(SocialAuthRefreshToken.from(encrypted));
}

// 5. JWT 토큰 발급
return tokenService.issueTokens(user.getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ public User getOrCreateUser(OAuth2Provider provider, String providerId, String e
}
return user;
})
.orElseGet(() -> saveNewSocialUser(email, provider, providerId));
.orElseGet(() -> {
// 신규 사용자 생성 시 email 필수
if (email == null || email.isBlank()) {
throw new BadRequestException(ErrorCode.AUTH_OAUTH2_USERINFO_FAILED);
}
return saveNewSocialUser(email, provider, providerId);
});
}

// User 조회 실패시 신규 소셜 로그인 사용자 생성
Expand All @@ -64,6 +70,7 @@ private User saveNewSocialUser(String email, OAuth2Provider provider, String pro
if (existingSocialAccount != null) {
// 소셜 로그인 계정
ErrorCode errorCode = switch (existingSocialAccount.getProviderType()) {
case APPLE -> ErrorCode.AUTH_EMAIL_ALREADY_REGISTERED_WITH_APPLE;
case GOOGLE -> ErrorCode.AUTH_EMAIL_ALREADY_REGISTERED_WITH_GOOGLE;
case KAKAO -> ErrorCode.AUTH_EMAIL_ALREADY_REGISTERED_WITH_KAKAO;
case NAVER -> ErrorCode.AUTH_EMAIL_ALREADY_REGISTERED_WITH_NAVER;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public String getAuthorizationUrl(OAuth2Provider provider) {
case NAVER -> naverOAuth2Client.buildAuthorizationUrl(state);
case GOOGLE -> googleOAuth2Client.buildAuthorizationUrl(state);
case KAKAO -> kakaoOAuth2Client.buildAuthorizationUrl(state);
default -> throw new OAuth2Exception(ErrorCode.AUTH_UNSUPPORTED_OAUTH2_PROVIDER);
};
}

Expand All @@ -60,13 +61,15 @@ public TokenBundle executeOAuth2Login(OAuth2Provider provider, String code, Stri
case NAVER -> naverOAuth2Client.fetchAccessToken(code, state);
case GOOGLE -> googleOAuth2Client.fetchAccessToken(code);
case KAKAO -> kakaoOAuth2Client.fetchAccessToken(code);
default -> throw new OAuth2Exception(ErrorCode.AUTH_UNSUPPORTED_OAUTH2_PROVIDER);
};

// 3. 사용자 정보 조회
OAuth2UserInfo userInfo = switch (provider) {
case NAVER -> naverOAuth2Client.fetchUserInfo(accessToken);
case GOOGLE -> googleOAuth2Client.fetchUserInfo(accessToken);
case KAKAO -> kakaoOAuth2Client.fetchUserInfo(accessToken);
default -> throw new OAuth2Exception(ErrorCode.AUTH_UNSUPPORTED_OAUTH2_PROVIDER);
};

// 4. 사용자 조회 또는 생성
Expand Down
Loading
Loading