auth media
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
|
authid.txt
|
||||||
|
|||||||
7
analytics-view/.claude/settings.local.json
Normal file
7
analytics-view/.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(find /c/Users/balex/IdeaProjects/post-hub-platform -type f \\\\\\(-name *.tsx -o -name *.ts -o -name *.jsx -o -name *.js -o -name *.vue \\\\\\) ! -path */node_modules/* ! -path */.next/*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,6 +106,13 @@
|
|||||||
<version>${lombok.version}</version>
|
<version>${lombok.version}</version>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- OAuth2 Client (social login) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.posthub.gateway.config;
|
package com.posthub.gateway.config;
|
||||||
|
|
||||||
|
import com.posthub.gateway.security.OAuth2AuthenticationSuccessHandler;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
@@ -13,8 +15,11 @@ import reactor.core.publisher.Mono;
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||||
return http
|
return http
|
||||||
@@ -27,7 +32,9 @@ public class SecurityConfig {
|
|||||||
.pathMatchers(
|
.pathMatchers(
|
||||||
"/api/auth/login",
|
"/api/auth/login",
|
||||||
"/api/auth/register",
|
"/api/auth/register",
|
||||||
"/api/auth/refresh/token"
|
"/api/auth/refresh/token",
|
||||||
|
"/oauth2/authorization/**",
|
||||||
|
"/login/oauth2/code/**"
|
||||||
).permitAll()
|
).permitAll()
|
||||||
.pathMatchers(
|
.pathMatchers(
|
||||||
"/actuator/**",
|
"/actuator/**",
|
||||||
@@ -36,6 +43,9 @@ public class SecurityConfig {
|
|||||||
).permitAll()
|
).permitAll()
|
||||||
.anyExchange().permitAll()
|
.anyExchange().permitAll()
|
||||||
)
|
)
|
||||||
|
.oauth2Login(oauth2 -> oauth2
|
||||||
|
.authenticationSuccessHandler(oAuth2AuthenticationSuccessHandler)
|
||||||
|
)
|
||||||
.exceptionHandling(exceptions -> exceptions
|
.exceptionHandling(exceptions -> exceptions
|
||||||
.authenticationEntryPoint((exchange, ex) -> {
|
.authenticationEntryPoint((exchange, ex) -> {
|
||||||
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
|
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.posthub.gateway.model.entity;
|
package com.posthub.gateway.model.entity;
|
||||||
|
|
||||||
|
import com.posthub.gateway.model.enums.AuthProvider;
|
||||||
import com.posthub.gateway.model.enums.RegistrationStatus;
|
import com.posthub.gateway.model.enums.RegistrationStatus;
|
||||||
import com.posthub.gateway.model.enums.UserRole;
|
import com.posthub.gateway.model.enums.UserRole;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -40,4 +41,10 @@ public class User {
|
|||||||
|
|
||||||
@Column("role")
|
@Column("role")
|
||||||
private UserRole role;
|
private UserRole role;
|
||||||
|
|
||||||
|
@Column("auth_provider")
|
||||||
|
private AuthProvider authProvider;
|
||||||
|
|
||||||
|
@Column("provider_id")
|
||||||
|
private String providerId;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.posthub.gateway.model.enums;
|
||||||
|
|
||||||
|
public enum AuthProvider {
|
||||||
|
LOCAL,
|
||||||
|
GOOGLE,
|
||||||
|
FACEBOOK,
|
||||||
|
GITHUB
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.posthub.gateway.repository;
|
package com.posthub.gateway.repository;
|
||||||
|
|
||||||
import com.posthub.gateway.model.entity.User;
|
import com.posthub.gateway.model.entity.User;
|
||||||
|
import com.posthub.gateway.model.enums.AuthProvider;
|
||||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
@@ -11,4 +12,6 @@ public interface UserRepository extends ReactiveCrudRepository<User, Integer> {
|
|||||||
Mono<User> findByUsername(String username);
|
Mono<User> findByUsername(String username);
|
||||||
|
|
||||||
Mono<Boolean> existsByEmail(String email);
|
Mono<Boolean> existsByEmail(String email);
|
||||||
|
|
||||||
|
Mono<User> findByAuthProviderAndProviderId(AuthProvider authProvider, String providerId);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package com.posthub.gateway.security;
|
||||||
|
|
||||||
|
import com.posthub.gateway.model.entity.RefreshToken;
|
||||||
|
import com.posthub.gateway.model.entity.User;
|
||||||
|
import com.posthub.gateway.model.enums.AuthProvider;
|
||||||
|
import com.posthub.gateway.model.enums.RegistrationStatus;
|
||||||
|
import com.posthub.gateway.model.enums.UserRole;
|
||||||
|
import com.posthub.gateway.repository.RefreshTokenRepository;
|
||||||
|
import com.posthub.gateway.repository.UserRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.security.web.server.WebFilterExchange;
|
||||||
|
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OAuth2AuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final RefreshTokenRepository refreshTokenRepository;
|
||||||
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
|
@Value("${oauth2.redirect-uri:https://balexvic.com/login}")
|
||||||
|
private String redirectUri;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
|
||||||
|
OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) authentication;
|
||||||
|
OAuth2User oAuth2User = authToken.getPrincipal();
|
||||||
|
String registrationId = authToken.getAuthorizedClientRegistrationId();
|
||||||
|
|
||||||
|
AuthProvider provider = AuthProvider.valueOf(registrationId.toUpperCase());
|
||||||
|
String providerId = extractProviderId(oAuth2User, registrationId);
|
||||||
|
String email = extractEmail(oAuth2User, registrationId);
|
||||||
|
String name = extractName(oAuth2User, registrationId);
|
||||||
|
|
||||||
|
return userRepository.findByAuthProviderAndProviderId(provider, providerId)
|
||||||
|
.switchIfEmpty(
|
||||||
|
email != null
|
||||||
|
? userRepository.findByEmail(email)
|
||||||
|
.flatMap(existingUser -> {
|
||||||
|
existingUser.setAuthProvider(provider);
|
||||||
|
existingUser.setProviderId(providerId);
|
||||||
|
existingUser.setUpdated(LocalDateTime.now());
|
||||||
|
return userRepository.save(existingUser);
|
||||||
|
})
|
||||||
|
.switchIfEmpty(Mono.defer(() -> createNewUser(provider, providerId, email, name)))
|
||||||
|
: Mono.defer(() -> createNewUser(provider, providerId, email, name))
|
||||||
|
)
|
||||||
|
.flatMap(user -> {
|
||||||
|
user.setLastLogin(LocalDateTime.now());
|
||||||
|
user.setUpdated(LocalDateTime.now());
|
||||||
|
return userRepository.save(user);
|
||||||
|
})
|
||||||
|
.flatMap(this::generateTokensAndRedirect)
|
||||||
|
.flatMap(redirectUrl -> {
|
||||||
|
webFilterExchange.getExchange().getResponse().setStatusCode(HttpStatus.FOUND);
|
||||||
|
webFilterExchange.getExchange().getResponse().getHeaders().setLocation(URI.create(redirectUrl));
|
||||||
|
return webFilterExchange.getExchange().getResponse().setComplete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<User> createNewUser(AuthProvider provider, String providerId, String email, String name) {
|
||||||
|
User user = new User();
|
||||||
|
user.setAuthProvider(provider);
|
||||||
|
user.setProviderId(providerId);
|
||||||
|
user.setEmail(email);
|
||||||
|
user.setUsername(name != null ? name : "user_" + providerId.substring(0, 8));
|
||||||
|
user.setRegistrationStatus(RegistrationStatus.ACTIVE);
|
||||||
|
user.setRole(UserRole.USER);
|
||||||
|
user.setCreated(LocalDateTime.now());
|
||||||
|
user.setUpdated(LocalDateTime.now());
|
||||||
|
user.setDeleted(false);
|
||||||
|
log.info("Creating new OAuth2 user: provider={}, email={}", provider, email);
|
||||||
|
return userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<String> generateTokensAndRedirect(User user) {
|
||||||
|
return refreshTokenRepository.findByUserId(user.getId())
|
||||||
|
.flatMap(existing -> {
|
||||||
|
existing.setCreated(LocalDateTime.now());
|
||||||
|
existing.setToken(generateUuid());
|
||||||
|
existing.setSessionId(generateUuid());
|
||||||
|
return refreshTokenRepository.save(existing);
|
||||||
|
})
|
||||||
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
|
RefreshToken newToken = new RefreshToken();
|
||||||
|
newToken.setUserId(user.getId());
|
||||||
|
newToken.setCreated(LocalDateTime.now());
|
||||||
|
newToken.setToken(generateUuid());
|
||||||
|
newToken.setSessionId(generateUuid());
|
||||||
|
return refreshTokenRepository.save(newToken);
|
||||||
|
}))
|
||||||
|
.map(refreshToken -> {
|
||||||
|
String accessToken = jwtTokenProvider.generateToken(user, refreshToken.getSessionId());
|
||||||
|
return redirectUri + "?token=" + accessToken + "&refreshToken=" + refreshToken.getToken();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractProviderId(OAuth2User oAuth2User, String registrationId) {
|
||||||
|
return switch (registrationId) {
|
||||||
|
case "google" -> oAuth2User.getAttribute("sub");
|
||||||
|
case "facebook" -> oAuth2User.getAttribute("id");
|
||||||
|
case "github" -> String.valueOf(oAuth2User.getAttributes().get("id"));
|
||||||
|
default -> oAuth2User.getName();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractEmail(OAuth2User oAuth2User, String registrationId) {
|
||||||
|
return oAuth2User.getAttribute("email");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractName(OAuth2User oAuth2User, String registrationId) {
|
||||||
|
return switch (registrationId) {
|
||||||
|
case "github" -> oAuth2User.getAttribute("login");
|
||||||
|
default -> oAuth2User.getAttribute("name");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateUuid() {
|
||||||
|
return UUID.randomUUID().toString().replace("-", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,23 @@ spring:
|
|||||||
application:
|
application:
|
||||||
name: gateway-service
|
name: gateway-service
|
||||||
|
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
client:
|
||||||
|
registration:
|
||||||
|
google:
|
||||||
|
client-id: ${GOOGLE_CLIENT_ID:}
|
||||||
|
client-secret: ${GOOGLE_CLIENT_SECRET:}
|
||||||
|
scope: email,profile
|
||||||
|
github:
|
||||||
|
client-id: ${GITHUB_CLIENT_ID:}
|
||||||
|
client-secret: ${GITHUB_CLIENT_SECRET:}
|
||||||
|
scope: user:email,read:user
|
||||||
|
facebook:
|
||||||
|
client-id: ${FACEBOOK_CLIENT_ID:}
|
||||||
|
client-secret: ${FACEBOOK_CLIENT_SECRET:}
|
||||||
|
scope: email,public_profile
|
||||||
|
|
||||||
# ---- R2DBC (reactive DB) ----
|
# ---- R2DBC (reactive DB) ----
|
||||||
r2dbc:
|
r2dbc:
|
||||||
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:appdb}
|
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:appdb}
|
||||||
@@ -75,12 +92,18 @@ jwt:
|
|||||||
secret: ${JWT_SECRET:}
|
secret: ${JWT_SECRET:}
|
||||||
expiration: ${JWT_EXPIRATION:103600000}
|
expiration: ${JWT_EXPIRATION:103600000}
|
||||||
|
|
||||||
|
# ---- OAuth2 redirect after success ----
|
||||||
|
oauth2:
|
||||||
|
redirect-uri: ${OAUTH2_REDIRECT_URI:https://balexvic.com/login}
|
||||||
|
|
||||||
# ---- Auth path config ----
|
# ---- Auth path config ----
|
||||||
auth:
|
auth:
|
||||||
public-paths:
|
public-paths:
|
||||||
- /api/auth/login
|
- /api/auth/login
|
||||||
- /api/auth/register
|
- /api/auth/register
|
||||||
- /api/auth/refresh/token
|
- /api/auth/refresh/token
|
||||||
|
- /oauth2/authorization/**
|
||||||
|
- /login/oauth2/code/**
|
||||||
- /actuator/**
|
- /actuator/**
|
||||||
- /api/*/v3/api-docs/**
|
- /api/*/v3/api-docs/**
|
||||||
- /api/*/swagger-ui/**
|
- /api/*/swagger-ui/**
|
||||||
@@ -102,7 +125,7 @@ management:
|
|||||||
health:
|
health:
|
||||||
show-details: always
|
show-details: always
|
||||||
gateway:
|
gateway:
|
||||||
enabled: true
|
access: unrestricted
|
||||||
|
|
||||||
# ---- Logging ----
|
# ---- Logging ----
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
Reference in New Issue
Block a user