Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.chaineeproject.chainee;

import com.chaineeproject.chainee.config.AppProperties;
import com.chaineeproject.chainee.jwt.JwtProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@EnableConfigurationProperties(JwtProperties.class)
@EnableConfigurationProperties({JwtProperties.class, AppProperties.class})
public class ChaineeApplication {
public static void main(String[] args) {
SpringApplication.run(ChaineeApplication.class, args);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.chaineeproject.chainee.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app")
public record AppProperties(String frontendBaseUrl, Oauth2Props oauth2) {
public record Oauth2Props(String callbackPath) {}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
// src/main/java/com/chaineeproject/chainee/security/handler/CustomOAuth2SuccessHandler.java
package com.chaineeproject.chainee.security.handler;

import com.chaineeproject.chainee.auth.AuthService;
import com.chaineeproject.chainee.auth.AuthTokens;
import com.chaineeproject.chainee.config.AppProperties;
import com.chaineeproject.chainee.jwt.JwtProperties;
import com.chaineeproject.chainee.security.CustomOauth2UserDetails;
import com.chaineeproject.chainee.security.oauth.CookieUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@RequiredArgsConstructor
@Component
@Slf4j
public class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler {

private final AuthService authService; // ✅ TokenService 대신 AuthService
private final JwtProperties jwtProps; // TTL/쿠키 정책은 yml에서
private final ObjectMapper om = new ObjectMapper();
private final AuthService authService; // Access/Refresh 발급
private final JwtProperties jwtProps; // TTL/쿠키 정책
private final AppProperties appProps; // 프론트 콜백 URL 구성

@Override
public void onAuthenticationSuccess(
Expand All @@ -34,51 +37,71 @@ public void onAuthenticationSuccess(

CustomOauth2UserDetails u = (CustomOauth2UserDetails) authentication.getPrincipal();

// 1) Access/Refresh 한 번에 발급 + Refresh 해시 저장(DB)
// 1) 토큰 한 번에 발급
AuthTokens t = authService.issueFor(u.getUser());

// 2) 단계 안내
// 2) 다음 단계 결정
String nextStep = !u.isKycVerified() ? "KYC" : (!u.isDidVerified() ? "DID" : "HOME");

// 3) Refresh 쿠키 설정 (도메인 분기: 로컬= null, 운영= chainee.store)
// 3) Refresh 토큰은 HttpOnly 쿠키로 내려서 JS에서 읽히지 않게
int maxAge = (int) jwtProps.refreshTtl().toSeconds();
boolean secure = Boolean.TRUE.equals(jwtProps.cookie().secure());
boolean secure = Boolean.TRUE.equals(jwtProps.cookie().secure());
String sameSite = jwtProps.cookie().sameSite();
String cookieDomain = cookieDomainFor(request);
CookieUtil.addCookie(
response,
jwtProps.refreshCookieName(),
t.refreshToken(),
cookieDomain,
true, // HttpOnly
secure, // Secure
sameSite, // Lax/None 등
true, // HttpOnly
secure, // Secure
sameSite, // e.g., Lax / None
maxAge
);

// 4) 응답 바디 (SPA가 accessToken 저장/사용)
var body = Map.of(
"authenticated", true,
"registered", !u.isNewUser(),
"email", u.getUsername(),
"status", Map.of("kyc", u.isKycVerified(), "did", u.isDidVerified()),
"nextStep", nextStep,
// 4) Access 토큰/상태는 URL fragment(#)로 전달 → 서버/프록시 로그에 남지 않음
// 예) https://app.chainee.store/auth/callback#accessToken=...&accessExp=...&nextStep=KYC&registered=true&email=...
String base = normalizeBase(appProps.frontendBaseUrl()) + appProps.oauth2().callbackPath();
String redirectUrl = buildFragmentUrl(
base,
"accessToken", t.accessToken(),
"accessExp", t.accessExpEpochSec(),
"refreshExp", t.refreshExpEpochSec()
"accessExp", String.valueOf(t.accessExpEpochSec()),
"refreshExp", String.valueOf(t.refreshExpEpochSec()),
"nextStep", nextStep,
"registered", String.valueOf(!u.isNewUser()),
"email", u.getUsername()
);

response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write(om.writeValueAsString(body));
response.setStatus(HttpStatus.FOUND.value()); // 302
response.setHeader("Location", redirectUrl);
// 바디 없음
}

/** 운영 도메인에서만 Domain=chainee.store 지정, 로컬/개발은 null로 둬야 쿠키가 저장됨 */
private String cookieDomainFor(HttpServletRequest req) {
String host = req.getServerName();
if (host != null && (host.equals("chainee.store") || host.endsWith(".chainee.store"))) {
return "chainee.store";
return "chainee.store"; // 운영에서만 Domain 지정
}
return null; // dev/local (localhost/127.0.0.1 등) 에서는 Domain 미지정
}

private String normalizeBase(String base) {
if (base == null || base.isBlank()) return "/";
return base.endsWith("/") ? base.substring(0, base.length() - 1) : base;
}

private String enc(String s) {
return URLEncoder.encode(s, StandardCharsets.UTF_8);
}

/** fragment에 key=value&key=value 형태로 안전하게 넣는다 */
private String buildFragmentUrl(String base, String... kv) {
StringBuilder sb = new StringBuilder(base);
sb.append("#");
for (int i = 0; i < kv.length; i += 2) {
if (i > 0) sb.append("&");
sb.append(enc(kv[i])).append("=").append(enc(kv[i + 1]));
}
return null; // localhost/127.0.0.1 등 개발환경
return sb.toString();
}
}
6 changes: 5 additions & 1 deletion src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@ app:
refresh-cookie-name: refresh_token
cookie:
secure: false # 로컬은 http라 false
same-site: Lax
same-site: Lax

frontend-baseurl: https://app.chainee.store
oauth2:
callback-path: /auth/callback
6 changes: 5 additions & 1 deletion src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ app:
refresh-cookie-name: refresh_token
cookie:
secure: true
same-site: Lax
same-site: Lax

frontend-baseurl: https://app.chainee.store
oauth2:
callback-path: /auth/callback