Skip to content

Commit

Permalink
feat/#16 -> dev
Browse files Browse the repository at this point in the history
[FEAT/#16] 회원가입 API 구현
  • Loading branch information
sung-silver authored Jan 10, 2025
2 parents 25ba0ba + 6699c60 commit f7b6699
Show file tree
Hide file tree
Showing 24 changed files with 484 additions and 160 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ ResponseEntity<BaseResponse<?>> authenticateSocialAuthInfoFromWeb(

ResponseEntity<BaseResponse<?>> authenticateSocialAuthInfoFromApp(
AuthRequest.AuthenticateSocialAuthInfo socialAuthInfo);

ResponseEntity<BaseResponse<?>> signUp(AuthRequest.SignUpInfo signUp);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import sopt.makers.authentication.usecase.auth.port.in.AuthenticateSocialAccountUsecase;
import sopt.makers.authentication.usecase.auth.port.in.AuthenticateSocialAccountUsecase.AuthenticateTokenInfo;
import sopt.makers.authentication.usecase.auth.port.in.CreatePhoneVerificationUsecase;
import sopt.makers.authentication.usecase.auth.port.in.SignUpUsecase;
import sopt.makers.authentication.usecase.auth.port.in.VerifyPhoneVerificationUsecase;

import org.springframework.http.HttpHeaders;
Expand All @@ -28,6 +29,7 @@ public class AuthApiController implements AuthApi {
private final CreatePhoneVerificationUsecase createVerificationUsecase;
private final VerifyPhoneVerificationUsecase verifyVerificationUsecase;
private final AuthenticateSocialAccountUsecase authenticateSocialAccountUsecase;
private final SignUpUsecase signUpUsecase;
private final CookieUtil cookieUtil;

@Override
Expand Down Expand Up @@ -74,4 +76,11 @@ public ResponseEntity<BaseResponse<?>> authenticateSocialAuthInfoFromApp(
AuthResponse.AuthenticateSocialAuthInfoForApp.of(
tokenInfo.accessToken(), tokenInfo.refreshToken()));
}

@Override
@PostMapping("/signup")
public ResponseEntity<BaseResponse<?>> signUp(AuthRequest.SignUpInfo signUpInfo) {
signUpUsecase.signUp(signUpInfo.toCommand());
return ResponseUtil.success(AuthSuccess.CREATE_SIGN_UP_USER);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import static lombok.AccessLevel.PRIVATE;

import sopt.makers.authentication.domain.auth.AuthPlatform;
import sopt.makers.authentication.domain.auth.PhoneVerificationType;
import sopt.makers.authentication.usecase.auth.port.in.AuthenticateSocialAccountUsecase.AuthenticateSocialAccountCommand;
import sopt.makers.authentication.usecase.auth.port.in.CreatePhoneVerificationUsecase.CreateVerificationCommand;
import sopt.makers.authentication.usecase.auth.port.in.SignUpUsecase.SignUpCommand;
import sopt.makers.authentication.usecase.auth.port.in.VerifyPhoneVerificationUsecase.VerifyVerificationCommand;

import com.fasterxml.jackson.annotation.JsonProperty;
Expand Down Expand Up @@ -43,4 +45,15 @@ public AuthenticateSocialAccountCommand toCommand() {
return AuthenticateSocialAccountCommand.of(authPlatform, code);
}
}

public record SignUpInfo(
@JsonProperty("name") String name,
@JsonProperty("phone") String phone,
@JsonProperty("token") String token,
@JsonProperty("authPlatform") String authPlatform) {
public SignUpCommand toCommand() {
return new SignUpCommand(
this.name, this.phone, this.token, AuthPlatform.find(this.authPlatform));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,10 @@ public UserRegisterInfo findByPhone(String phone) {
UserRegisterInfoEntity targetRegisterInfo = retriever.findByPhone(phone);
return targetRegisterInfo.toDomain();
}

@Override
public void delete(UserRegisterInfo userRegisterInfo) {
UserRegisterInfoEntity registerInfoEntity = retriever.findByPhone(userRegisterInfo.getPhone());
remover.remove(registerInfoEntity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ public Long findIdByUser(User user) {
return userRetriever.findIdByUser(user);
}

@Override
public void save(User user) {
UserEntity userEntity = UserEntity.fromDomain(user);
userRegister.save(userEntity);
}

@Override
public User findByPhone(String phone) {
return userRetriever.findByPhone(phone);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class UserRegisterInfoEntity {

@NotNull private String name;
@NotNull private String phone;
@NotNull private String email;
@NotNull private LocalDate birthday;

@Min(1)
Expand All @@ -39,6 +40,7 @@ public class UserRegisterInfoEntity {
private Part part;

public UserRegisterInfo toDomain() {
return UserRegisterInfo.of(this.name, this.phone, this.birthday, this.generation, this.part);
return UserRegisterInfo.of(
this.name, this.phone, this.email, this.birthday, this.generation, this.part);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
package sopt.makers.authentication.database.rdb.repository;

import sopt.makers.authentication.database.rdb.entity.UserRegisterInfoEntity;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

@Component
@Transactional
public class UserRegisterInfoRemover {}
@RequiredArgsConstructor
public class UserRegisterInfoRemover {
private final UserRegisterInfoJpaRepository jpaRepository;

public void remove(final UserRegisterInfoEntity entity) {

jpaRepository.delete(entity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
public class UserRegisterInfo {
private final String name;
private final String phone;
private final String email;
private final LocalDate birthday;
private final int generation;
private final Part part;

public static UserRegisterInfo of(
String name, String phone, LocalDate birthday, int generation, Part part) {
return new UserRegisterInfo(name, phone, birthday, generation, part);
String name, String phone, String email, LocalDate birthday, int generation, Part part) {
return new UserRegisterInfo(name, phone, email, birthday, generation, part);
}
}
Original file line number Diff line number Diff line change
@@ -1,75 +1,95 @@
package sopt.makers.authentication.external.oauth;

import static sopt.makers.authentication.support.code.external.failure.ClientError.APPLE_RESPONSE_UNAVAILABLE;
import static sopt.makers.authentication.support.code.external.failure.ClientError.FAIL_READ_APPLE_PRIVATE_KEY_FILE;
import static sopt.makers.authentication.support.code.external.failure.ClientError.INVALID_APPLE_AUTH_CODE;
import static sopt.makers.authentication.support.constant.OAuthConstant.ACCEPT;
import static sopt.makers.authentication.support.constant.OAuthConstant.ACCEPT_VALUE;
import static sopt.makers.authentication.support.code.external.failure.ClientError.*;
import static sopt.makers.authentication.support.constant.OAuthConstant.APPLE_ALGORITHM_HEADER;
import static sopt.makers.authentication.support.constant.OAuthConstant.APPLE_ALGORITHM_VALUE;
import static sopt.makers.authentication.support.constant.OAuthConstant.APPLE_ISSUER;
import static sopt.makers.authentication.support.constant.OAuthConstant.APPLE_KEY_ID_HEADER;
import static sopt.makers.authentication.support.constant.OAuthConstant.APPLE_TOKEN_URL;
import static sopt.makers.authentication.support.constant.OAuthConstant.CLIENT_ID;
import static sopt.makers.authentication.support.constant.OAuthConstant.CLIENT_SECRET;
import static sopt.makers.authentication.support.constant.OAuthConstant.CODE;
import static sopt.makers.authentication.support.constant.OAuthConstant.CONTENT_TYPE;
import static sopt.makers.authentication.support.constant.OAuthConstant.CONTENT_TYPE_VALUE;
import static sopt.makers.authentication.support.constant.OAuthConstant.GRANT_TYPE;
import static sopt.makers.authentication.support.constant.OAuthConstant.GRANT_TYPE_VALUE;

import sopt.makers.authentication.external.oauth.client.AppleAuthClient;
import sopt.makers.authentication.external.oauth.dto.IdTokenResponse;
import sopt.makers.authentication.support.code.domain.failure.AuthFailure;
import sopt.makers.authentication.support.code.support.failure.TokenFailure;
import sopt.makers.authentication.support.exception.domain.AuthException;
import sopt.makers.authentication.support.exception.external.ClientRequestException;
import sopt.makers.authentication.support.exception.external.ClientResponseException;
import sopt.makers.authentication.support.exception.support.TokenException;
import sopt.makers.authentication.support.util.*;
import sopt.makers.authentication.support.value.AppleOAuthProperty;

import java.io.IOException;
import java.security.PrivateKey;
import java.text.ParseException;
import java.time.Instant;
import java.util.Date;

import org.springframework.stereotype.Component;

import com.google.gson.Gson;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

@Component
@RequiredArgsConstructor
@Slf4j
public class AppleAuthService implements OAuthService {
private final AppleOAuthProperty appleOAuthProperty;
private final Gson gson;
private final OkHttpClient client;
private final AppleAuthClient appleAuthClient;

@Override
public IdTokenResponse getIdTokenByCode(final String code) {
FormBody formBody = createTokenRequestFormBody(code);
Request request = createHttpRequest(formBody);
Response response = executeRequest(request);
String clientSecret = createClientSecret();
return appleAuthClient.getIdToken(clientSecret, code);
}

return parseResponseBody(response);
public String getIdentifierByToken(final String token) {
try {
SignedJWT signedJWT = SignedJWT.parse(token);
JWK targetJwk = findMatchJWK(signedJWT);

verifyAppleIdTokenJwt(signedJWT, targetJwk);
String identifier = signedJWT.getJWTClaimsSet().getSubject();
return identifier;
} catch (ParseException e) {
throw new TokenException(TokenFailure.TOKEN_PARSE_FAILED);
}
}

private FormBody createTokenRequestFormBody(final String code) {
String clientId = appleOAuthProperty.sub();
String clientSecret = createClientSecret();
return new FormBody.Builder()
.add(CLIENT_ID, clientId)
.add(CLIENT_SECRET, clientSecret)
.add(CODE, code)
.add(GRANT_TYPE, GRANT_TYPE_VALUE)
.build();
private JWK findMatchJWK(final SignedJWT jwt) {
JWKSet loadedJWKSet = appleAuthClient.getPublicKeySet();
String keyID = jwt.getHeader().getKeyID();
return loadedJWKSet.getKeys().stream()
.filter(jwk -> jwk.getKeyID().equals(keyID))
.findFirst()
.orElseThrow(() -> new AuthException(AuthFailure.NOT_FOUND_AVAILABLE_PUBLIC_KEY_SET));
}

private void verifyAppleIdTokenJwt(final SignedJWT jwt, JWK jwk) throws ParseException {
try {
JWTClaimsSet jwtClaimsSet = jwt.getJWTClaimsSet();
JWSVerifier verifier = new ECDSAVerifier(jwk.toECKey());

boolean isVerifiedSignature = jwt.verify(verifier);
boolean isCorrectIssuer = jwtClaimsSet.getIssuer().equals(APPLE_ISSUER);
boolean isCorrectAudience = jwtClaimsSet.getAudience().contains(appleOAuthProperty.aud());
boolean isNotExpired = jwtClaimsSet.getExpirationTime().after(Date.from(Instant.now()));

if (!(isVerifiedSignature && isCorrectIssuer && isCorrectAudience && isNotExpired)) {
throw new AuthException(AuthFailure.INVALID_ID_TOKEN);
}
} catch (JOSEException e) {
throw new AuthException(AuthFailure.INVALID_ID_TOKEN);
}
}

// TODO : AuthXXX 객체에서 ClientXXXException 발생하는 구조는 개선되면 좋을 것 같습니다. (@동규)
private String createClientSecret() {
Date now = new Date();
PrivateKey privateKey =
Expand All @@ -87,42 +107,4 @@ private String createClientSecret() {
.signWith(privateKey, SignatureAlgorithm.ES256)
.compact();
}

private static Request createHttpRequest(RequestBody requestBody) {
return new Request.Builder()
.url(APPLE_TOKEN_URL)
.post(requestBody)
.addHeader(CONTENT_TYPE, CONTENT_TYPE_VALUE)
.addHeader(ACCEPT, ACCEPT_VALUE)
.build();
}

private Response executeRequest(Request request) {
try {
Response response = client.newCall(request).execute();

validateResponse(response);
return response;
} catch (IOException e) {
throw new ClientResponseException(APPLE_RESPONSE_UNAVAILABLE);
}
}

private void validateResponse(Response response) {
boolean isNotSuccessResponse = !response.isSuccessful();

if (isNotSuccessResponse) {
throw new ClientRequestException(INVALID_APPLE_AUTH_CODE);
}
}

private IdTokenResponse parseResponseBody(Response response) {
ResponseBody responseBody = response.body();
boolean isBodyNull = responseBody == null;

if (isBodyNull) {
throw new ClientResponseException(APPLE_RESPONSE_UNAVAILABLE);
}
return gson.fromJson(response.body().toString(), IdTokenResponse.class);
}
}
Loading

0 comments on commit f7b6699

Please sign in to comment.