[REST API 보안 적용]_06_인증 서버 설정

6 minute read

스프링 시큐리티 OAuth 2 설정: 인증 서버 설정

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-test</artifactId>
  <version>${spring-security.version}</version>
  <scope>test</scope>
</dependency>

토큰 발행 테스트

  • User
  • Client
  • POST /oauth/token
    • HTTP Basic 인증 헤더 (클라이언트 아이디 + 클라이언트 시크릿)
    • 요청 매개변수 (MultiValuMap<String, String>)
      • grant_type: password
      • username
      • password
    • 응답에 access_token 나오는지 확인

Grant Type: Password

  • Granty Type: 토큰 받아오는 방법
  • 서비스 오너가 만든 클라이언트에서 사용하는 Grant Type
  • https://developer.okta.com/blog/2018/06/29/what-is-the-oauth2-password-grant

AuthorizationServer 설정

  • @EnableAuthorizationServer
  • extends AuthorizationServerConfigurerAdapter
  • configure(AuthorizationServerSecurityConfigurer security)
    • PassswordEncode 설정
  • configure(ClientDetailsServiceConfigurer clients)
    • 클라이언트 설정
    • grantTypes
      • password
      • refresh_token
    • scopes
    • secret / name
    • accessTokenValiditySeconds
    • refreshTokenValiditySeconds
  • AuthorizationServerEndpointsConfigurer
    • tokenStore
    • authenticationManager
    • userDetailsService

다른 OAuth2 인증 처리 방식

클라이언트(Account)가 인증을 받으려면 써드파티 앱이기 때문에 페이스북, 구글등 유저의 진짜 인증정보를 가지고있는 서버에 리다이랙션이 일어난다.

토큰을 발급 받을 수 있는 또 다른 토큰을 먼저 받고, 다음에 토큰을 받고, 다시 리다이렉션이 일어난다. 홉이 굉장히 많음.

Grant Type

  • 스프링 OAuth가 인증을 제공하는 6가지 방법 중 PasswordRefreshToken 두 가지 방법을 지원하도록 구현한다.

  • 최초에 OAuth 토큰을 발급받을 때는 Password라는 GrantType으로 발급 받는다.
  • Password Grant Type은 다른 인증방식과 다른 특징이 있다.
    • 홉이 한번이다. 요청-응답이 한 쌍으로 한 번으로 토큰을 바로 받을 수 있다.
  • 인증을 제공하는 서비스들이 만든 앱(ex. facebook, google)들이 사용하는 방식
  • 언제 Password Grant Type을 쓸 수 있을까?
    • 유저의 유저네임과 패스워드를 직접 요구하므로 써드파티한테 이 방식을 허용하면 안되고 오로지 사용자 인증 정보를 보유하고 있는 서비스에서만 허용해야하는 인증 방법
    • 장점은 정보를 보내면 응답으로 바로 한번에 액세스 토큰을 받을 수 있음
    • 서비스 오너가 만든 클라이언트에서 사용하는 Grant Type

Grant Type: Password 요청시 필요한 정보

client_id, client_secretbasic Authentication 형태로 Header에 넣고, grant_type=password, username, passwordRequest에 파라메터로 넘겨줄 수 있다.

grant_type=password
&username=exampleuser
&password=1234luggage
&client_id=xxxxxxxxxx

테스트코드

  • 일종의 컨트롤러 테스트이기 때문에 BaseControllerTest를 상속

  • 기본으로 OAuth2 서버가 등록이 되면 /oauth/token을 요청할 수 있는 핸들러가 적용이 됨

HttpBasic를 사용하기 위해 spring-security-test 의존성 추가

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-test</artifactId>
  <version>${spring-security.version}</version>
  <scope>test</scope>
</dependency>

HttpBasic를 사용하기 위해선 clientIdclientSecret 가 필요

public class AuthServerConfigTest extends BaseControllerTest {

    @Autowired
    AccountService accountService;

    @Test
    @TestDescription("인증 토큰을 발급 받는 테스트")
    public void getAuthToken() throws Exception {
        // Given
        String username = "solar@email.com";
        String password = "solar";
        Account solar = Account.builder()
                .email(username)
                .password(password)
                .roles(Set.of(AccountRole.ADMIN, AccountRole.USER))
                .build();
        this.accountService.saveAccount(solar);

        String clientId = "myApp";
        String clientSecret = "pass";

      	// When & Then
        this.mockMvc.perform(post("/oauth/token")
                    .with(httpBasic(clientId, clientSecret)) // Basic OAuth Header
                    .param("username", username)
                    .param("password", password)
                    .param("grant_type", "password"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("access_token").exists());
    }

}

인증 서버 설정

이 설정이 완료되면 인증토큰을 발급받을 수 있어야 한다.

인증서버 설정에서는 @EnableAuthorizationServer 어노테이션을 통해 oauth 서버에 필요한 기본설정을 셋팅할 수 있다. (물론 @Configuration 어노테이션도 함께 붙여야 한다.)

클래스에 @EnableAuthorizationServer 어노테이션을 붙이는 것 만으로도 OAuth 관련 endpoints 가 생성된다.

(/oauth/token, /oauth/authorize 등. )

3가지 configure 구현

