Skip to content

Commit

Permalink
[PEAUTY-19] Impl social sign in (#3)
Browse files Browse the repository at this point in the history
* PEAUTY-19 add auth module

* env: add gitignore

* feat: application.yml and etc env

* feat: auth module

* feat: customer module

* feat: designer module

* feat: domain module

* feat: persistence module

* feat: local token exp update

* env: temp app key

* env: hide kakao client key
  • Loading branch information
weejinyoung authored Nov 21, 2024
1 parent 2b359ae commit e1813e5
Show file tree
Hide file tree
Showing 85 changed files with 1,866 additions and 87 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,7 @@ out/
*.zip
*.tar.gz
*.rar
!gradle/wrapper/gradle-wrapper.jar
!gradle/wrapper/gradle-wrapper.jar

# For secret #
.env
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ subprojects {
}

dependencies {

compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: Peauty
MYSQL_USER: Peauty
MYSQL_DATABASE: peauty
MYSQL_USER: peauty
MYSQL_PASSWORD: 1234
MYSQL_ROOT_PASSWORD: 1234
20 changes: 20 additions & 0 deletions peauty-auth/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
bootJar {
enabled = false
}

jar {
enabled = true
}

dependencies {
implementation(project(':peauty-domain'))
implementation(project(':peauty-cache'))
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("com.google.api-client:google-api-client-jackson2:2.2.0")
implementation("com.google.api-client:google-api-client:2.2.0")
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.peauty.auth.annotation;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessToken {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.peauty.auth.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RefreshToken {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.peauty.auth.annotation;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface SignedUser {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.peauty.auth.client;

import com.peauty.domain.exception.PeautyException;
import com.peauty.domain.response.PeautyResponseCode;
import com.peauty.auth.properties.OAuthProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
@RequiredArgsConstructor
public class AppleAuthClient implements ExternalAuthClient {

private final RestClient restClient;
private final OAuthProperties oAuthProperties;

@Override
public OidcPublicKeyList getPublicKeys() {
OidcPublicKeyList publicKeys = restClient.get()
.uri(oAuthProperties.applePublicKeyUrl())
.retrieve()
.body(OidcPublicKeyList.class);

if (publicKeys == null) {
throw new PeautyException(PeautyResponseCode.APPLE_AUTH_CLIENT_ERROR);
}

return publicKeys;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.peauty.auth.client;

public interface ExternalAuthClient {
OidcPublicKeyList getPublicKeys();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.peauty.auth.client;


import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.peauty.domain.exception.PeautyException;
import com.peauty.domain.response.PeautyResponseCode;
import com.peauty.auth.properties.OAuthProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient;
import org.springframework.web.util.UriComponentsBuilder;

@Component
@RequiredArgsConstructor
@Slf4j
public class KakaoAuthClient implements ExternalAuthClient {

private final RestClient restClient;
private final OAuthProperties oAuthProperties;

@Override
public OidcPublicKeyList getPublicKeys() {
OidcPublicKeyList publicKeys = restClient.get()
.uri(oAuthProperties.kakaoPublicKeyInfo())
.retrieve()
.body(OidcPublicKeyList.class);

if (publicKeys == null) {
throw new PeautyException(PeautyResponseCode.KAKAO_AUTH_CLIENT_ERROR);
}

return publicKeys;
}

public String getIdTokenFromKakao(String authCode) {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("grant_type", "authorization_code");
formData.add("client_id", oAuthProperties.kakaoClientId());
formData.add("redirect_uri", oAuthProperties.kakaoRedirectUrl());
formData.add("code", authCode);
String response = restClient.post()
.uri("https://kauth.kakao.com/oauth/token")
.header("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
.body(formData)
.retrieve()
.body(String.class);
try {
JsonNode node = new ObjectMapper().readTree(response);
return node.get("id_token").asText();
} catch (Exception e) {
throw new PeautyException(PeautyResponseCode.INTERNAL_SERVER_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.peauty.auth.client;

public record OidcPublicKey(
String kid,
String kty,
String alg,
String use,
String n,
String e
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.peauty.auth.client;

import com.peauty.domain.exception.PeautyException;
import com.peauty.domain.response.PeautyResponseCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class OidcPublicKeyList {

private List<OidcPublicKey> keys;

public OidcPublicKey getMatchedKey(String kid, String alg) {
return keys.stream()
.filter(key -> key.kid().equals(kid) && key.alg().equals(alg))
.findFirst()
.orElseThrow(() -> new PeautyException(PeautyResponseCode.INTERNAL_SERVER_ERROR));
}
}
24 changes: 24 additions & 0 deletions peauty-auth/src/main/java/com/peauty/auth/config/JwtConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.peauty.auth.config;

import com.peauty.auth.properties.JwtProperties;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.security.Key;

@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(JwtProperties.class)
public class JwtConfig {

private final JwtProperties jwtProperties;

@Bean
public Key key() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtProperties.secretKey()));
}
}
41 changes: 41 additions & 0 deletions peauty-auth/src/main/java/com/peauty/auth/config/OAuthConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.peauty.auth.config;

import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.peauty.auth.properties.JwtProperties;
import com.peauty.auth.properties.OAuthProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

import java.time.Duration;
import java.util.Collections;

@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(OAuthProperties.class)
public class OAuthConfig {

private final OAuthProperties oAuthProperties;

@Bean
public GoogleIdTokenVerifier googleIdTokenVerifier() {
return new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), GsonFactory.getDefaultInstance())
.setAudience(Collections.singletonList(oAuthProperties.googleClientId()))
.build();
}

// For AuthClient
@Bean
public RestClient restClient() {
var restTemplate = new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(10))
.setReadTimeout(Duration.ofSeconds(5))
.build();
return RestClient.create(restTemplate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.peauty.auth.config;

import com.peauty.auth.filter.JwtAuthenticationFilter;
import com.peauty.auth.filter.JwtExceptionFilter;
import com.peauty.auth.properties.JwtProperties;
import com.peauty.auth.properties.OAuthProperties;
import com.peauty.auth.resolver.AccessTokenResolver;
import com.peauty.auth.resolver.RefreshTokenResolver;
import com.peauty.auth.resolver.SignedUserResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

// TODO 의존하는 모듈에서의 자체 확장 가능하게 ConditionalMissingBean?
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig implements WebMvcConfigurer {

private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtExceptionFilter jwtExceptionFilter;

@Bean
public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
return http
.httpBasic(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.sessionManagement(this::setSessionManagement)
.authorizeHttpRequests(this::setAuthorizePath)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class)
.build();
}

private void setSessionManagement(SessionManagementConfigurer<HttpSecurity> sessionManagement) {
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

private void setAuthorizePath(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry authorize) {
authorize.requestMatchers(
"/sign-out"
).hasAnyRole("CUSTOMER", "DESIGNER", "ADMIN")
.requestMatchers(
"/**"
).permitAll()
.anyRequest().authenticated();
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new SignedUserResolver());
resolvers.add(new AccessTokenResolver());
resolvers.add(new RefreshTokenResolver());
}
}
Loading

0 comments on commit e1813e5

Please sign in to comment.