oauth-authorization-server
Published: 2023-11-29 00:10:20

### 1. api-client , resource server

https://www.baeldung.com/spring-security-oauth-auth-server

baeldung gihub

https://github.com/Baeldung/spring-security-oauth/tree/master/oauth-authorization-server

### 2. api-client server 소스분석

### 2.1 yml 설정

```yaml
server:
  port: 8080

spring:
  security:
    oauth2:
      client:
        registration:
          articles-client-oidc: #클라이언트 인증
            provider: spring
            client-id: articles-client
            client-secret: secret
            authorization-grant-type: authorization_code
            redirect-uri: "http://127.0.0.1:8080/login/oauth2/code/{registrationId}"
            scope: openid
            client-name: articles-client-oidc
          articles-client-authorization-code: #클라이언트 코드로 액세스 토큰 요청시 사용
            provider: spring
            client-id: articles-client
            client-secret: secret
            authorization-grant-type: authorization_code
            redirect-uri: "http://127.0.0.1:8080/authorized"
            scope: articles.read
            client-name: articles-client-authorization-code
        provider:
          spring:
            issuer-uri: http://auth-server:9000
```

### 2.1.1 **요청 응답 흐름 (시퀀스)**

1. **사용자 로그인 요청**: 사용자가 애플리케이션에 로그인을 시도합니다.
2. **리다이렉션**: 클라이언트 애플리케이션은 사용자를 인증 서버의 로그인 페이지로 리다이렉션합니다. 이 때 **`client-id`**, **`redirect-uri`**, **`scope`** 등의 정보가 포함됩니다. 이 정보는 **`registration`** 섹션에서 정의됩니다.
3. **사용자 인증**: 사용자는 인증 서버에서 로그인을 진행합니다. 이 과정에서 **`issuer-uri`** (인증 서버 주소)가 사용됩니다.
4. **인증 코드 발급**: 로그인이 성공하면 인증 서버는 **`redirect-uri`**로 사용자를 다시 리다이렉션하며, 이 때 인증 코드를 함께 전달합니다.
5. **인증 코드를 토큰으로 교환**: 클라이언트 애플리케이션은 받은 인증 코드를 사용하여 인증 서버에 토큰을 요청합니다. 여기서 **`client-id`** 및 **`client-secret`**이 사용됩니다.
6. **토큰 응답**: 인증 서버는 클라이언트에게 액세스 토큰(및 필요시 리프레시 토큰)을 발급합니다.
7. **리소스 서버 접근**: 클라이언트는 받은 토큰을 사용하여 보호된 리소스에 액세스합니다. 예를 들어, **`scope: articles.read`**를 사용하여 특정 API에 접근할 수 있습니다.

### 2.1.2 **설정 파일 내의 역할**

- **`client-id`** 및 **`client-secret`**: 클라이언트의 신원을 인증 서버에 증명하는데 사용됩니다.
- **`redirect-uri`**: 사용자가 인증 후 리다이렉션될 URI입니다. 인증 코드를 받는 데 사용됩니다.
- **`scope`**: 클라이언트가 요청할 수 있는 권한의 범위를 정의합니다.
- **`issuer-uri`**: 인증 서버의 주소입니다. 사용자 인증 및 토큰 발급에 사용됩니다.

### 2.1.3 상세설명

1. **`articles-client-oidc`: 최초 클라이언트 인증**
    - 이 설정은 사용자가 최초로 로그인할 때 사용됩니다.
    - 사용자가 애플리케이션에 로그인을 요청하면, 애플리케이션은 사용자를 OAuth 2.0 인증 서버의 로그인 페이지로 리다이렉션합니다.
    - **`articles-client-oidc`** 설정에 포함된 **`client-id`**, **`client-secret`**, **`redirect-uri`**, **`scope`** 등의 정보가 이 과정에서 사용됩니다.
    - 사용자가 인증 서버에서 성공적으로 인증을 완료하면, 인증 서버는 **`redirect-uri`**로 사용자를 리디렉션하고 인증 코드를 제공합니다.
2. **`articles-client-authorization-code`: 리소스 서버에 대한 요청**
    - 인증 코드를 받은 후, 애플리케이션은 이 코드를 사용하여 인증 서버에 액세스 토큰을 요청합니다. 이 때 **`articles-client-authorization-code`** 설정이 사용됩니다.
    - **`client-id`**, **`client-secret`**는 이 과정에서 토큰 교환을 위해 사용되며, **`redirect-uri`**는 이전 단계에서 인증 코드를 받기 위해 사용된 주소입니다.
    - 애플리케이션이 액세스 토큰을 받으면, 이 토큰을 사용하여 보호된 리소스에 접근할 수 있습니다. 이 경우 **`scope: articles.read`**에 따라 특정 리소스에 대한 액세스 권한이 부여됩니다.

요약하자면, **`articles-client-oidc`** 설정은 사용자 인증을 위한 초기 로그인 과정에 사용되며, **`articles-client-authorization-code`** 설정은 인증된 사용자가 보호된 리소스에 접근하기 위해 필요한 액세스 토큰을 얻는 과정에 사용