  1. security PasswordEncoder 설정

    User의 계정에 있는 패스워드 확인과 마찬가지로, secret의 패스워드도 인코딩해서 관리한다. (Best practice)

  2. clients 설정
  3. endpoints 설정
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    AccountService accountService;

    @Autowired
    TokenStore tokenStore;

  	// 이 passwordEncoder를 사용해서 클라이언트의 secret을 확인할 때 사용
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.passwordEncoder(passwordEncoder);  //client_secret를 확인할 때 사용
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory() // 학습을 위해 inMemory 용으로 생성함. DB로 관리하는 것이 이상적
                .withClient("myApp") // myApp에 대한 클라이언트를 하나 생성
                .authorizedGrantTypes("password", "refresh_token") // 지원하는 grant_Type
                .scopes("read", "write") // 앱에서 정의하는 값에 따라
                .secret(this.passwordEncoder.encode("pass")) // 앱의 secret
                .accessTokenValiditySeconds(10 * 60) // 엑세스 토큰의 유효시간(초)
                .refreshTokenValiditySeconds(6 * 10 * 60); // refresh_token의 유효시간(초)
    }

  	// AuthenticationManager, TokenStore, UserDetailsService를 설정할 수 있음
	  // 유저정보를 확인을 해야 토큰을 발급 받을 수 있음
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager) //우리 유저정보를 알고 있는 authenticationManager로 설정
                .userDetailsService(accountService) // UserDetailsService 설정
                .tokenStore(tokenStore); // TokenStore 설정
    }
}

Grant Type : refresh_token

OAuth 토큰을 발급 받을 때, refresh_token도 같이 발급을 해주는데, refresh_token을 가지고 새로운 AccessToken을 발급받는 Grant Type

발행한 토큰 확인

  • 테스트 코드를 돌려서 Response Body에 발급된 토큰 정보
{"access_token":"S4T1Mio23E3pDcbLBgOmw7qGo4k=",
 "token_type":"bearer",
 "refresh_token":"/YgNTXwyoEloKLsVLBzzODV3C94=",
 "expires_in":599,
 "scope":"read write"}

테스트코드 실패 : 에러 디버깅 스토리

테스트코드 실행 결과 200응답이 와야하는데 403 응답이 와서 테스트에 실패했다.

2021-01-22 18:15:27.851 DEBUG 9064 --- [           main] o.s.security.web.FilterChainProxy        : Securing POST /oauth/token
2021-01-22 18:15:27.857 DEBUG 9064 --- [           main] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2021-01-22 18:15:27.859 DEBUG 9064 --- [           main] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:8080/oauth/token
2021-01-22 18:15:27.860 DEBUG 9064 --- [           main] o.s.s.w.access.AccessDeniedHandlerImpl   : Responding with 403 status code
2021-01-22 18:15:27.860 DEBUG 9064 --- [           main] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2021-01-22 18:20:11.333 DEBUG 9213 --- [           main] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request

Invalid CSRF token found for http://localhost:8080/oauth/token 에러 메시지를 참고해서 CSRF token Spring Security를 검색

※ 참고한 글

회원가입을 진행하던 중 HTTP 403 에러가 발생하였다.

Spring Security 3.2 이후 버전에서는 적절한 CSRF 토큰을 포함시켜주지 않으면 에러를 발생하게끔 되어있다.

> CSRF ? https://namu.wiki/w/CSRF

⇒ 현재 스프링시큐리티 버전 : 5.4.2

  • csrf 비활성화 코드 추가를 추가
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	// ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf()
                .disable();
    }
}

⇒ 앞서 csrf 오류는 사라짐

다시 테스트한 결과 이번에는 404 응답이 돌아왔다.

