Skip to content
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

[FEAT/#16] 회원가입 API 구현 #30

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
Open

[FEAT/#16] 회원가입 API 구현 #30

wants to merge 12 commits into from

Conversation

yummygyudon
Copy link
Member

@yummygyudon yummygyudon commented Jan 7, 2025

Related Issue 🚀


Work Description ✏️

회원가입 기능을 구현했습니다.

⚙️ 구현 회원 가입 프로세스

각 플랫폼마다의 회원 가입을 분리하여 구현했습니다.

(아래 플랫폼은 authPlatform 기준으로 분기 처리됩니다)

  • Google

    1. Body - name, phone, code, authPlatform
    2. code(ID Token) → https://oauth2.googleapis.com/tokeninfo?id_token=ID_TOKEN 호출 (참고 Docs)
    3. Unique Identifier sub 추출
    4. name & phone → UserRegisterInfo 저장소에서 사전 제출 인적정보 조회
    5. UserRegisterInfo 데이터 & sub 값 기반 신규 회원(User) 데이터 생성 및 저장
    6. 완료
  • Apple

    1. Body - name, phone, code, authPlatform
    2. code(ID Token) → ID Token Parsing
    3. Unique Identifier sub 추출
    4. name & phone → UserRegisterInfo 저장소에서 사전 제출 인적정보 조회
    5. UserRegisterInfo 데이터 & sub 값 기반 신규 회원(User) 데이터 생성 및 저장
    6. 완료

(참고 Apple Docs : Authenticating users with Sign in with Apple, Verifying a user, Generate and validate tokens, JWKS-Keys)

위 API의 Request Body로 전달받는code 값은 ID_TOKEN(ACCESS_TOKEN ❌) 입니다.


🔑 Id Token 검증 방식

  • Google (공식 문서)
    • ID 토큰에 발급자가 올바르게 서명했는지 확인합니다. Google에서 발급한 토큰은 검색 문서의 jwks_uri 메타데이터 값에 지정된 URI에 있는 인증서 중 하나를 사용하여 서명됩니다.
    • ID 토큰의 iss 클레임 값이 https://accounts.google.com 또는 accounts.google.com와 같은지 확인합니다.
    • ID 토큰의 aud 클레임 값이 앱의 클라이언트 ID와 같은지 확인합니다.
    • ID 토큰의 만료 시간 (exp 클레임)이 지났는지 확인합니다.
    • 요청에 hd 매개변수 값을 지정한 경우 ID 토큰에 Google Cloud 조직과 연결된 허용된 도메인과 일치하는 hd 클레임이 있는지 확인합니다.
  • Apple (공식 문서)
    • Verify the JWS E256 signature using the server’s public key
    • Verify the nonce for the authentication
    • Verify that the iss field contains https://appleid.apple.com
    • Verify that the aud field is the developer’s client_id
    • Verify that the time is earlier than the exp value of the token

PR Point 📸

✅ 인증 과정에서 사용되는 객체들의 책임과 역할

과거 첫 구현 당시 워낙 큰 볼륨의 작업이었기에 빠르게 반영하기 어려울 듯 하여 구체적으로 개선안에 대해 제안드리지 못했었는데요!
이번 기능 구현을 진행하면서 혼재되어 있는 역할과 처리로 인해 작업에 어려움이 있었습니다.

기존

- OAuthAuthenticator
  - 인증 메서드 호출

- AppleAuthService
  - Apple 통신
    - Request 객체 제작
    - Request Header 제작
    - Apple Client Secret JWT 생성
    - Response 파싱
    - Response 검증 (null & 2xx 여부)
  - Apple 검증/인증 처리
- GoogleAuthService
    - Request 객체 제작
    - Request Header 제작
    - Response 파싱
    - Response 검증 (null & 2xx 여부)
  - Google 검증/인증 처리

위 구조라면 각 OAuth 플랫폼에서 요구하는 검증/인증 작업에 대한 구현xxxAuthService 객체 내에 계속해서 구현해야 하는 상황이었습니다.


변경

  • Domain 레이어에서는 Service 구현체만 주입받아 비즈니즈 요구사항을 위한 행위에 대해서만 �알도록 했습니다.
  • Port 객체는 플랫폼에 따라 달라지는 맞춤 구현에 대한 책임이 없기 때문에 분리했습니다.
    • 플랫폼과 관계없이 동일한 "인증행위"에 대해서만 호출할 수 있도록 했습니다.
  • 인증 작업 객체들은 구체적인 통신 구성 작업에 대한 책임이 없기 때문에 분리했습니다. → 인증 작업만 처리하도록 했습니다.
// Domain에서 사용되는 port
- OAuthService (OAuthServiceImpl)
  - 인증 메서드 호출

// 구체적인 각 플랫폼들을 위한 인증 작업 객체
- AppleAuthenticator
  - Apple 측에서 명시한 검증/인증 처리
  - Apple Client Secret JWT 생성
  - 통신 호출
- GoogleAuthenticator
  - Google 측에서 명시한 검증/인증 처리
  - 통신 호출

// 외부 서버 통신 담당 객체
- AppleAuthClient
- GoogleAuthClient

조금더 유연하고 확장 및 재사용이 가능하도록 개선할 수 있는 요소가 충분히 더 존재하는 것 같습니다.
(ex. Request / Response / Header 생성 Util 객체 분리 등)

추후에 더 논의해보았으면 좋겠습니다.


✅ 인증 프로세스 통일화 (웹 ↔ 네이티브앱)

초반 access_tokenid_token의 개념이 혼동되어 섣불리 확정 짓기 어려웠는데요.

웹 환경과 네이티브 앱 환경에서 모두 동일한 로그인/회원가입 경험을 제공해야했기 때문에 아래와 같은 고민을 했습니다.

  1. 웹과 앱은 서로의 방식에 맞춰 구현하고 서버에서 각 환경에 맞추어 API를 분리한다.
  2. 서버에서 단일 API로 통일화하고 웹과 앱에서 해당 API 요구사항에 맞추어 구현한다.

이 때, 추후 "유지보수성"와 "빠른 대응성"을 위해 2번 방식이 적합하다고 생각했습니다.
(웹 클라이언트 전달 : Configuring your webpage for Sign in with Apple


✅ Access Token ↔️ Id Token

위 언급했듯 2번 방식을 채택함에 따라 각 OAuht 플랫폼의 Token API 공식 문서를 살펴본 결과, 모두 동일하게 access_tokenid_token 을 사용하는 토큰 정책이 적용되어 있고

기본적으로 회원가입 절차에서 사용할 사용자 식별값은 각 플랫폼의 토큰들에 내제되어 있는 Unique Identifiersub를 사용하고자했습니다.
하지만 해당 sub 값은 access_tokenid_token 모두에 포함되어 있습니다.

하지만 각 공식 문서에서 동일하게 얘기하는 내용은 아래와 같았고 이에 따라 ID Token 사용을 채택했습니다.

※ access token은 각자의 앱에서 검증하는 대상이 아니며 OAuth resource sever에 접근하는 용도로 사용한다.

※ access token은 불투명 토큰이며, 이는 독점적 형식임을 의미합니다. 애플리케이션이 이를 검사할 수 없습니다.(Google)

※ 애플리케이션이 검사할 수 없는 불투명 객체인 액세스 토큰과 달리 ID 토큰은 애플리케이션에서 검사하고 사용하도록 되어 있습니다. 토큰에 서명한 사용자 또는 ID 토큰이 발급된 ID와 같은 토큰의 정보가 애플리케이션에서 사용하도록 제공됩니다. (Google)

※ verify 대상은 JWT로서 사용자의 식별 데이터를 포함한 id token으로 사용한다. (Apple)


✅ 왜 OAuth 플랫폼 계정의 email은 식별값으로 사용하지 않는가

해당 내용은 Google보다는 Apple의 영향이 큽니다.
Apple의 공식 문서에 따르면 아래와 같이 전했습니다.

사용자의 이메일 주소를 나타내는 문자열 값입니다. 이메일 주소는 사용자의 개인 이메일 중계 서비스에 따라 사용자의 실제 이메일 주소 또는 프록시 주소입니다. 직장 및 학교의 Apple로 로그인 사용자의 경우 이 값은 비어 있을 수 있습니다. 예를 들어, 어린 학생의 경우 이메일 주소가 없을 수 있습니다.

image
image

위와 같이 학교 계정/직장 계정에 대해 불확실성을 가지고 있었기에 필수 식별값으로 사용하기엔 부적합하다고 판단했습니다.
(물론 Apple에서도 private proxy email을 email로 변환하는 방식도 제공하지만 이를 위한 클라이언트와 서버의 추가적인 구현은 비효율적일 것이라고 판단했습니다.)


🤯 도메인 객체의 ID 부재 해결방법...

레퍼런스를 찾아보니 비슷한 고민을 겪은 케이스가 많은 것 같아요.
이에 몇 가지 대안을 찾아왔습니다.

  1. 도메인 객체에서 ID를 포함하도록 설계 변경
  2. Aggregate Root 도입

추가적인 내용에 대해서는 BE 정기회의 때 얘기나눠보도록 해요!

전화번호 인증 이후 진행되는 회원가입 API를 정의합니다.
- Body로 `name`과 `phone`, `code` (access code)와 `authPlatform`(GOOGLE/APPLE) 요청
- 회원가입 성공에 대한 Success Code 정의
- Auth - SignUpUsecase 정의
  - SignUpCommand 타입 파라미터 선언
- 정의한 API 구현
@yummygyudon yummygyudon self-assigned this Jan 7, 2025
Copy link

height bot commented Jan 7, 2025

Link Height tasks by mentioning a task ID in the pull request title or commit messages, or description and comments with the keyword link (e.g. "Link T-123").

💡Tip: You can also use "Close T-X" to automatically close a task when the pull request is merged.

Copy link
Contributor

@hyunw9 hyunw9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다 !

역시 깔끔함 탑3


@Override
@PostMapping("/signup")
public ResponseEntity<BaseResponse<?>> signUp(AuthRequest.SignUp signUp) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3

오 이 부분 signUp이 직관적이긴 한데, 행위를 뜻하는 것 같아서 signUpRequest 정도로 바꾸는건 어떠신가요 ?!

- Map to Headers 생성 팩토리 메서드
- URL & Headers & Body to Request 생성 팩토리 메서드
  - Body가 필요한 Post Request 와 Body가 불필요한 Get Request 생성을 분리햇습니다.
클라이언트 <-> 인증 사이에서의 관심사가 혼재되어 정의되어 있던 요소를 수정했습니다.
- Issuer 값
- Public Key Set URL 추가
- 외부 통신의 Response 타입이 도메인 레이어까지 범람하는 것을 방지하고자 "XXXResponse" 타입의 return type 에서 String으로 변경했습니다.
  - 메서드 당 단일 책임 수행 및 반환을 실현하기 위해 Port Class Return Type이 아닌 기본형 타입을 채택했습니다.
  - 추후 필드 추가 필요 시, Record 타입 변경 및 필드 추가를 고려하는 것이 좋다고 생각했습니다.
인증 단계에서 ID를 알 수 없는 상황이기에 phone 값 기준으로 조회 및 삭제하도록 구현했습니다.
@pull-request-size pull-request-size bot added size/XL and removed size/M labels Jan 7, 2025
Copy link
Contributor

@sung-silver sung-silver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자잘한 리뷰 남겨보았습니다! 볼륨이 큰데 고생하셨습니다 구조 변경 이후가 더 확실히 이해하기 좋은 것 같네요 많이 배워갑니다~

+) 머지 이후에 소셜 로그인도 id token 방식을 사용하도록 코드 변경 이후 사용되지 않는 코드는 삭제하겠습니다!


@Component
@RequiredArgsConstructor
public class GoogleAuthService implements OAuthService {
private final GoogleOAuthProperty googleOAuthProperty;
private final GoogleAuthClient googleAuthClient;
private final Gson gson;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3

더 이상 사용되지 않는 필드인 것 같은데 지우면 좋을 것 같습니다!

SignedJWT signedJWT = SignedJWT.parse(token);
JWK targetJwk = findMatchJWK(signedJWT);

verifyAppleIdTokenJwt(signedJWT, targetJwk);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p2
GoogleAuthService인데 이름이 잘못 지어진 것 같아요..!


if (isBodyNull) {
throw new ClientResponseException(GOOGLE_RESPONSE_UNAVAILABLE);
private void verifyAppleIdTokenJwt(final SignedJWT jwt, JWK jwk) throws ParseException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p2

verifyGoogleIdTokenJwt로 변경되면 좋을 것 같습니다!

import static sopt.makers.authentication.support.code.external.failure.ClientError.INVALID_ID_TOKEN;

import sopt.makers.authentication.domain.auth.AuthPlatform;
import sopt.makers.authentication.external.oauth.dto.IdTokenResponse;
import sopt.makers.authentication.support.exception.external.*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p4
와일드카드 제거해주시면 좋을 것 같아요!

Copy link
Contributor

@hyunw9 hyunw9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다! 헛발질 했네요 오전에..

저는 성은이가 언급한 내용 말고 추가 논의할 사항은 없는 것 같습니다 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FEAT] 회원가입 기능 구현
3 participants