google oauth2 적용

This commit is contained in:
Hyojin Ahn 2026-06-23 16:17:23 -04:00
parent 544a653f47
commit 54d76e6eb4
9 changed files with 455 additions and 49 deletions

View File

@ -43,6 +43,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// ---- OpenAPI / Swagger UI ----
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"

View File

@ -42,7 +42,7 @@ public class AuthenticationService {
private final AuthCookieService authCookieService;
private final UserDetailsService userDetailsService;
// ===================== 로그인 =====================
// ===================== 로그인 (비밀번호) =====================
public AuthenticationResponse authenticate(
AuthenticationRequest request,
@ -55,6 +55,16 @@ public class AuthenticationService {
throw new InvalidPasswordException("Invalid password");
}
// 발급 로직은 공용 메서드로 위임 (Google 로그인과 동일 경로)
return issueTokens(employee, response);
}
// ===================== 토큰 발급 (공용) =====================
// 비밀번호 로그인 / Google(OAuth2) 로그인 모두 메서드로 토큰을 발급한다.
// 호출자는 Employee 확보한 메서드를 부른다.
public AuthenticationResponse issueTokens(Employee employee, HttpServletResponse response) {
List<EmployeeRole> activeRoles =
employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
@ -65,11 +75,9 @@ public class AuthenticationService {
String refreshToken = jwtService.generateRefreshToken(employee, roles, permissions);
revokeAllEmployeeTokens(employee);
// refresh 저장
saveEmployeeToken(employee, accessToken, TokenType.ACCESS);
saveEmployeeToken(employee, refreshToken, TokenType.REFRESH);
// Cookie로 내려줌
authCookieService.addAuthCookie(response, accessToken);
authCookieService.addRefreshCookie(response, refreshToken);
@ -117,15 +125,14 @@ public class AuthenticationService {
List<String> roles = extractRoles(activeRoles);
List<String> permissions = extractPermissions(activeRoles);
String newAccessToken = jwtService.generateToken(employee, roles, permissions);
String newRefreshToken = jwtService.generateRefreshToken(employee, roles, permissions);
// 기존 토큰 전부 revoke
revokeAllEmployeeTokens(employee);
// 토큰 저장
// 토큰 저장
saveEmployeeToken(employee, newAccessToken, TokenType.ACCESS);
saveEmployeeToken(employee, newRefreshToken, TokenType.REFRESH);
@ -138,34 +145,34 @@ public class AuthenticationService {
}
// ===================== System Token =====================
// OPR / CRM 서버간 호출용 (Header 기반)
public SystemAuthenticationResponseDto authenticateSystem(SystemAuthenticationRequestDto request) {
if (request.getClientId() == null || request.getClientSecret() == null) {
throw new InvalidPasswordException("Missing client credentials");
}
var clientConfig = systemClientsProperties.getClients().get(request.getClientId());
if (clientConfig == null) {
throw new InvalidPasswordException("Invalid system client");
}
if (!clientConfig.getSecret().equals(request.getClientSecret())) {
throw new InvalidPasswordException("Invalid system secret");
}
String jwtToken = jwtService.generateSystemToken(
request.getClientId(),
clientConfig.getPermissions()
);
return SystemAuthenticationResponseDto.builder()
.accessToken(jwtToken)
.refreshToken(null)
.build();
}
// ===================== System Token =====================
// OPR / CRM 서버간 호출용 (Header 기반)
public SystemAuthenticationResponseDto authenticateSystem(SystemAuthenticationRequestDto request) {
if (request.getClientId() == null || request.getClientSecret() == null) {
throw new InvalidPasswordException("Missing client credentials");
}
var clientConfig = systemClientsProperties.getClients().get(request.getClientId());
if (clientConfig == null) {
throw new InvalidPasswordException("Invalid system client");
}
if (!clientConfig.getSecret().equals(request.getClientSecret())) {
throw new InvalidPasswordException("Invalid system secret");
}
String jwtToken = jwtService.generateSystemToken(
request.getClientId(),
clientConfig.getPermissions()
);
return SystemAuthenticationResponseDto.builder()
.accessToken(jwtToken)
.refreshToken(null)
.build();
}
// ===================== 내부 헬퍼 =====================
@ -199,6 +206,6 @@ public class AuthenticationService {
}
private void revokeAllEmployeeTokens(Employee employee) {
tokenRepository.revokeAllValidTokensByEmployee(employee.getEmpId());
tokenRepository.revokeAllValidTokensByEmployee(employee.getEmpId());
}
}
}

View File

@ -11,6 +11,8 @@ 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.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
@ -21,6 +23,12 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.boot.actuate.health.HealthEndpoint;
import com.goi.erp.oauth.HostedDomainAuthorizationRequestResolver;
import com.goi.erp.oauth.HttpCookieOAuth2AuthorizationRequestRepository;
import com.goi.erp.oauth.OAuth2LoginFailureHandler;
import com.goi.erp.oauth.OAuth2LoginSuccessHandler;
import com.goi.erp.oauth.OAuth2Properties;
//import static com.goi.erp.user.Permission.ADMIN_CREATE;
//import static com.goi.erp.user.Permission.ADMIN_DELETE;
//import static com.goi.erp.user.Permission.ADMIN_READ;
@ -45,8 +53,11 @@ import java.util.List;
@EnableMethodSecurity
public class SecurityConfiguration {
// NOTE: "/auth/**" 통째로 permitAll 하면 /auth/me 인증 없이 통과되어
// (access 토큰 만료 ) 200 {authenticated:false} 돌려주고, 프론트의
// 401-기반 refresh 트리거되지 않는다. 공개가 필요한 auth 엔드포인트만
// 아래 authorizeHttpRequests 에서 개별적으로 permitAll 한다.
private static final String[] WHITE_LIST_URL = {
"/auth/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
@ -60,17 +71,24 @@ public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
// ---- OAuth2 (Google Workspace) ----
private final HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;
private final ClientRegistrationRepository clientRegistrationRepository;
private final OAuth2Properties oAuth2Properties;
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of(
"https://portal.greenoilinc.com",
"http://localhost:5173",
"http://192.168.*.*:5173",
"http://10.*.*.*:5173"
));
"https://portal.greenoilinc.com",
"http://localhost:5173",
"http://192.168.*.*:5173",
"http://10.*.*.*:5173"
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-CSRF-TOKEN"));
config.setAllowCredentials(true);
@ -83,15 +101,24 @@ public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// hd 파라미터를 주입하는 커스텀 인가요청 리졸버
OAuth2AuthorizationRequestResolver authorizationRequestResolver =
new HostedDomainAuthorizationRequestResolver(
clientRegistrationRepository,
oAuth2Properties.getAllowedDomain());
http
.cors(Customizer.withDefaults())
.cors(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(req ->
req.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
.requestMatchers("/auth/login", "/auth/token/refresh", "/auth/login/system").permitAll()
.requestMatchers("/auth/logout").authenticated()
.requestMatchers(WHITE_LIST_URL)
.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
.requestMatchers("/auth/login", "/auth/token/refresh", "/auth/login/system").permitAll()
// OAuth2 로그인 엔드포인트 (인가 시작 + Google 콜백)
.requestMatchers("/oauth2/**", "/login/oauth2/**").permitAll()
.requestMatchers("/auth/logout").authenticated()
.requestMatchers(WHITE_LIST_URL)
.permitAll()
// .requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name(), MANAGER.name())
// .requestMatchers(GET, "/api/v1/management/**").hasAnyAuthority(ADMIN_READ.name(), MANAGER_READ.name())
@ -101,6 +128,26 @@ public class SecurityConfiguration {
.anyRequest()
.authenticated()
)
// OAuth2 로그인 (Google Workspace). SPA 명시적으로 /oauth2/authorization/google
// 보낼 때만 시작된다. 보호 리소스 접근 자동 로그인 리다이렉트는 아래 401 entryPoint
// 우선하므로 일어나지 않는다(= API/SPA 401 받고 refresh 로직이 동작).
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authz -> authz
.baseUri("/oauth2/authorization")
.authorizationRequestRepository(cookieAuthorizationRequestRepository)
.authorizationRequestResolver(authorizationRequestResolver)
)
.redirectionEndpoint(redir -> redir
.baseUri("/login/oauth2/code/*")
)
.successHandler(oAuth2LoginSuccessHandler)
.failureHandler(oAuth2LoginFailureHandler)
)
// 미인증 요청은 403이 아니라 401 응답한다.
// (Spring Security 기본값은 보통 403 이라 프론트의 401-기반 refresh 걸린다)
.exceptionHandling(ex -> ex.authenticationEntryPoint(
(request, resp, authException) ->
resp.setStatus(jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED)))
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)

View File

@ -0,0 +1,53 @@
package com.goi.erp.oauth;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import java.util.HashMap;
import java.util.Map;
/**
* Google 인가 요청에 hd(hosted domain) 파라미터를 붙인다.
*
* - 회사 도메인(greenoilinc.com) 계정으로 로그인 흐름을 유도하고 계정 선택 화면을 줄여준다.
* - , hd 파라미터는 "힌트" 보안 경계가 아니다. 실제 도메인 강제는
* 성공 핸들러에서 ID 토큰의 hd 클레임을 검증하는 것으로 한다(Step 3).
*/
public class HostedDomainAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private final DefaultOAuth2AuthorizationRequestResolver defaultResolver;
private final String hostedDomain;
public HostedDomainAuthorizationRequestResolver(ClientRegistrationRepository repo, String hostedDomain) {
// 기본 인가 엔드포인트 base-uri 동일하게 맞춤
this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, "/oauth2/authorization");
this.hostedDomain = hostedDomain;
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
return customize(defaultResolver.resolve(request));
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
return customize(defaultResolver.resolve(request, clientRegistrationId));
}
private OAuth2AuthorizationRequest customize(OAuth2AuthorizationRequest req) {
if (req == null) {
return null;
}
if (hostedDomain == null || hostedDomain.isBlank()) {
return req;
}
Map<String, Object> extra = new HashMap<>(req.getAdditionalParameters());
extra.put("hd", hostedDomain);
return OAuth2AuthorizationRequest.from(req)
.additionalParameters(extra)
.build();
}
}

