Skip to content

Commit

Permalink
Merge branch 'dev' into feat/#31
Browse files Browse the repository at this point in the history
  • Loading branch information
hyunw9 authored Jan 10, 2025
2 parents 2428173 + 42ad8ed commit a0d154a
Show file tree
Hide file tree
Showing 33 changed files with 354 additions and 251 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ ResponseEntity<BaseResponse<?>> refreshTokenFromApp(

ResponseEntity<BaseResponse<?>> refreshTokenFromWeb(
AuthRequest.AuthenticationTokenInfo authenticationTokenInfo);

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 @@ -75,6 +77,13 @@ public ResponseEntity<BaseResponse<?>> authenticateSocialAuthInfoFromApp(
tokenInfo.accessToken(), tokenInfo.refreshToken()));
}

@PostMapping("/signup")
public ResponseEntity<BaseResponse<?>> signUp(AuthRequest.SignUpInfo signUpInfo) {
signUpUsecase.signUp(signUpInfo.toCommand());
return ResponseUtil.success(AuthSuccess.CREATE_SIGN_UP_USER);

}

@Override
@PostMapping("/refresh/app")
public ResponseEntity<BaseResponse<?>> refreshTokenFromApp(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

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.AuthenticateSocialAccountUsecase.AuthenticateTokenInfo;
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 @@ -39,9 +41,21 @@ public VerifyVerificationCommand toCommand() {
}
}

public record AuthenticateSocialAuthInfo(String code, String authPlatform) {
public record AuthenticateSocialAuthInfo(
@JsonProperty("token") String token, @JsonProperty("authPlatform") String authPlatform) {
public AuthenticateSocialAccountCommand toCommand() {
return AuthenticateSocialAccountCommand.of(authPlatform, code);
return AuthenticateSocialAccountCommand.of(this.token, AuthPlatform.find(this.authPlatform));
}
}

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));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

import static lombok.AccessLevel.PRIVATE;

import sopt.makers.authentication.domain.auth.*;
import sopt.makers.authentication.usecase.auth.port.in.UpdateSocialAccountUsecase.UpdateSocialAccountCommand;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor(access = PRIVATE)
public final class SocialAccountRequest {
public record UpdateSocialAccount(String phone, String code, String authPlatform) {
public record UpdateSocialAccount(String phone, String token, String authPlatform) {
public UpdateSocialAccountCommand toCommand() {
return UpdateSocialAccountCommand.of(phone, authPlatform, code);
return UpdateSocialAccountCommand.of(phone, token, AuthPlatform.find(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 @@ -83,7 +83,7 @@ public static UserEntity fromDomain(final User user) {
}

public User toDomain() {
SocialAccount socialAccount = SocialAccount.of(authPlatformId, authPlatformType.name());
SocialAccount socialAccount = SocialAccount.of(authPlatformId, authPlatformType);
Profile profile = Profile.of(name, email, phone, birthday);
return User.createNewUser(socialAccount, profile);
}
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 @@ -4,7 +4,7 @@

public record SocialAccount(
@NotNull String authPlatformId, @NotNull AuthPlatform authPlatformType) {
public static SocialAccount of(final String authPlatformId, final String authPlatformType) {
return new SocialAccount(authPlatformId, AuthPlatform.find(authPlatformType));
public static SocialAccount of(final String authPlatformId, final AuthPlatform authPlatformType) {
return new SocialAccount(authPlatformId, authPlatformType);
}
}
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,128 +1,78 @@
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.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_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.dto.IdTokenResponse;
import sopt.makers.authentication.support.exception.external.ClientRequestException;
import sopt.makers.authentication.support.exception.external.ClientResponseException;
import static sopt.makers.authentication.support.code.external.failure.ClientError.*;
import static sopt.makers.authentication.support.constant.OAuthConstant.APPLE_ISSUER;

import sopt.makers.authentication.external.oauth.client.AppleAuthClient;
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.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);

return parseResponseBody(response);
}

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 String createClientSecret() {
Date now = new Date();
PrivateKey privateKey =
KeyFileUtil.getPrivateKey(appleOAuthProperty.key().path())
.orElseThrow(() -> new ClientRequestException(FAIL_READ_APPLE_PRIVATE_KEY_FILE));

return Jwts.builder() // 토큰 생성 로직은 tokenProvider? 근데 얘는 parse는 없음
.setHeaderParam(APPLE_KEY_ID_HEADER, appleOAuthProperty.key().id())
.setHeaderParam(APPLE_ALGORITHM_HEADER, APPLE_ALGORITHM_VALUE)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + appleOAuthProperty.expiration().tokenExpiration()))
.setIssuer(appleOAuthProperty.team().id())
.setAudience(appleOAuthProperty.aud())
.setSubject(appleOAuthProperty.sub())
.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) {
public String getIdentifierByToken(final String token) {
try {
Response response = client.newCall(request).execute();

validateResponse(response);
return response;
} catch (IOException e) {
throw new ClientResponseException(APPLE_RESPONSE_UNAVAILABLE);
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 void validateResponse(Response response) {
boolean isNotSuccessResponse = !response.isSuccessful();

if (isNotSuccessResponse) {
throw new ClientRequestException(INVALID_APPLE_AUTH_CODE);
}
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 IdTokenResponse parseResponseBody(Response response) {
ResponseBody responseBody = response.body();
boolean isBodyNull = responseBody == null;

if (isBodyNull) {
throw new ClientResponseException(APPLE_RESPONSE_UNAVAILABLE);
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);
}
return gson.fromJson(response.body().toString(), IdTokenResponse.class);
}
}
Loading

0 comments on commit a0d154a

Please sign in to comment.