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: 애플 로그인 구현 #35

Merged
merged 20 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
27 changes: 27 additions & 0 deletions src/main/java/com/dnd/runus/auth/oidc/provider/OidcProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.dnd.runus.auth.oidc.provider;

import com.dnd.runus.global.exception.BusinessException;
import com.dnd.runus.global.exception.type.ErrorType;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;

import java.io.IOException;
import java.util.Map;

import static com.dnd.runus.global.util.DecodeUtils.decodeBase64;

public interface OidcProvider {

Claims getClaimsBy(String idToken);

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

try {
return new ObjectMapper().readValue(decodeBase64(header), new TypeReference<>() {});
hee9841 marked this conversation as resolved.
Show resolved Hide resolved
} catch (IOException e) {
throw new BusinessException(ErrorType.FAILED_PARSING, e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.dnd.runus.auth.oidc.provider;

import com.dnd.runus.auth.oidc.provider.impl.AppleAuthProvider;
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 org.springframework.stereotype.Component;

import java.util.EnumMap;
import java.util.Map;

import static java.util.Objects.isNull;

@Component
public class OidcProviderFactory {

private final Map<SocialType, OidcProvider> authProviderMap;
private final AppleAuthProvider appleAuthProvider;

public OidcProviderFactory(AppleAuthProvider appleAuthProvider) {
authProviderMap = new EnumMap<>(SocialType.class);
this.appleAuthProvider = appleAuthProvider;

init();
}

private void init() {
authProviderMap.put(SocialType.APPLE, appleAuthProvider);
}
hee9841 marked this conversation as resolved.
Show resolved Hide resolved

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

private OidcProvider getProvider(SocialType socialType) {
OidcProvider oidcProvider = authProviderMap.get(socialType);

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

return oidcProvider;
}
hee9841 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.dnd.runus.auth.oidc.provider;

import com.dnd.runus.client.vo.OidcPublicKey;
import com.dnd.runus.client.vo.OidcPublicKeyList;
import com.dnd.runus.global.exception.BusinessException;
import com.dnd.runus.global.exception.type.ErrorType;
import org.springframework.stereotype.Component;

import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.Map;

import static com.dnd.runus.global.util.DecodeUtils.decodeBase64;

@Component
public class PublicKeyProvider {

public PublicKey generatePublicKey(final Map<String, String> tokenHeaders, final OidcPublicKeyList publicKeys) {

OidcPublicKey publicKey = publicKeys.getMatchedKeyBy(tokenHeaders.get("kid"), tokenHeaders.get("alg"));

return getPublicKey(publicKey);
}

private PublicKey getPublicKey(final OidcPublicKey key) {

try {
final byte[] nByte = decodeBase64(key.n());
final byte[] eByte = decodeBase64(key.e());

BigInteger n = new BigInteger(1, nByte);
BigInteger e = new BigInteger(1, eByte);

RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e);
KeyFactory keyFactory = KeyFactory.getInstance(key.kty());

return keyFactory.generatePublic(publicKeySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new BusinessException(ErrorType.UNSUPPORTED_JWT_TOKEN, e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.dnd.runus.auth.oidc.provider.impl;

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.OidcPublicKeyList;
import com.dnd.runus.client.web.AppleAuthClient;
import com.dnd.runus.global.exception.type.ErrorType;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.SignatureException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.security.PublicKey;

@Component
@RequiredArgsConstructor
public class AppleAuthProvider implements OidcProvider {

private final AppleAuthClient appleAuthClient;
private final PublicKeyProvider publicKeyProvider;

@Override
public Claims getClaimsBy(String identityToken) {
// 퍼블릭 키 리스트
OidcPublicKeyList publicKeys = appleAuthClient.getPublicKeys();
// 토큰 헤더에서 디코딩 -> 퍼블릭 키 리스트 대조회 n,e갑 디코딩 후 퍼블릭 키 생성
PublicKey publicKey = publicKeyProvider.generatePublicKey(parseHeaders(identityToken), publicKeys);

return parseClaims(identityToken, publicKey);
}

public Claims parseClaims(String token, PublicKey publicKey) {
hee9841 marked this conversation as resolved.
Show resolved Hide resolved
try {
return Jwts.parser()
.verifyWith(publicKey)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (SignatureException | MalformedJwtException e) {
// 토큰 서명 검증 또는 구조 문제
throw new AuthException(ErrorType.MALFORMED_ACCESS_TOKEN, e.getMessage());
} catch (ExpiredJwtException e) {
throw new AuthException(ErrorType.INVALID_ACCESS_TOKEN, e.getMessage());
}
}
}
34 changes: 34 additions & 0 deletions src/main/java/com/dnd/runus/client/config/RestClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.dnd.runus.client.config;

import com.dnd.runus.client.web.AppleAuthClientComponent;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.ClientHttpRequestFactories;
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

import java.time.Duration;

@Configuration
public class RestClientConfig {

@Bean
public AppleAuthClientComponent appleAuthClientService(@Value("${oauth.apple.public-key-url}") String url) {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS
.withReadTimeout(Duration.ofSeconds(5))
.withConnectTimeout(Duration.ofSeconds(10));
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(settings);

RestClient restClient =
RestClient.builder().baseUrl(url).requestFactory(requestFactory).build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory =
HttpServiceProxyFactory.builderFor(adapter).build();

return factory.createClient(AppleAuthClientComponent.class);
}
}
3 changes: 3 additions & 0 deletions src/main/java/com/dnd/runus/client/vo/OidcPublicKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.dnd.runus.client.vo;

public record OidcPublicKey(String kty, String kid, String use, String alg, String n, String e) {}
17 changes: 17 additions & 0 deletions src/main/java/com/dnd/runus/client/vo/OidcPublicKeyList.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.dnd.runus.client.vo;

import com.dnd.runus.global.exception.BusinessException;
import com.dnd.runus.global.exception.type.ErrorType;

import java.util.List;

public record OidcPublicKeyList(List<OidcPublicKey> keys) {

public OidcPublicKey getMatchedKeyBy(String kid, String alg) {
// kid, alg 일치한 퍼블릭 키
return keys.stream()
.filter(key -> key.kid().equals(kid) && key.alg().equals(alg))
.findAny()
.orElseThrow(() -> new BusinessException(ErrorType.MALFORMED_ACCESS_TOKEN, "검증할 수 없는 토큰입니다."));
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/dnd/runus/client/web/AppleAuthClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dnd.runus.client.web;

import com.dnd.runus.client.vo.OidcPublicKeyList;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AppleAuthClient {

private final AppleAuthClientComponent appleAuthClientComponent;

public OidcPublicKeyList getPublicKeys() {
return appleAuthClientComponent.getPublicKeys();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dnd.runus.client.web;

import com.dnd.runus.client.vo.OidcPublicKeyList;
import org.springframework.stereotype.Component;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;

@Component
@HttpExchange
public interface AppleAuthClientComponent {
@GetExchange("/keys")
OidcPublicKeyList getPublicKeys();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.dnd.runus.domain.login.controller;

import com.dnd.runus.domain.login.dto.request.LoginRequest;
import com.dnd.runus.domain.login.dto.response.TokenResponse;
import com.dnd.runus.domain.login.service.LoginService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/api/v1/sign")
@RestController
@RequiredArgsConstructor
public class SignController {

private final LoginService loginService;

@PostMapping("/in")
public TokenResponse signUp(@Valid @RequestBody LoginRequest request) {
return loginService.login(request);
hee9841 marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.dnd.runus.domain.login.dto.request;

import com.dnd.runus.global.constant.SocialType;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;


public record LoginRequest(
@NotNull
SocialType socialType,
@NotBlank
String idToken,
@NotBlank
@Email
String email,
@NotBlank
String nickName
){
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dnd.runus.domain.login.dto.response;


import com.dnd.runus.auth.token.dto.AuthTokenDto;

public record TokenResponse(
String accessToken,
//todo refresh token 구현 되면
// String refreshToken,
String grantType
) {

public static TokenResponse from(AuthTokenDto tokenDto) {
return new TokenResponse(tokenDto.accessToken(), tokenDto.type());
}
}
Loading