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
2 changes: 2 additions & 0 deletions authorization-server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ out/

### VS Code ###
.vscode/

**/security/
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.synapse.authorization_server.config;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;

import lombok.RequiredArgsConstructor;

/**
* 인가 서버의 통신 방식은 CLIENT_CREDENTIALS 방식만 허용합니다.
* 마이크로 서비스끼리의 통신이기 때문에 다른 방식은 사용하지 않습니다. 또한 Basic 처리방식으로 body처리가 아닌 header 처리로 합니다.
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(AuthorizationServerProperties.class)
@RequiredArgsConstructor
public class AppConfig {
private final AuthorizationServerProperties authorizationServerProperties;

@Bean
public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
List<RegisteredClient> registeredClients = authorizationServerProperties.clients().stream()
.map(client -> RegisteredClient.withId(UUID.randomUUID().toString())
.clientId(client.clientId())
.clientSecret(passwordEncoder.encode(client.clientSecret()))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scopes(scopes -> scopes.addAll(client.scopes()))
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(client.accessTokenTtlInHours()))
.build())
.build())
.collect(Collectors.toList());

return new InMemoryRegisteredClientRepository(registeredClients);
}

@Bean
public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}

private RSAKey generateRsa() throws NoSuchAlgorithmException {
KeyPair keyPair = generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}

private KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
}

@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.issuer(authorizationServerProperties.issuerUri())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.synapse.authorization_server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

http.securityMatcher(endpointsMatcher)
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.with(authorizationServerConfigurer, withDefaults());

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.synapse.authorization_server.config;

import java.util.List;

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

@ConfigurationProperties(prefix = "authorization-server")
public record AuthorizationServerProperties(
String issuerUri,
List<RegisteredClient> clients
) {
public record RegisteredClient(
String clientId,
String clientSecret,
List<String> scopes,
long accessTokenTtlInHours
) {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.synapse.authorization_server.config;

import static org.springframework.security.config.Customizer.withDefaults;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
@Configuration
public class DefaultSecurityConfig {
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(withDefaults());

return http.build();
}
}
26 changes: 26 additions & 0 deletions authorization-server/src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
authorization-server:
issuer-uri: ${local-authorization.issuer-uri}
clients:
- client-id: ${local-authorization.clients.account-service-client.client-id}
client-secret: ${local-authorization.clients.account-service-client.client-secret}
scopes:
- ${local-authorization.clients.account-service-client.scopes}
access-token-ttl-in-hours: ${local-authorization.clients.account-service-client.access-token-ttl-in-hours}

- client-id: ${local-authorization.clients.payment-service-client.client-id}
client-secret: ${local-authorization.clients.payment-service-client.client-secret}
scopes:
- ${local-authorization.clients.payment-service-client.scopes}
access-token-ttl-in-hours: ${local-authorization.clients.payment-service-client.access-token-ttl-in-hours}

- client-id: ${local-authorization.clients.chat-service-client.client-id}
client-secret: ${local-authorization.clients.chat-service-client.client-secret}
scopes:
- ${local-authorization.clients.chat-service-client.scopes}
access-token-ttl-in-hours: ${local-authorization.clients.chat-service-client.access-token-ttl-in-hours}

- client-id: ${local-authorization.clients.alarm-service-client.client-id}
client-secret: ${local-authorization.clients.alarm-service-client.client-secret}
scopes:
- ${local-authorization.clients.alarm-service-client.scopes}
access-token-ttl-in-hours: ${local-authorization.clients.alarm-service-client.access-token-ttl-in-hours}
5 changes: 5 additions & 0 deletions authorization-server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ server:
spring:
application:
name: authorization-server
profiles:
active: local
config:
import:
- security/application-authorization.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.synapse.authorization_server.integrationtest;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.Base64;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

/**
* 실제 애플리케이션의 전체 컨텍스트를 로드하여 통합 테스트를 진행합니다.
* 이 테스트는 Authorization Server가 클라이언트의 요청에 대해 정상적으로 토큰을 발급하는지 검증합니다.
*/
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class AuthorizationServerIntegrationTest {
@Autowired
private MockMvc mockMvc;

@Test
@DisplayName("토큰 발급 성공: 올바른 클라이언트 정보로 요청 시, Access Token을 성공적으로 발급한다")
void issueToken_success_withValidClient() throws Exception {
// given
String clientId = "test-client";
String clientSecret = "test-secret";

// 요청 바디(form-data) 구성
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "client_credentials");
params.add("scope", "api.internal test.scope");

// when
ResultActions actions = mockMvc.perform(post("/oauth2/token")
.params(params)
.header(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes())));

// then
actions
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").exists())
.andExpect(jsonPath("$.token_type").value("Bearer"))
.andExpect(jsonPath("$.scope").value("api.internal test.scope"))
.andExpect(jsonPath("$.expires_in").isNumber());
}

@Test
@DisplayName("토큰 발급 실패: 잘못된 Client Secret으로 요청 시, 401 Unauthorized를 응답한다")
void issueToken_fail_withInvalidClientSecret() throws Exception {
// given
String clientId = "test-client";
String wrongClientSecret = "wrong-secret"; // 잘못된 시크릿

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "client_credentials");

// when
ResultActions actions = mockMvc.perform(post("/oauth2/token")
.params(params)
.header(HttpHeaders.AUTHORIZATION, "Basic "
+ Base64.getEncoder().encodeToString((clientId + ":" + wrongClientSecret).getBytes())));

// then
actions
.andDo(print())
.andExpect(status().isUnauthorized()) // 401 Unauthorized 상태 코드 확인
.andExpect(jsonPath("$.error").value("invalid_client"));
}

@Test
@DisplayName("토큰 발급 실패: 허용되지 않은 Scope을 요청 시, 400 Bad Request를 응답한다")
void issueToken_fail_withInvalidScope() throws Exception {
// given
String clientId = "test-client";
String clientSecret = "test-secret";

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "client_credentials");
params.add("scope", "api.unauthorized"); // 등록되지 않은 scope

// when
ResultActions actions = mockMvc.perform(post("/oauth2/token")
.params(params)
.header(HttpHeaders.AUTHORIZATION,
"Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes())));

// then
actions
.andDo(print())
.andExpect(status().isBadRequest()) // 400 Bad Request 상태 코드 확인
.andExpect(jsonPath("$.error").value("invalid_scope"));
}
}
10 changes: 10 additions & 0 deletions authorization-server/src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,13 @@ logging:
spring:
transaction:
interceptor: info

authorization-server:
issuer-uri: http://localhost:1010
clients:
- client-id: test-client
client-secret: test-secret
scopes:
- api.internal
- test.scope
access-token-ttl-in-hours: 1
Loading