Skip to content
Draft
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
19 changes: 19 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@ KAKAO_REST_API_KEY=
KAKAO_JAVASCRIPT_KEY=
KAKAO_NATIVE_APP_KEY=

# Optional OAuth login client credentials.
# Redirect URI template: {backend-or-proxy-origin}/login/oauth2/code/{provider}
# Example local callbacks:
# - http://127.0.0.1:8080/login/oauth2/code/google
# - http://127.0.0.1:8080/login/oauth2/code/naver
# - http://127.0.0.1:8080/login/oauth2/code/kakao
# For a public tunnel proxy demo, set this to the public frontend origin.
# Example callback: https://example.ngrok-free.dev/login/oauth2/code/google
APP_AUTH_OAUTH_REDIRECT_BASE_URL=
OAUTH_GOOGLE_CLIENT_ID=
OAUTH_GOOGLE_CLIENT_SECRET=
OAUTH_NAVER_CLIENT_ID=
OAUTH_NAVER_CLIENT_SECRET=
OAUTH_KAKAO_CLIENT_ID=
# Kakao client secret is optional unless the Kakao app has client secret verification enabled.
OAUTH_KAKAO_CLIENT_SECRET=
APP_AUTH_OAUTH_SUCCESS_REDIRECT=http://127.0.0.1:5173/dashboard
APP_AUTH_OAUTH_FAILURE_REDIRECT=http://127.0.0.1:5173/auth/login?oauth=failed

# --- 적재/갱신 주기 ---
# 실거래(MOLIT): 월 단위 발행 + 최근 달 지각신고 → 하루 1회 갱신이면 충분.
# 기동 시 realestate-seed가 1회 멱등 적재, realestate-refresh가 현재월 재수집 +
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ artifacts/
output/
.codex/
.playwright-mcp/
.playwright-cli/
.run-logs/
/indicators-domestic-snapshot.md
/dashboard-feed-title-snapshot.md
/complex-trade-history-card.png
Expand Down
4 changes: 4 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.youbuyfirst.backend.auth;

import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

@Service
public class AppOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

private final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
private final OAuthAccountService oauthAccountService;

public AppOAuth2UserService(OAuthAccountService oauthAccountService) {
this.oauthAccountService = oauthAccountService;
}

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauthUser = delegate.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuthProviderProfile profile = OAuthProviderProfile.from(registrationId, oauthUser.getAttributes());
AppUser user = oauthAccountService.findOrCreateUser(profile);
return AppUserPrincipal.oauth(user, oauthUser.getAttributes());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.youbuyfirst.backend.auth;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Service;

@Service
public class AppOidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {

private final OAuth2UserService<OidcUserRequest, OidcUser> delegate;
private final OAuthAccountService oauthAccountService;

@Autowired
public AppOidcUserService(OAuthAccountService oauthAccountService) {
this(oauthAccountService, new OidcUserService());
}

AppOidcUserService(
OAuthAccountService oauthAccountService,
OAuth2UserService<OidcUserRequest, OidcUser> delegate
) {
this.oauthAccountService = oauthAccountService;
this.delegate = delegate;
}

@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser oidcUser = delegate.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuthProviderProfile profile = OAuthProviderProfile.from(registrationId, oidcUser.getAttributes());
AppUser user = oauthAccountService.findOrCreateUser(profile);
return AppUserPrincipal.oidc(
user,
oidcUser.getAttributes(),
oidcUser.getIdToken(),
oidcUser.getUserInfo()
);
}
}
22 changes: 22 additions & 0 deletions backend/src/main/java/com/youbuyfirst/backend/auth/AppUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,28 @@ public static AppUser local(String username, String email, String displayName, S
);
}

public static AppUser oauth(
String username,
String email,
String displayName,
String passwordHash,
String authProvider,
Instant now
) {
return new AppUser(
UUID.randomUUID().toString(),
username,
email,
displayName,
passwordHash,
authProvider,
"USER",
"active",
now,
now
);
}