View File

@ -0,0 +1,146 @@
package com.goi.erp.oauth;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;
/**
* 세션 대신 쿠키에 OAuth2AuthorizationRequest 저장한다.
*
* 앱이 SessionCreationPolicy.STATELESS 기본 세션 기반 저장소를 쓰기 때문에 필요.
* - 인가 요청 Google 리다이렉트 쿠키에 저장
* - Google 콜백 복귀 쿠키에서 복원 삭제
*
* 쿠키는 httpOnly + 짧은 수명(180s) + SameSite=Lax.
* (Google 에서 콜백으로 돌아오는 top-level GET 내비게이션에 쿠키가 실리려면 Lax 여야 . Strict )
* 역직렬화는 ObjectInputFilter 허용 클래스를 제한해 gadget 공격을 차단한다.
*/
@Component
public class HttpCookieOAuth2AuthorizationRequestRepository
implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
public static final String OAUTH2_AUTH_REQUEST_COOKIE = "OAUTH2_AUTH_REQUEST";
private static final int COOKIE_EXPIRE_SECONDS = 180;
@Value("${auth.cookie.secure:true}")
private boolean secure;
@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
return getCookie(request, OAUTH2_AUTH_REQUEST_COOKIE)
.map(c -> deserialize(c.getValue()))
.orElse(null);
}
@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest,
HttpServletRequest request,
HttpServletResponse response) {
if (authorizationRequest == null) {
deleteCookie(response, OAUTH2_AUTH_REQUEST_COOKIE);
return;
}
addCookie(response, OAUTH2_AUTH_REQUEST_COOKIE,
serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS);
}
@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,
HttpServletResponse response) {
OAuth2AuthorizationRequest authRequest = loadAuthorizationRequest(request);
if (authRequest != null) {
deleteCookie(response, OAUTH2_AUTH_REQUEST_COOKIE);
}
return authRequest;
}
// ===================== 직렬화 =====================
private static String serialize(OAuth2AuthorizationRequest obj) {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(obj);
oos.flush();
return Base64.getUrlEncoder().encodeToString(bos.toByteArray());
} catch (Exception e) {
throw new IllegalStateException("Failed to serialize OAuth2AuthorizationRequest", e);
}
}
private static OAuth2AuthorizationRequest deserialize(String value) {
try {
byte[] data = Base64.getUrlDecoder().decode(value);
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
ois.setObjectInputFilter(SAFE_FILTER);
return (OAuth2AuthorizationRequest) ois.readObject();
}
} catch (Exception e) {
// 위변조/손상 쿠키 인가요청 없음으로 처리
return null;
}
}
/** OAuth2AuthorizationRequest 와 그 필드(표준 java + spring-security-oauth2)만 허용 */
private static final ObjectInputFilter SAFE_FILTER = info -> {
Class<?> clazz = info.serialClass();
if (clazz == null) {
return ObjectInputFilter.Status.UNDECIDED;
}
String name = clazz.getName();
if (name.startsWith("org.springframework.security.oauth2.")
|| name.startsWith("java.")
|| name.startsWith("[Ljava.")
|| name.startsWith("[B")) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
};
// ===================== 쿠키 헬퍼 =====================
private void addCookie(HttpServletResponse response, String name, String value, int maxAgeSeconds) {
ResponseCookie cookie = ResponseCookie.from(name, value)
.httpOnly(true)
.secure(secure)
.path("/")
.sameSite("Lax")
.maxAge(maxAgeSeconds)
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
}
private void deleteCookie(HttpServletResponse response, String name) {
ResponseCookie cookie = ResponseCookie.from(name, "")
.httpOnly(true)
.secure(secure)
.path("/")
.sameSite("Lax")
.maxAge(0)
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
}
private Optional<Cookie> getCookie(HttpServletRequest request, String name) {
if (request.getCookies() == null) {
return Optional.empty();
}
return Arrays.stream(request.getCookies())
.filter(c -> name.equals(c.getName()))
.findFirst();
}
}