### 2.2 소스

```java
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
          .authorizeRequests(authorizeRequests ->
            authorizeRequests.anyRequest().authenticated()
          )
          .oauth2Login(oauth2Login ->
            oauth2Login.loginPage("/oauth2/authorization/articles-client-oidc"))
          .oauth2Client(withDefaults());
        return http.build();
    }
}
```

oauth2Login.loginPage("/oauth2/authorization/articles-client-oidc"))

해당 페이지는 클라이언트 서버에서 제공되는 페이지이며, security5에 제공되는 페이지로 사용된거.

```java
@RestController
public class ArticlesController {

    private WebClient webClient;

    @GetMapping(value = "/articles")
    public String[] getArticles(
      @RegisteredOAuth2AuthorizedClient("articles-client-authorization-code") OAuth2AuthorizedClient authorizedClient
    ) {
        return this.webClient
          .get()
          .uri("http://127.0.0.1:8090/articles")
          .attributes(oauth2AuthorizedClient(authorizedClient))
          .retrieve()
          .bodyToMono(String[].class)
          .block();
    }
}
```

*OAuth2AuthorizedClient* 클래스 형식의 요청에서 OAuth 인증 토큰을 가져옵니다 . 

이는 적절한 식별과 함께 *@RegisterdOAuth2AuthorizedClient* 주석을 사용하여 Spring에 의해 자동으로 바인딩됩니다 . *우리의 경우 이전에 .yml* 파일 에서 구성한 *article-client-authorizaiton-code* 에서 가져왔습니다

### 3. 테스트 호출

브라우저에서 호출 *http://127.0.0.1:8080/articles*

인증 url 로 리다이렉션 *http://auth-server:9000/login*

### 4. 리소스 서버

보안구성 - 인증서버 설정 

인증서버에 설정한 *ProviderSettings 값을 issuer-uri 로 설정*

```yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://auth-server:9000
```

웹보안 구성 설정 : 리소스에 대한 권한 설정( 읽기, 모든 요청에 대한 승인요청)

```java
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.mvcMatcher("/articles/**")
          .authorizeRequests() //1.요청에 대한 인증을 시작합니다. 이 단계에서는 이후에 정의될 인증 규칙들이 적용됩니다
          .mvcMatchers("/articles/**")//2.특정 경로(/articles/**)에 대한 추가적인 규칙을 정의. 
																			// 해당 경로에 대한 보안 규칙을 세분화
          .access("hasAuthority('SCOPE_articles.read')")//3./articles/** 경로에 대한 접근을 제어
					// 'SCOPE_articles.read' 권한을 가지고 있어야만 해당 경로에 접근 가능
					// 이 권한은 일반적으로 OAuth 2.0 토큰에 정의된 스코프에서 파생됩니다.
          .and()
          .oauth2ResourceServer() //3. OAuth 2.0 리소스 서버를 활성화하고 JWT (JSON Web Token)를 사용하여 인증을 수행
          .jwt(); //  들어오는 요청이 올바른 JWT를 포함하고 있는지 검증하여 해당 요청이 유효한지 확인
        return http.build();
    }
}
```

### 5. **인증서버**

```java
@Configuration
@Import(OAuth2AuthorizationServerConfiguration.class)
public class AuthorizationServerConfig {
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
          .clientId("articles-client")
          .clientSecret("{noop}secret")
          .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
          .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
          .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
          .redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")
          .redirectUri("http://127.0.0.1:8080/authorized")
          .scope(OidcScopes.OPENID)
          .scope("articles.read")
          .build();
        return new InMemoryRegisteredClientRepository(registeredClient);
    }
}
```

- **`RegisteredClient.withId(UUID.randomUUID().toString())`**: 클라이언트 ID를 생성합니다. 여기서는 고유한 UUID를 사용합니다.
- **`.clientId("articles-client")`**: OAuth 클라이언트의 식별자로 "articles-client"를 지정합니다. Spring은 이를 사용하여 리소스에 액세스하려는 클라이언트를 식별합니다.
- **`.clientSecret("{noop}secret")`**: 클라이언트의 비밀번호를 설정합니다. **`{noop}`**은 패스워드 인코더가 사용되지 않음을 의미합니다.
- **`.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)`**: 클라이언트 인증 방식을 **`CLIENT_SECRET_BASIC`**으로 설정합니다. 이는 클라이언트 ID와 비밀번호를 사용하여 인증을 수행하는 방식입니다.
- **`.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)`** (인증 부여 유형): 인증 코드 부여 유형을 지원합니다. 이는 OAuth 2.0의 표준 플로우 중 하나입니다.
- **`.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)`** (인증 부여 유형): 리프레시 토큰 부여 유형을 지원합니다. 이를 통해 클라이언트는 액세스 토큰을 갱신할 수 있습니다.
- **`.redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")`**: OAuth 인증 후 사용자를 리디렉션할 URI를 설정합니다.
- **`.redirectUri("http://127.0.0.1:8080/authorized")`**: 추가적인 리디렉션 URI를 설정합니다.
- **`.scope(OidcScopes.OPENID)`**: OpenID Connect 스코프를 사용합니다.
- **`.scope("articles.read")`**: 사용자 정의 스코프인 "articles.read"를 설정합니다. 이 스코프는 클라이언트가 특정 자원에 접근할 수 있도록 허용합니다.
- **`.build()`**: **`RegisteredClient`** 구성을 완성합니다.