public void markSeen(Instant now) {
this.lastSeenAt = now;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class AppUserPrincipal implements UserDetails {
public class AppUserPrincipal implements UserDetails, OAuth2User, OidcUser {

private final String userId;
private final String username;
Expand All @@ -16,19 +22,43 @@ public class AppUserPrincipal implements UserDetails {
private final String passwordHash;
private final String status;
private final List<GrantedAuthority> authorities;

private AppUserPrincipal(AppUser user) {
private final Map<String, Object> attributes;
private final OidcIdToken idToken;
private final OidcUserInfo userInfo;

private AppUserPrincipal(
AppUser user,
Map<String, Object> attributes,
OidcIdToken idToken,
OidcUserInfo userInfo
) {
this.userId = user.getId();
this.username = user.getUsername();
this.email = user.getEmail();
this.displayName = user.getDisplayName();
this.passwordHash = user.getPasswordHash();
this.status = user.getStatus();
this.authorities = List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
this.attributes = copyNonNull(attributes);
this.idToken = idToken;
this.userInfo = userInfo;
}

public static AppUserPrincipal from(AppUser user) {
return new AppUserPrincipal(user);
return new AppUserPrincipal(user, Map.of(), null, null);
}

public static AppUserPrincipal oauth(AppUser user, Map<String, Object> attributes) {
return new AppUserPrincipal(user, attributes, null, null);
}

public static AppUserPrincipal oidc(
AppUser user,
Map<String, Object> attributes,
OidcIdToken idToken,
OidcUserInfo userInfo
) {
return new AppUserPrincipal(user, attributes, idToken, userInfo);
}

public String getUserId() {
Expand All @@ -52,6 +82,31 @@ public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

@Override
public Map<String, Object> getAttributes() {
return attributes;
}

@Override
public Map<String, Object> getClaims() {
return attributes;
}

@Override
public OidcUserInfo getUserInfo() {
return userInfo;
}

@Override
public OidcIdToken getIdToken() {
return idToken;
}

@Override
public String getName() {
return userId;
}

@Override
public String getPassword() {
return passwordHash;
Expand Down Expand Up @@ -81,4 +136,13 @@ public boolean isCredentialsNonExpired() {
public boolean isEnabled() {
return "active".equals(status);
}

private static Map<String, Object> copyNonNull(Map<String, Object> attributes) {
if (attributes == null || attributes.isEmpty()) {
return Map.of();
}
return attributes.entrySet().stream()
.filter(entry -> entry.getKey() != null && entry.getValue() != null)
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.youbuyfirst.backend.auth.dto.CurrentUserResponse;
import com.youbuyfirst.backend.auth.dto.LoginRequest;
import com.youbuyfirst.backend.auth.dto.OAuthProviderStatusResponse;
import com.youbuyfirst.backend.auth.dto.RegisterRequest;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -25,16 +26,24 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

private final AuthService authService;
private final AuthenticationManager authenticationManager;
private final OAuthProviderStatusService oauthProviderStatusService;

public AuthController(AuthService authService, AuthenticationManager authenticationManager) {
public AuthController(
AuthService authService,
AuthenticationManager authenticationManager,
OAuthProviderStatusService oauthProviderStatusService
) {
this.authService = authService;
this.authenticationManager = authenticationManager;
this.oauthProviderStatusService = oauthProviderStatusService;
}

@PostMapping("/register")
Expand Down Expand Up @@ -65,6 +74,11 @@ public CurrentUserResponse me(Authentication authentication) {
return CurrentUserResponse.from(authService.markSeen(principal.getUserId()));
}

@GetMapping("/oauth/providers")
public List<OAuthProviderStatusResponse> oauthProviders() {
return oauthProviderStatusService.providers();
}

@PostMapping("/logout")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void logout(HttpServletRequest request, HttpServletResponse response) {
Expand Down
Loading
Loading