View File

@ -0,0 +1,28 @@
package com.goi.erp.oauth;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* OAuth2 로그인 실패 401 + 에러 메시지(JSON).
* 기본 핸들러는 /login?error 리다이렉트하는데 그런 화면이 없으므로 명시적으로 처리.
*/
@Component
public class OAuth2LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
String msg = exception.getMessage() == null ? "" : exception.getMessage().replace("\"", "'");
response.getWriter().write(
"{\"error\":\"oauth2_login_failed\",\"message\":\"" + msg + "\"}");
}
}

View File

@ -0,0 +1,80 @@
package com.goi.erp.oauth;
import com.goi.erp.auth.AuthenticationService;
import com.goi.erp.employee.Employee;
import com.goi.erp.employee.EmployeeRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* Google(OAuth2) 로그인 성공 후처리.
*
* 1) hd 클레임으로 도메인 검증 (greenoilinc.com)
* 2) 이메일(emp_login_id) Employee 조회
* 3) 활성 상태 확인
* 4) 기존 발급 경로(AuthenticationService.issueTokens) 재사용 JWT + 쿠키
* 5) SPA(success-redirect) 리다이렉트
*
* 실패 케이스는 SPA ?login_error=코드 붙여 리다이렉트한다.
*/
@Component
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {
private final OAuth2Properties oAuth2Properties;
private final EmployeeRepository employeeRepository;
private final AuthenticationService authenticationService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
OidcUser oidcUser = (OidcUser) authentication.getPrincipal();
String email = oidcUser.getEmail();
String hd = (String) oidcUser.getClaims().get("hd");
// 1) 도메인 검증
String allowed = oAuth2Properties.getAllowedDomain();
if (allowed != null && !allowed.isBlank() && !allowed.equalsIgnoreCase(hd)) {
redirectError(response, "domain_not_allowed");
return;
}
// 2) 이메일로 Employee 조회 (emp_login_id == Google Workspace 이메일)
Employee employee = employeeRepository.findByEmpLoginId(email).orElse(null);
if (employee == null) {
redirectError(response, "not_registered");
return;
}
// 3) 활성 상태 확인 ('A' = Active)
if (!"A".equals(employee.getEmpStatus())) {
redirectError(response, "inactive");
return;
}
// 4) 기존 발급 경로 재사용 (비밀번호 로그인과 동일): JWT 발급 + AUTH/REFRESH 쿠키 세팅
authenticationService.issueTokens(employee, response);
// 5) SPA 리다이렉트 (쿠키는 302 응답에 함께 실려 나간다)
response.sendRedirect(oAuth2Properties.getSuccessRedirect());
}
private void redirectError(HttpServletResponse response, String code) throws IOException {
String base = oAuth2Properties.getSuccessRedirect();
String sep = base.contains("?") ? "&" : "?";
response.sendRedirect(base + sep + "login_error="
+ URLEncoder.encode(code, StandardCharsets.UTF_8));
}
}

