본문 바로가기
프로그래밍/기타

[OAuth2] spring-authorization-server를 이용하여 인증 서버(auth server) 만들기

by 사바라다 2023. 3. 18.

안녕하세요. 이전시간까지 우리는 jwt란 무엇인지, OAuth2 스펙과 기본적인 flow에 대해서 각각 알아보았습니다.

이번 포스팅과 다음 포스팅에서는 OAuth2의 인증 서버와 리소스 서버를 실제로 구현해보도록 하겠습니다.

이번 실습은 이전시간에 배운 authorize_code grant type을 토대로 진행합니다.

오늘 실습에 대한 코드는 github Repository에서 확인하실 수 있습니다.

spring-authorization-server의 등장

기존에는 Spring Security에서 인증 서버를 구성할 수 있도록 지원하고 있었습니다. 하지만 Spring Security 팀은 2019년 11월 발표에서 더이상 Spring Security는 OAuth의 인증서버(authorization-server)를 지원하지 않는다고 발표합니다. 그 이유는 인증 서버는 jwt, SAML IdP, Cas 또는 LDAP 등의 다양한 library의 집합으로 이루어지는데 이러한 것들은 비즈와 연관없는 라이브러리들의 집합이라고 판단하였기 때문입니다.

Spring Security’s Authorization Server support was never a good fit. An Authorization Server requires a library to build a product. Spring Security, being a framework, is not in the business of building libraries or products. For example, we don’t have a JWT library, but instead we make Nimbus easy to use. And we don’t maintain our own SAML IdP, CAS or LDAP products.

원문 : https://spring.io/blog/2019/11/14/spring-security-oauth-2-0-roadmap-update

하지만 이러한 발표 이후 해당 포스팅의 덧글 및 gitter 등의 커뮤니티에서는 이러한 결정에 대해서 수많은 반발과 반대가 있었습니다. 그리고 이러한 커뮤니티의 반발을 받아들여 spring security에서는 제외했지만 새롭게 인증서버 용도의 spring-authorization-server를 만들었고 이를 2020년 4월에 아래처럼 런칭하게 됩니다.

https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server

그렇다면 이제 실제로 해당 라이브러리를 이용하여 인증서버를 만들어보도록 하겠습니다.

환경 설정

오늘 만든 코드의 환경은 아래와 같습니다. spring-authorization-server의 경우 1.0.0 버전부터는 Spring Boot 3.0 이상 지원되는 항목으므로 2.7.8 버전에 호환되는 0.4.1 버전을 선택하였습니다.

  • kotlin 1.6.21
  • java 17
  • spring-boot 2.7.8
  • spring-authorization-server 0.4.1

spring-authorization-server를 통해서 인증서버를 구성하는것은 Configuration 정의를 잘 하는것이 중요합니다.

아래 실습에서는 해당 Configuration을 설정하는 방법에 대해서 알아보도록 하겠습니다.

OAuth 2.0 인증을 위한 authorizationServerSecurityFilterChain 등록

가장먼저 설정해볼 것은 OAuth 인증이 일어나도록 Filter를 구성하는 것입니다. SecurityFilterChain Bean을 설정하는 아래 설정은 RFC SPEC에 나와있는 path를 사용해서 OAuth2 인증가능하게 만들어주는 Bean 입니다.

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
@Throws(java.lang.Exception::class)
fun authorizationServerSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)

    http.getConfigurer(OAuth2AuthorizationServerConfigurer::class.java)

    // @formatter:off
    http
        .exceptionHandling { exceptions: ExceptionHandlingConfigurer<HttpSecurity> ->
            exceptions.authenticationEntryPoint(
                LoginUrlAuthenticationEntryPoint("/login")
            )
        }
    // @formatter:on
    return http.build()
}

위 처럼 기본 설정을 하게 되면 여러 필터와 Provider가 자동으로 제공됩니다. 그 중 주요하게 봐야할 부분은 아래와 같습니다.

  1. authorizationRequestConverter
    • HttpServletRequest에서 OAuth2 인증 요청을 추출하는 역할을 합니다.
  2. authenticationProvider
    • 실제로 인증을 제공하는 Provider를 설정합니다.
  3. authorizationResponseHandler
    • 인증이 완료되면 응답을 만들어내는 역할을 합니다.

위 설정은 아래의 설정을 통해서 커스터마이징 가능합니다.

http.getConfigurer(OAuth2AuthorizationServerConfigurer::class.java)
    .authorizationEndpoint {
        it.authorizationRequestConverter({customizing_class})
        it.authenticationProvider({customizing_class})
        it.authorizationResponseHandler({customizing_class})
    }

