Skip to content

Commit

Permalink
[#37] Kakao Oauth 기능 추가 (#39)
Browse files Browse the repository at this point in the history
* [#37] feat, move: common에 jwt, properties 이동
- common 모듈 Test룰 위해 실행 가능한 application 생성
- Jwt와 properties 의존성 및 yml 설정 이동
- 각 환경 테스트를 위해 common.yml 파일 resolver에 추가

* [#37] feat, test: OIDC를 위한 도메인 필드 값 변경

* [#37] feat: Kakao Oauth OIDC방식 기능 추가

* [#37] style: spotless
  • Loading branch information
kdomo authored Aug 3, 2023
1 parent 0f09064 commit f810046
Show file tree
Hide file tree
Showing 63 changed files with 1,014 additions and 113 deletions.
4 changes: 4 additions & 0 deletions TodaysFail-Common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ jar.enabled = true

dependencies {
api 'org.springframework.boot:spring-boot-starter-aop'

implementation ("io.jsonwebtoken:jjwt-api:0.11.5")
runtimeOnly ( "io.jsonwebtoken:jjwt-impl:0.11.5")
runtimeOnly ( "io.jsonwebtoken:jjwt-jackson:0.11.5")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.todaysfail.common;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TodaysFailCommonApplication {
public static void main(String[] args) {
SpringApplication.run(TodaysFailCommonApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.todaysfail.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Helper {}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class TodaysFailConst {
public static final String AUTH_HEADER = "Authorization";
public static final String BEARER = "Bearer";
public static final String BEARER = "Bearer ";
public static final String WITHDRAW_PREFIX = "DELETED:";
public static final String TOKEN_ROLE = "role";
public static final String TOKEN_TYPE = "type";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.todaysfail.common.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class OIDCDecodePayload {
/** issuer ex https://kauth.kakao.com */
private String iss;
/** client id */
private String aud;
/** oauth provider account unique id */
private String sub;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.todaysfail.common.jwt;

import com.todaysfail.common.dto.OIDCDecodePayload;
import com.todaysfail.common.exception.ExpiredTokenException;
import com.todaysfail.common.exception.InvalidTokenException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
@Slf4j
public class JwtOIDCProvider {

private final String KID = "kid";

public String getKidFromUnsignedTokenHeader(String token, String iss, String aud) {
return (String) getUnsignedTokenClaims(token, iss, aud).getHeader().get(KID);
}

private Jwt<Header, Claims> getUnsignedTokenClaims(String token, String iss, String aud) {
try {
return Jwts.parserBuilder()
.requireAudience(aud)
.requireIssuer(iss)
.build()
.parseClaimsJwt(getUnsignedToken(token));
} catch (ExpiredJwtException e) {
throw ExpiredTokenException.EXCEPTION;
} catch (Exception e) {
log.error(e.toString());
throw InvalidTokenException.EXCEPTION;
}
}

private String getUnsignedToken(String token) {
String[] splitToken = token.split("\\.");
if (splitToken.length != 3) throw InvalidTokenException.EXCEPTION;
return splitToken[0] + "." + splitToken[1] + ".";
}

public Jws<Claims> getOIDCTokenJws(String token, String modulus, String exponent) {
try {
return Jwts.parserBuilder()
.setSigningKey(getRSAPublicKey(modulus, exponent))
.build()
.parseClaimsJws(token);
} catch (ExpiredJwtException e) {
throw ExpiredTokenException.EXCEPTION;
} catch (Exception e) {
log.error(e.toString());
throw InvalidTokenException.EXCEPTION;
}
}

public OIDCDecodePayload getOIDCTokenBody(String token, String modulus, String exponent) {
Claims body = getOIDCTokenJws(token, modulus, exponent).getBody();
return new OIDCDecodePayload(body.getIssuer(), body.getAudience(), body.getSubject());
}

private Key getRSAPublicKey(String modulus, String exponent)
throws NoSuchAlgorithmException, InvalidKeySpecException {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] decodeN = Base64.getUrlDecoder().decode(modulus);
byte[] decodeE = Base64.getUrlDecoder().decode(exponent);
BigInteger n = new BigInteger(1, decodeN);
BigInteger e = new BigInteger(1, decodeE);

RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e);
return keyFactory.generatePublic(keySpec);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.todaysfail.config.jwt;
package com.todaysfail.common.jwt;

import static com.todaysfail.common.consts.TodaysFailConst.*;

import com.todaysfail.common.exception.ExpiredTokenException;
import com.todaysfail.common.exception.InvalidTokenException;
import com.todaysfail.common.exception.RefreshTokenExpiredException;
import com.todaysfail.common.properties.JwtProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
Expand All @@ -13,19 +14,13 @@
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class JwtTokenHelper {
@Value("${auth.jwt.secret-key}")
private String jwtSecretKey;

@Value("${auth.jwt.access-exp}")
private Long jwtAccessExp;

@Value("${auth.jwt.refresh-exp}")
private Long jwtRefreshExp;
private final JwtProperties jwtProperties;

private Jws<Claims> getJws(String token) {
try {
Expand All @@ -38,7 +33,7 @@ private Jws<Claims> getJws(String token) {
}

private Key getSecretKey() {
return Keys.hmacShaKeyFor(jwtSecretKey.getBytes(StandardCharsets.UTF_8));
return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8));
}

private String buildAccessToken(
Expand Down Expand Up @@ -70,15 +65,15 @@ private String buildRefreshToken(Long id, Date issuedAt, Date accessTokenExpires
public String generateAccessToken(Long id, String role) {
final Date issuedAt = new Date();
final Date accessTokenExpiresIn =
new Date(issuedAt.getTime() + jwtAccessExp * MILLI_TO_SECOND);
new Date(issuedAt.getTime() + jwtProperties.getAccessExp() * MILLI_TO_SECOND);

return buildAccessToken(id, issuedAt, accessTokenExpiresIn, role);
}

public String generateRefreshToken(Long id) {
final Date issuedAt = new Date();
final Date refreshTokenExpiresIn =
new Date(issuedAt.getTime() + jwtRefreshExp * MILLI_TO_SECOND);
new Date(issuedAt.getTime() + jwtProperties.getRefreshExp() * MILLI_TO_SECOND);
return buildRefreshToken(id, issuedAt, refreshTokenExpiresIn);
}

Expand Down Expand Up @@ -111,6 +106,6 @@ public Long parseRefreshToken(String token) {
}

public Long getRefreshTokenTTlSecond() {
return jwtRefreshExp;
return jwtProperties.getRefreshExp();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.todaysfail.config.properties;
package com.todaysfail.common.properties;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@EnableConfigurationProperties({OauthProperties.class})
@EnableConfigurationProperties({OauthProperties.class, JwtProperties.class})
@Configuration
public class EnableConfigProperties {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.todaysfail.common.properties;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;

@Getter
@AllArgsConstructor
@ConfigurationProperties(prefix = "auth.jwt")
@ConstructorBinding
public class JwtProperties {
private String secretKey;
private Long accessExp;
private Long refreshExp;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.todaysfail.config.properties;
package com.todaysfail.common.properties;

import lombok.AllArgsConstructor;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

@Getter
@AllArgsConstructor
public enum AccountRole {
public enum UserRole {
USER("USER"),
ADMIN("ADMIN");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

@Getter
@AllArgsConstructor
public enum AccountStatus {
public enum UserStatus {
NORMAL("NORMAL"),
DELETED("DELETED"),
FORBIDDEN("FORBIDDEN");
Expand Down
14 changes: 14 additions & 0 deletions TodaysFail-Common/src/main/resources/application-common.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
auth:
jwt:
secret-key: ${JWT_SECRET_KEY:testkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkey}
access-exp: ${JWT_ACCESS_EXP:3600}
refresh-exp: ${JWT_REFRESH_EXP:3600}

oauth:
kakao:
base-url: ${KAKAO_BASE_URL}
client-id: ${KAKAO_CLIENT}
client-secret: ${KAKAO_SECRET}
redirect-url: ${KAKAO_REDIRECT}
app-id: ${KAKAO_APP_ID:default}
admin-key: ${KAKAO_ADMIN_KEY}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.todaysfail.common.properties;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest
@ActiveProfiles("common")
class PropertiesTest {
@Autowired private OauthProperties oauthProperties;

@Autowired private JwtProperties jwtProperties;

@Test
void oauth_프로퍼티가_정상적으로_init_되어야한다() {
assertEquals(oauthProperties.getKakao().getAppId(), "default");
}

@Test
void jwt_프로퍼티가_정상적으로_init_되어야한다() {
Long accessExp = jwtProperties.getAccessExp();
Long refreshExp = jwtProperties.getRefreshExp();
assertEquals(accessExp, 3600);
assertEquals(refreshExp, 3600);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.todaysfail.domains.auth.domain;

public record OauthToken(String accessToken, String refreshToken, String idToken) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.todaysfail.domains.auth.domain;

import com.todaysfail.common.type.user.OauthProvider;
import com.todaysfail.domains.user.domain.OauthInfo;

public record OauthUserInfo(
String oauthId,
String profileImage,
Boolean isDefaultImage,
String name,
OauthProvider oauthProvider) {
public static OauthUserInfo of(
String oauthId,
String profileImage,
Boolean isDefaultImage,
String name,
OauthProvider oauthProvider) {
return new OauthUserInfo(oauthId, profileImage, isDefaultImage, name, oauthProvider);
}

public OauthInfo toOauthInfo() {
return OauthInfo.of(oauthId, oauthProvider);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.todaysfail.domains.auth.domain;

import com.todaysfail.domains.user.domain.UserDetail;

public record TokenAndUser(
String accessToken,
Long accessTokenExpireIn,
String refreshToken,
Long refreshTokenExpireIn,
UserDetail user) {}
Loading

0 comments on commit f810046

Please sign in to comment.