### 5.1 상세

**`.redirectUri`** 설정에 대해 설명드리겠습니다. 이 설정은 OAuth 2.0 인증 프로세스에서 매우 중요한 부분입니다.

5.1.1. **인증 코드 흐름 (Authorization Code Flow)**:
    - **`.redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")`**: 이 리디렉션 URI는 일반적으로 인증 코드 흐름에 사용됩니다. 사용자가 OAuth 인증을 시작하면, 사용자는 이 URI로 리디렉션됩니다. 여기서 사용자는 로그인하고 동의를 제공한 후, 인증 서버는 이 URI로 인증 코드를 리디렉션합니다. 이 인증 코드는 나중에 액세스 토큰을 얻는 데 사용됩니다.
5.1.2. **액세스 토큰 요청**:
    - **`.redirectUri("http://127.0.0.1:8080/authorized")`**: 이 URI는 액세스 토큰을 받기 위한 추가적인 리디렉션 포인트로 사용될 수 있습니다. 사용자가 인증 코드를 사용하여 액세스 토큰을 요청할 때, 인증 서버는 이 URI로 사용자를 리디렉션할 수 있습니다.

**리디렉션 URI의 순서는 일반적으로 중요하지 않습니다. 중요한 것은 클라이언트 애플리케이션이 이 URI들 중 하나를 사용하여 인증 서버에 리디렉션을 요청하는 것입니다. 클라이언트 애플리케이션이 인증 요청 시에 사용하는 `redirect_uri` 매개변수의 값은 등록된 리디렉션 URI 중 하나와 정확히 일치해야 합니다.**

요약하자면, **`.redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")`**는 사용자가 로그인하고 인증 서버가 인증 코드를 반환하는 데 사용되며, **`.redirectUri("http://127.0.0.1:8080/authorized")`**는 추가적인 리디렉션 옵션으로, 특정 상황에서 사용될 수 있습니다. 이 두 URI는 인증 프로세스의 다른 단계에서 사용되며, 그 순서는 특별한 의미를 가지지 않습니다.

### 5.2 구성

기본 OAuth 보안을 적용하고 기본 양식 로그인 페이지를 생성하도록 Bean을 구성

```java
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    return http.formLogin(Customizer.withDefaults()).build();
}
```

각 인증 서버에는 보안 도메인 간의 적절한 경계를 유지하기 위해 토큰에 대한 서명 키가 필요합니다. 2048바이트 RSA 키를 생성해 보겠습니다.

```java
@Bean
public JWKSource<SecurityContext> jwkSource() {
    RSAKey rsaKey = generateRsa();
    JWKSet jwkSet = new JWKSet(rsaKey);
    return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

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

private static KeyPair generateRsaKey() {
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    keyPairGenerator.initialize(2048);
    return keyPairGenerator.generateKeyPair();
}
```

서명 키를 제외하고 각 인증 서버에는 고유한 발급자 URL도 있어야 합니다. *ProviderSettings* 빈을 생성하여 포트 *9000 에서 http://auth-server* 에 대한 localhost 별칭으로 설정하겠습니다 .

```java
@Bean
public ProviderSettings providerSettings() {
    return ProviderSettings.builder()
      .issuer("http://auth-server:9000")
      .build();
}
```

또한 */etc/hosts 파일에 " 127.0.0.1 auth-server* " 항목을 추가하겠습니다 . 이를 통해 로컬 컴퓨터에서 클라이언트와 인증 서버를 실행할 수 있으며 둘 사이의 세션 쿠키 덮어쓰기 문제를 피할 수 있습니다.

*그런 다음 @EnableWebSecurity* 주석이 달린 구성 클래스 를 사용하여 Spring 웹 보안 모듈을 활성화합니다

```java
@EnableWebSecurity
public class DefaultSecurityConfig {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(authorizeRequests ->
          authorizeRequests.anyRequest().authenticated()
        )
          .formLogin(withDefaults());
        return http.build();
    }

    // ...
}
```

- 모든 요청에 대해 인증을 요구하기 위해 *AuthorizeRequests.anyRequest().authenticated()를* 호출
- *formLogin(defaults())* 메서드 를 호출하여 양식 기반 인증을 제공하고

테스트에 사용할 예제 사용자 집합을 정의

```java
@Bean
UserDetailsService users() {
    UserDetails user = User.withDefaultPasswordEncoder()
      .username("admin")
      .password("password")
      .build();
    return new InMemoryUserDetailsManager(user);
}
```