authorization_code grant의 기본적인 end-points path는 아래와 같습니다. 아래의 path는 draft-ietf-oauth-discovery-10 를 참고로하여 만들어진 것으로 알 고 있습니다.

  • GET /.well-known/oauth-authorization-server
    • 인증 서버에 설정된 end-point와 grant-type을 조회하는 API
  • GET /oauth2/authorize
    • 리소스 소유자 인증하는 API
  • POST /oauth2/token
    • code를 token으로 교환하는 API

인증 서버에 요청하기 위한 인증 defaultSecurityFilterChain 등록

인증 되지 않은 요청에 대해서 formLogin을 할 수 있는 페이지로 redirection 시켜주기 위한 설정입니다.

@Bean
@Throws(Exception::class)
fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
    http
        .authorizeHttpRequests { authorize ->
            authorize.anyRequest().authenticated()
        }
        .formLogin(withDefaults())
    return http.build()
}

@Bean
fun userDetailsService(): UserDetailsService {
    val userDetails: UserDetails = User.withDefaultPasswordEncoder()
        .username("client")
        .password("password")
        .roles("USER")
        .build()
    return InMemoryUserDetailsManager(userDetails)
}

아래 페이지로 이동되면 로그인을 해서 인증을 한다면 이후 OAuth2 path를 통해 OAuth2와 연관된 flow를 정상적으로 가져갈 수 있으며 이를 통해서 token을 얻을 수 있습니다.

허용된 client만 인증할 수 있도록 Client 등록

OAuth 2.0 인증을 위해서는 기 등록된 Client를 사용해야합니다. 아래 코드는 Client를 RegisteredClientRepository에 등록 시키는 코드입니다. Client 등록은 인증 서버로써는 인증된 클라이언트만 token을 얻을 수 있도록 하는 중요한 작업입니다. 저장소의 경우 InMemoryRegisteredClientRepository와 JDBCRegisteredClientRepository를 기본적으로 제공해주고 있습니다. 아쉽게도 JPA는 기본적으로 제공해주고 있지 않는데요. 사용하기 위해서는 별도의 커스터마이징이 필요합니다.

@Bean 
public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("messaging-client")
            .clientSecret("{noop}secret")
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // Basic Auth로 Client 확인
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
            .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
            .redirectUri("http://127.0.0.1:8080/authorized")
            .scope(OidcScopes.OPENID)
            .scope(OidcScopes.PROFILE)
            .scope("message.read")
            .scope("message.write")
            .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
            .build();

    return new InMemoryRegisteredClientRepository(registeredClient);
}

등록할 수 있는 properties

client를 등록할 때 등록할 수 있는 properties로는 아래와 같습니다. 필수적인 properties도 있고 선택적인 properties도 있습니다. docs를 기준으로 properties에 대해서 알아보도록 하겠습니다. ( Bold체가 중요한 부분 )

  • id: RegisteredClient를 식별하는 고유 unique ID.
  • clientId: 클라이언트의 ID.
  • clientIdIssuedAt: clientID 발급 된 시간.
  • clientSecret: 클라이언트의 Secret. 기본적으로 PasswordEncoder로 encoding 해서 관리합니다. (NoOps를 통해서 plain하게 관리가능)
  • clientSecretExpiresAt: 클라이언트의 secret이 만료되는 시간입니다.
  • clientName: 화면에 표시되는 클라이언트의 이름.
  • clientAuthenticationMethods: 본인이 정상적인 클라이언트라는 것을 인증하는 방법. client_secret_basic, client_secret_post, private_key_jwt, client_secret_jwt 리스트가 존재함
  • authorizationGrantTypes: 해당 클라이언트가 사용할 수 있는 authorization grant type. authorization_code, client_credentials, and refresh_token 정도를 서포팅함.
  • redirectUris: 해당 클라이언트에게 허용된 redirection 주소
  • scopes: Client가 결과적으로 얻은 토큰을 이용하여 요청 할 수 있는 범위

마무리

오늘은 이렇게 spring-authorization-server를 이용해서 OAUth 2.0 authorize_code grant Type의 flow를 통해 access token을 발급받아보았습니다.

해당 실습은 github repository에 업로드 해두었습니다. 해당 Repository에서 확인하실 수 있습니다.

다음 시간에는 이렇게 얻은 AccessToken을 이용해서 실제로 Resource 서버에 접근하여 원하는 데이터를 가져올 수 있도록 구현해 보겠습니다.

감사합니다.

참조

[1] https://docs.spring.io/spring-authorization-server/docs/current/reference/html/

[2] https://github.com/spring-projects/spring-authorization-server

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

[4] https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server

[5] https://spring.io/blog/2019/11/14/spring-security-oauth-2-0-roadmap-update

댓글