2021-01-22 18:37:56.601 DEBUG 9791 --- [           main] o.s.security.web.FilterChainProxy        : Securing POST /login/oauth2/code/myApp
2021-01-22 18:37:56.603 DEBUG 9791 --- [           main] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2021-01-22 18:37:56.606 DEBUG 9791 --- [           main] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
2021-01-22 18:37:56.606 DEBUG 9791 --- [           main] o.s.security.web.FilterChainProxy        : Secured POST /login/oauth2/code/myApp
2021-01-22 18:37:56.617 DEBUG 9791 --- [           main] w.c.HttpSessionSecurityContextRepository : Did not store anonymous SecurityContext
2021-01-22 18:37:56.618 DEBUG 9791 --- [           main] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request

404 NOT FOUND 에러 발생 ⇒ oauth end point가 변경된 것인가??

end point 관련 문서를 찾아봄. https://www.baeldung.com/spring-security-5-oauth2-login

아닌 것 같다. 다른 글을 검색

인증서버 설정에서는 @EnableAuthorizationServer 어노테이션을 통해 oauth 서버에 필요한 기본설정을 셋팅할 수 있다. (물론 @Configuration 어노테이션도 함께 붙여야 한다.)

클래스에 @EnableAuthorizationServer 어노테이션을 붙이는 것 만으로도 OAuth 관련 endpoints 가 생성된다.

(/oauth/token, /oauth/authorize 등. 자세한 것은 여기 참고)

⇒ 어노테이션을 잘못 붙여서 안되었던 것!! @EnableGlobalAuthentication 잘못 붙인 애노테이션을 수정함

@Configuration
@EnableAuthorizationServer //이 애노테이션
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

Deprecated 클래스

Spring Security OAuth 프로젝트가 deprecation 됐기 때문입니다. Auth Server를 따로 커뮤니티 프로젝트로 분리되었고, 그밖에 다른 기능으 스프링 시큐리티 프로젝트 5.*에 포함되었습니다.

자세한 건 이 문서를 참고해 주세요. https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide

질문

“스프링 시큐리티 OAuth2 인증 서버 설정” 강의 중

Token 발급 TEST 코드를 작성하면서 “/oauth/token” URL을 POST 방식으로 요청하는 것을 볼 수 있었습니다.

저희가 직접 “/oauth/token” URL에 대해 매핑을 하지 않아도 처리가 가능했던 이유는

pom.xml에서 “spring-security-oauth2-autoconfigure” 의존성 설정을 함으로써 가능했던 것이고,

더 나아가 실제로 어떤 지점에서 “/oauth/token” URL이 매핑되어 처리가 되는지 디버깅을 통해 찾아본 결과,

org.springframewirk.security.oauth2.config.annotaion.web.configuration 패키지의

WebSecurityConfigurerAdapter를 상속받은 AuthorizationServerSecurityConfiguration 클래스의 configure(HttpSecurity http) 메소드가 재정의 됨으로써

“/oauth/token” URL이 매핑이 되었다는 것을 알 수 있었습니다.

제가 확인한 사항이 맞는지 궁금합니다.

TEST 코드, 인증 서버 설정 파일(AuthServerConfig.class) 에서 cliend_id, client_secret을 설정하는 부분에 대하여

추가적인 궁금 사항이 발생하여 여기에 글을 남깁니다.

예를 들어 Naver 아이디로 로그인 서비스를 사용할 때 절차를 보면,

네이버 개발자 센터 접속 > 네이버 아이디로 로그인 > 네이버 애플리케이션 생성 및 CLIENT_ID, CLIENT_SECRET 발급 > 이후 추가적인 API 사용 설정 등과 같은 절차로 네이버에서 제공하는 API를 사용할 수가 있었는데요.

이때 API를 사용하려고하는 유저(사용자)마다 각기 다른 CLIENT_ID, CLIENT_SECRET를 발급받는 것으로 알고 있습니다.

하지만 강의 내용에서 설정한 이값들은 애플리케이션 자체 고정적인 ID, SECRET 값을 의미하는지 아니면 사용자에게 발급되는 ID, SECRET인지 궁금합니다.

만약 후자인 경우 사용자마다 유효한 CLIENT_ID, CLIENT_SECRET인지 확인할 수 있는 방법은 어떤식으로 구현을 해야하는지 궁금합니다.

인증 토큰 처리는 언급하신대로 동작하는게 맞습니다. 스프링 시큐리티 OAuth2 프로젝트에 있는 컨트롤러가 처리해 줍니다. 클라이언트 ID와 시크릿은 사용자가 아니라 클라이언트 애플리케이션당 만들어집니다. 페이스북이나 깃헙 로그인을 만들거나 연동하는 애플리케이션을 만들 때 발급받는 애플리케이션 id와 시크릿과 동일하다고 생각하시면 됩니다.