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: 회원탈퇴 구현 #55

Merged
merged 32 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2048a63
Fix: ddl-auto validate 속성으로 변경
Jaewon-pro Aug 5, 2024
caaf16e
Fix: flyway 설정 application.yml에 추가
Jaewon-pro Aug 5, 2024
78df8fb
Fix: running record 테이블 pace 컬럼 타입을 정수형으로 변경 (#46)
Jaewon-pro Aug 6, 2024
bd99a18
Merge remote-tracking branch 'origin/feature/#40/account-delete' into…
hee9841 Aug 6, 2024
993a9d6
Chore : pem key 파싱을 위한 bouncycastle 라이브러리 추가
hee9841 Aug 7, 2024
9c517d1
Chore : pem key 파싱을 위한 bouncycastle 라이브러리 추가
hee9841 Aug 7, 2024
78b239b
Merge remote-tracking branch 'origin/feature/#40/account-delete' into…
hee9841 Aug 7, 2024
7ea6084
Feat: BadgeAchievement Member로 삭제 기능
hee9841 Aug 7, 2024
ce2d685
Test: BadgeAchievement Member로 삭제 기능 테스트
hee9841 Aug 7, 2024
2964439
Feat: SocialProfileRepository Member로 삭제 기능 추가
hee9841 Aug 7, 2024
c4e6829
Test: SocialProfileRepository 테스트
hee9841 Aug 7, 2024
9430259
Fix: badgeAchievementRepository의 deleteByMember의 인자를 member_id로 받게 수정
hee9841 Aug 7, 2024
e5a7205
Fix: SocialProfileRepository의 deleteByMember의 인자를 member_id로 받게 수정
hee9841 Aug 7, 2024
cd5bc87
Feat: memberId에 의해 MemberLevel 삭제 구현
hee9841 Aug 7, 2024
6a4cc3f
Test: memberId에 의해 MemberLevel 삭제 테스트
hee9841 Aug 7, 2024
21e37cd
Feat: memberId에 의해 Member 삭제
hee9841 Aug 7, 2024
79e40d0
Test: memberId로 Member 삭제 테스트
hee9841 Aug 7, 2024
e008ccf
Chore: 애플 관련 환경 변수 추가
hee9841 Aug 7, 2024
6bd64b5
Feat: 애플 토큰 발급, 토큰 해제 로직 구현
hee9841 Aug 7, 2024
256ba9e
Feat: 러닝 기록 멤버id로 삭제 구현 (#51)
hee9841 Aug 7, 2024
097036f
Fix: Repository Transactional 삭제
hee9841 Aug 7, 2024
b7669a7
Feat: 회원탈퇴 구현
hee9841 Aug 7, 2024
0b6ec3a
Fix: 시크릿 파일 위치 변경으로 인한 수정
hee9841 Aug 7, 2024
e410cdc
Fix: member_level table member_id fk 오류 수정 (#54)
Jaewon-pro Aug 7, 2024
4f308c7
Fix: 테스트 환경변수 파일 가짜 정보 입력
hee9841 Aug 7, 2024
31ea1a4
Fix: pr 리뷰 사항 반영
hee9841 Aug 7, 2024
62868c0
Refactor: OidcProviderFactory와 OidcProvider 역활 분리
hee9841 Aug 7, 2024
250cd49
Rename: 패키지 구조 변경
hee9841 Aug 7, 2024
98d303b
Merge branch 'main' into feature/#40/account-delete
hee9841 Aug 7, 2024
3d85eb9
Chore: p8 파일 gitIgnore 추가
hee9841 Aug 8, 2024
476ad53
Fix: PR 리뷰 사항 반영
hee9841 Aug 8, 2024
b31090f
Fix: AppleAuthTokenResponse 코드 카멜로 변경으로 인핸 오류 수정
hee9841 Aug 8, 2024
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
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ dependencies {
implementation(libs.jetbrains.annotations)
implementation(libs.springdoc)

implementation(libs.bcpkix)

// JWT
implementation(libs.jjwt.api)
runtimeOnly(libs.jjwt.impl)
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ spring-boot-devtools = { group = "org.springframework.boot", name = "spring-boot
spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test", version.ref = "spring-boot" }
spring-boot-testcontainers = { group = "org.springframework.boot", name = "spring-boot-testcontainers", version.ref = "spring-boot" }

bcpkix = {group= "org.bouncycastle", name = "bcpkix-jdk18on", version = "1.78.1" }

lombok = { group = "org.projectlombok", name = "lombok", version = "1.18.34" }
dotenv = { group = "me.paulschwarz", name = "spring-dotenv", version = "4.0.0" }
jetbrains-annotations = { group = "org.jetbrains", name = "annotations", version = "24.1.0" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ public interface OidcProvider {

Claims getClaimsBy(String idToken);

String getAccessToken(String code);

void revoke(String accessToken);

default Map<String, String> parseHeaders(String token) {
String header = token.split("\\.")[0];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,24 @@ public OidcProviderFactory(List<OidcProvider> providers) {
}

public Claims getClaims(SocialType socialType, String idToken) {
return getOidcProviderBy(socialType).getClaimsBy(idToken);
}

public String getAccessToken(SocialType socialType, String code) {
return getOidcProviderBy(socialType).getAccessToken(code);
}

public void revoke(SocialType socialType, String accessToken) {
getOidcProviderBy(socialType).revoke(accessToken);
}

Jaewon-pro marked this conversation as resolved.
Show resolved Hide resolved
private OidcProvider getOidcProviderBy(SocialType socialType) {
OidcProvider oidcProvider = authProviderMap.get(socialType);

if (isNull(oidcProvider)) {
throw new BusinessException(ErrorType.UNSUPPORTED_SOCIAL_TYPE, socialType.getValue());
}

return oidcProvider.getClaimsBy(idToken);
return oidcProvider;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,38 @@
import com.dnd.runus.auth.exception.AuthException;
import com.dnd.runus.auth.oidc.provider.OidcProvider;
import com.dnd.runus.auth.oidc.provider.PublicKeyProvider;
import com.dnd.runus.client.vo.AppleAuthRevokeRequest;
import com.dnd.runus.client.vo.AppleAuthTokenRequest;
import com.dnd.runus.client.vo.OidcPublicKeyList;
import com.dnd.runus.client.web.AppleAuthClient;
import com.dnd.runus.global.constant.SocialType;
import com.dnd.runus.global.exception.BusinessException;
import com.dnd.runus.global.exception.type.ErrorType;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Jwts.SIG;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.SignatureException;
import lombok.RequiredArgsConstructor;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

@Component
@RequiredArgsConstructor
Expand All @@ -24,6 +43,15 @@ public class AppleAuthProvider implements OidcProvider {
private final AppleAuthClient appleAuthClient;
private final PublicKeyProvider publicKeyProvider;

@Value("${oauth.apple.key_id}")
private String keyId;

@Value("${oauth.apple.team_id}")
private String teamId;

@Value("${oauth.apple.client_id}")
private String clientId;

@Override
public SocialType getSocialType() {
return SocialType.APPLE;
Expand All @@ -39,6 +67,36 @@ public Claims getClaimsBy(String identityToken) {
return parseClaims(identityToken, publicKey);
}

@Override
public String getAccessToken(String code) {
try {
return appleAuthClient
.getAccessAppleToken(AppleAuthTokenRequest.builder()
.client_secret(createClientSecret())
.client_id(clientId)
.code(code)
.grant_type("authorization_code")
.build())
.access_token();
} catch (HttpClientErrorException e) {
throw new BusinessException(ErrorType.FAILED_AUTHENTICATION, e.getMessage());
}
}

@Override
public void revoke(String accessToken) {
try {
appleAuthClient.revokeAccount(AppleAuthRevokeRequest.builder()
.client_id(clientId)
.client_secret(createClientSecret())
.token(accessToken)
.token_type_hint("access_token")
.build());
} catch (HttpClientErrorException e) {
throw new BusinessException(ErrorType.FAILED_AUTHENTICATION, e.getMessage());
}
}

private Claims parseClaims(String token, PublicKey publicKey) {
try {
return Jwts.parser()
Expand All @@ -53,4 +111,57 @@ private Claims parseClaims(String token, PublicKey publicKey) {
throw new AuthException(ErrorType.EXPIRED_ACCESS_TOKEN, e.getMessage());
}
}

private void validateClaims(Claims claims) {
// todo 4가지 검증 추가
// 1. Verify the nonce for the authentication
// 2. Verify that the iss field contains https://appleid.apple.com
// 3. Verify that the aud field is the developer’s client_id
// 4. Verify that the time is earlier than the exp value of the token
// nonce 검증 방법 알아보기
// if(claims.get("nonce") == null) {
// throw new AuthException(ErrorType.TAMPERED_ACCESS_TOKEN,
// }
// if (!claims.getIssuer().contains("https://appleid.apple.com")) {
// throw new AuthException(ErrorType.TAMPERED_ACCESS_TOKEN, "잘못된 iss");
// }
}

private PrivateKey getPrivateKey() {
ClassPathResource resource = new ClassPathResource("AuthKey_" + keyId + ".p8");
try {
String privateKey = new String(Files.readAllBytes(Paths.get(resource.getURI())));
Reader pemReader = new StringReader(privateKey);
PEMParser pemParser = new PEMParser(pemReader);
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();

converter.getPrivateKey(object);
return converter.getPrivateKey(object);
} catch (IOException e) {
throw new BusinessException(ErrorType.UNHANDLED_EXCEPTION, e.getMessage());
}
}

private String createClientSecret() {
Date expirationDate = Date.from(LocalDateTime.now()
.plusMinutes(10)
.atZone(ZoneId.systemDefault())
.toInstant());

return Jwts.builder()
.header()
.add("kid", keyId)
.add("alg", "ES256")
.and()
.issuer(teamId)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(expirationDate)
.audience()
.add("https://appleid.apple.com")
.and()
.subject(clientId)
.signWith(getPrivateKey(), SIG.ES256)
.compact();
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/dnd/runus/client/vo/AppleAuthRevokeRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.dnd.runus.client.vo;


import lombok.Builder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

@Builder
public record AppleAuthRevokeRequest(
String client_id,
String client_secret,
String token,
String token_type_hint
) {
public MultiValueMap<String, String> toMultiValueMap() {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("client_id", client_id);
params.add("client_secret", client_secret);
params.add("token", token);
params.add("token_type_hint", token_type_hint);

return params;
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/dnd/runus/client/vo/AppleAuthTokenRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.dnd.runus.client.vo;

import lombok.Builder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

@Builder
public record AppleAuthTokenRequest(
String code,
String client_id,
String client_secret,
String grant_type
) {

public MultiValueMap<String, String> toMultiValueMap() {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("code", code);
params.add("client_id", client_id);
params.add("client_secret", client_secret);
params.add("grant_type", grant_type);

return params;
Jaewon-pro marked this conversation as resolved.
Show resolved Hide resolved
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/dnd/runus/client/vo/AppleAuthTokenResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dnd.runus.client.vo;


public record AppleAuthTokenResponse(
String access_token,
String expires_in,
String id_token,
String refresh_token,
String token_type,
String error
) {
Jaewon-pro marked this conversation as resolved.
Show resolved Hide resolved

}
11 changes: 11 additions & 0 deletions src/main/java/com/dnd/runus/client/web/AppleAuthClient.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.dnd.runus.client.web;

import com.dnd.runus.client.vo.AppleAuthRevokeRequest;
import com.dnd.runus.client.vo.AppleAuthTokenRequest;
import com.dnd.runus.client.vo.AppleAuthTokenResponse;
import com.dnd.runus.client.vo.OidcPublicKeyList;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
Expand All @@ -13,4 +16,12 @@ public class AppleAuthClient {
public OidcPublicKeyList getPublicKeys() {
return appleAuthClientComponent.getPublicKeys();
}

public AppleAuthTokenResponse getAccessAppleToken(AppleAuthTokenRequest request) {
return appleAuthClientComponent.getAccessAppleToken(request.toMultiValueMap());
}

public void revokeAccount(AppleAuthRevokeRequest request) {
appleAuthClientComponent.revoke(request.toMultiValueMap());
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
package com.dnd.runus.client.web;

import com.dnd.runus.client.vo.AppleAuthTokenResponse;
import com.dnd.runus.client.vo.OidcPublicKeyList;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;

@Component
@HttpExchange
public interface AppleAuthClientComponent {

@GetExchange("/keys")
OidcPublicKeyList getPublicKeys();

@PostExchange(
url = "/token",
contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
accept = MediaType.APPLICATION_JSON_VALUE)
AppleAuthTokenResponse getAccessAppleToken(@RequestBody MultiValueMap<String, String> request);

@PostExchange(
url = "/revoke",
contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
accept = MediaType.APPLICATION_JSON_VALUE)
void revoke(@RequestBody MultiValueMap<String, String> request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@

import java.time.OffsetDateTime;

public record BadgeAchievement(Badge badge, Member member, OffsetDateTime createdAt, OffsetDateTime updatedAt) {}
public record BadgeAchievement(Badge badge, Member member, OffsetDateTime createdAt, OffsetDateTime updatedAt) {

public BadgeAchievement(Badge badge, Member member) {
this(badge, member, null, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.dnd.runus.domain.badge;

public interface BadgeAchievementRepository {

void deleteByMemberId(long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.dnd.runus.domain.member;

public interface MemberLevelRepository {

void deleteByMemberId(long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ public interface MemberRepository {
Optional<Member> findById(long id);

Member save(Member member);

void deleteById(long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
import java.util.Optional;

public interface SocialProfileRepository {

Optional<SocialProfile> findById(Long socialProfileId);

Optional<SocialProfile> findBySocialTypeAndOauthId(SocialType socialType, String oauthId);

SocialProfile save(SocialProfile socialProfile);

void updateOauthEmail(long socialProfileId, String oauthEmail);

void deleteByMemberId(long memberId);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.dnd.runus.domain.oauth.controller;

import com.dnd.runus.domain.oauth.dto.request.OauthRequest;
import com.dnd.runus.domain.oauth.dto.request.WithdrawRequest;
import com.dnd.runus.domain.oauth.dto.response.TokenResponse;
import com.dnd.runus.domain.oauth.service.OauthService;
import com.dnd.runus.global.exception.type.ApiErrorType;
import com.dnd.runus.global.exception.type.ErrorType;
import com.dnd.runus.presentation.annotation.MemberId;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -30,4 +32,9 @@ public class OauthController {
public TokenResponse signIn(@Valid @RequestBody OauthRequest request) {
return oauthService.signIn(request);
}

@PostMapping("/withdraw")
public void withdraw(@MemberId long memberId, @Valid @RequestBody WithdrawRequest request) {
oauthService.withdraw(memberId, request);
}
}
Loading