View File

@ -0,0 +1,27 @@
package com.goi.erp.oauth;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* application.yml app.oauth2.* 바인딩.
*
* app:
* oauth2:
* allowed-domain: greenoilinc.com
* success-redirect: https://portal.greenoilinc.com
*/
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "app.oauth2")
public class OAuth2Properties {
/** 로그인 허용 Google Workspace 도메인 (hd 클레임 검증 + hd 파라미터) */
private String allowedDomain;
/** 로그인 성공 후 SPA 로 돌려보낼 주소 (Step 3에서 사용) */
private String successRedirect;
}

View File

@ -13,7 +13,19 @@ spring:
format_sql: true
database: postgresql
database-platform: org.hibernate.dialect.PostgreSQLDialect
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope:
- openid
- email
- profile
redirect-uri: "{baseUrl}/login/oauth2/code/google"
# provider 'google'은 Spring Security 내장이라 엔드포인트 설정 불필요
application:
security:
jwt:
@ -27,6 +39,11 @@ application:
secret: ${OPR_SYSTEM_CLIENT_SECRET}
permissions:
- "H:R:A"
app:
oauth2:
allowed-domain: greenoilinc.com
# 로그인 성공 후 SPA로 돌려보낼 주소
success-redirect: https://portal.greenoilinc.com
auth:
cookie:
secure: true # prod: true / local: false