spring/spring security

스프링 시큐리티 기본 개념공부 10. (OAUTH2 란?)

대기업 가고 싶은 공돌이 2024. 7. 18. 21:34

Section 11.

OAUTH2

  • oauth2는 많은 업계에서 사용중인 인증과 인가를 위한 프로토콜이다.

OAUTH2가 해결한 문제 시나리오

  • 트위터 앱을 사용중인 유저들이 자신들의 트위터 사용 기록을 분석하고 싶어한다.
  • 트위터 Analizer라는 웹 어플리케이션을 개발하려한다.
  • 트위터 Analizer는 트위치 내 유저의 모든 정보가 필요하다.
  • 트위터 Analizer는 트위치에 유저 정보를 요구하기위해 유저의 인증관련 정보가 필요하다.
  • 유저는 트위치 Analizer에 자신의 인증 정보를 모두 공유하고 트위치 Analizer는 그 정보를 통해 트위치에서 유저 관련 정보를 받아왔다.
  • 하지만 본인의 인증 정보를 제 3어플리케이션에 공유하는 것이기에, 만약 해당 웹이 불법 사이트였을 경우 자신의 정보가 악용될 가능성이 있었다.
  • 그렇기에 인증 정보는 공유하기 않고 제한적인 접근을 허용하기 위해 나온 것이 OAUTH2이다.
  • 인증 토큰을 트위치 Analizer에게 공유하고 토큰을 통해 트위터 정보에 접근할 수 있는것이다.

OAUTH2의 장점

  • 위의 예시와 같이 한 개의 회사에서 여러가지 어플리케이션을 서비스하고 있는 상황을 생각해보자
  • 구글은 별개의 인증 서버를 운용하며 모든 인증은 해당 인증서버를 통해서만 이뤄진다.
  • 구글의 모든 어플리케이션들은 동일한 토큰을 공유하며, 어느 어플리케이션에 접근하던 동일한 토큰을 동일한 인증서버에 보내면 되니 재로그인이 필요없어진다.

OAUTH2 소개

  • OAUTH2는 프레임워크나 api 같은 것이 아닌 단순 프로토콜이다.
  • OAUTH2는 특정 애플리케이션 정보에 접근하고 싶을 때 따라야하는 표준 규격이다.
    • EX) 우리가 만들 사진 어플리케이션이 구글 Photo 내의 사진에 접근해야 하는 경우
    • 오직 read에 대한 권한만 부여함으로 사진을 보는 것 이외에 아무런 동작을 할 수 없게 만들면 된다.
  • 액세스 토큰을 통하여 위의 기능을 구현한다.

여러가지 중요한 용어들

  • Resource owner: OAUTH2에서 엔드 유저는 Resource owner라고 불린다.
  • Client: 위의 예시에서 트위터 Analizer 애플리케이션을 Client라고 부른다.
  • Authorization Server: 유저에게 아이디와 비밀번호 권한등의 정보를 묻고 리소스 오너인지 판단하는 서버다.
  • Resource Serve: 리소스들을 저장하고 있는 서버를 의미한다.위의 예시에선 트위터에 해당한다.
  • Scopes: 허가해줄 권한의 범위를 의미한다. 위의 예시에서는 트위터 정보를 읽기만 가능하게 할 것이니 트윗 읽기, 사진 보기 등 권한이 scope에 해당한다.

OAUTH2 구현 시나리오

  • 트위터 Analizer를 구현한다고 생각해보자
    1. 우선 트위터 어플리케이션에 트위터 Analizer를 클라이언트로 등록해야한다. (웹 이름, 로고 등등)
    2. 트위터에서 클라이언트 ID와 클라이언트 시크릿 이라는 비밀번호를 발급해준다.
    3. 유저는 (트위터를 통해 회원가입하기) 버튼을 통해 트위터 계정으로 회원가입을 시도한다.
    4. 인증서버에서 유저에게 리소스 접근 권한을 허용하냐는 질문을 하고 승인했다면, 인증서버에서 클라이언트에게 액세스 토큰과 리프레쉬 토큰을 발급한다. (해당 토큰을 애플리케이션에 잘 보관해둬야 반복적으로 트위터를 통해 로그인할 필요가 없어진다.)
    5. 이제 트위터 Analizer는 리소스 소유자에게 /getTweets라는 api를 통해 리소스를 요청한다. 요청 시마다 액세스 토큰을 함께 보내줘야한다.
    6. 리소스 서버 즉 트위터는 액세스 토큰을 확인한다.
    7. 그럼 정보 읽기가 가능하다.

Authorizationcode grant flow

  • OUATH2에는 여러가지 인증유형이 있고 우리가 웹 어플리케이션을 만들 때 그러한 여러가지 인증 유형중 하나를 선택해야한다.
  • Authorization code: 엔드유저가 연관되어 있거나 두 개의 다른 어플리케이션이 서로 교류하려할 때 이 방법을 사용해야한다.
    1. 엔드유저가 리소스에 접근하고 싶다는 의사를 밝힌다.
    2. 클라이언트 어플리케이션은 유저의 권한이 필요하다는 정보를 보낸다.
    3. 유저는 인증 서버 로그인 페이지로 이동하게 된다.
    4. 로그인을 하면 인증서버는 액세스 토큰이 아닌 일시적인 Authorization code를 클라이언트 어플리케이션에 제공한다.
    5. 클라이언트 어플리케이션은 Authorization code를 인증 서버에 보내면서 유저의 인증 정보들도 함께 보낸다. 해당 정보들을 바탕으로 액세스 토큰을 발금해달라고 한다.
    6. 인증 서버는 정보들을 바탕으로 액세스 토큰을 클라이언트 어플리케이션에 반환해준다.
    7. 클라이언트 어플리케이션은 액세스 토큰을 리소스 서버에 보내면서 리소스요청을 한다.
    8. 액세스 토큰을 확인하고 리소스를 반환한다.
    • 위의 2,3단계에서는 client_id, redirect_url, scope, state(csrf 토큰값), return_type등의 정보가 인증 서버에 보내진다.
      • 인증서버는 Authorization code를 반환한다.
    • 위의 5단계에서는 액세스 토큰을 발급받기 위해 인증서버에 Authorization code, client_id. client_secret, grant_type(Authorization_code의 유형이다를 명시), redirect_url의 정보가 인증 서버에 보내진다.
    • 왜 두 번의 인증이 존재할까?
      • 첫 번째 Authorization code 발급을 위한 인증은 유저가 인증서버에 유효한 유저임을 인증받는 과정이다.
      • 두 번째 액세스 토큰 발급을 위한 인증은 해당 웹 어플리케이션이 Client로 등록된 웹이다 라는 것을 인증하기 위한 과정이다. 따라서 Client_secret이 두 번째 단계에서 전송된다.
  • 위의 두 단계를 하나로 합친 implicit grant type이란 인증 타입이 존재하긴 하지만 보안성이 떨어지기 때문에 사용하지 않는다.
Authorization code 흐름 요약
  1. 엔드유저가 회원가입 등의 요청을 클라이언트 어플리케이션으로 보냄
  2. 클라이언트 어플리케이션에서 엔드유저를 인증서버로 보냄
  3. 같이 보내는 정보는 response type과 client id, redirect url, scope, state의 정보를 함께 보냄
  4. 유저는 인증서버로 이동한 후 자신의 인증 정보를 입력함
  5. 유저에게 클라이언트 어플리케이션이 당신의 사진에 접근하려 한다 허용하겠냐? 등의 메세지가 표시된다.
  6. 인증서버는 Authorization code와 함께 state에 담겨있던 csrf 토큰을 같이 클라이언트 어플리케이션으로 보낸다.
  7. csrf 토큰에 오류가 없다면 무사히 수신된다.
  8. 클라이언트 어플리케이션은 post 유형으로 인증서버에 액세스 토큰을 요청한다.
  9. 같이 보내는 정보는 grant type, client id, client secret, redirect url, authorization code다.
  10. 인증서버에서 해당 정보들을 확인후 액세스 토큰을 반환한다.
  11. 같이 반환되는 정보에는 token 타입, 만료 시간, 액세스 토큰, scope, refresh 토큰이 있다.

Password grant / Resource Owner credentails grant flow

  1. 엔드유저가 자신의 실제 인증 정보를 클라이언트 어플리케이션에게 보낸다.
  2. 클라이언트 어플리케이션이 post 요청으로 유저의 인증정보와 client id, client secret 정보를 인증 서버에 넘긴다.
  3. 인증 서버는 액세스 토큰을 바로 클라이언트 어플리케이션에 반환해준다.
  4. 클라이언트 어플리케이션은 액세스 토큰을 리소스 서버에 보내면서 리소스를 요청한다.
  5. 액세스 토큰이 적절하다면 리소스를 반환한다.
  • 단점: 엔드유저가 실제 인증 정보를 애플리케이션과 공유한다. (OUATH2를 이용할 이유가 없다.)
  • 클라이언트 어플리케이션이 인증 서버에 보내는 정보: client 아이디 및 시크릿, scope, username&password, grant type
  • 클라이언트 어플리케이션, 리소스 서버, 인증 서버가 모두 같은 단체에 속해있을 경우 위와 같은 인증방식을 사용한다.

Client credentials grant flow

  • 엔드유저가 연결돼있지 않은 두 백엔드 어플리케이션에 관한 상황에서 사용한다.
  • 클라이언트 어플리케이션은 A회사에 소속, 인증 서버와 리소스 서버는 B회사에 소속된 상황이라 생각해보자. 클라이언트 어플리케이션이 B회사의 리소스 서버에서 자료를 가져오고 싶다면 인증 서버에서 인증을 성공한 후에 가져올 수 있을것이다.
  1. 클라이언트 어플리케이션에서 유저 세부사항을 인증 서버로 보낸다.(엔드유저는 관련돼있지 않다.)
  2. 인증과 관련된 정보들을 인증서버에서 확인한 후 클라이언트 어플리케이션에 액세스 토큰을 반환한다.
  3. 클라이언트 어플리케이션에서 액세스 토큰과 함께 리소스 요청을 리소스 서버에 보낸다.
  4. 액세스 토큰이 적절하다면 리소스를 반환해준다.
  • 클라이언트 어플리케이션에서 인증 서버에 보내야하는 정보
    • client id, client secret, scope, grant type
    • grant type에 client_credentials를 적어서 보내면 엔드유저가 관련되지 않다는 것을 인증 서버는 이해할것이다.
    • 해당 방식에서 state에 csrf 토큰을 넣어서 보내지 않는 이유는 브라우저와 통신이 없기 때문이다.(세션, 쿠키가 없으므로 사용자 정보를 바탕으로 csrf 공격을 할 수 없기 때문)

refresh token grant flow

  • 액세스 토큰의 기간이 만료됐을 때 리프레쉬 토큰을 다시 인증서버에 보낸다. 인증 서버는 리프레쉬 토큰을 확인한 후 새로운 액세스 토큰과 리프레쉬 토큰을 발급해준다.
  • refresh token grant flow는 엔드유저가 관련되지 않는다. 액세스 토큰의 기간이 만료됐다면, 자동으로 서버에서 인증 서버로 refresh 토큰을 보낼것이기 때문이다.
  1. 리소스 서버에 액세스 토큰을 보내며 자원을 요청함
  2. 리소스 서버에서 토큰 만료된 것을 보고 403 에러를 반환함
  3. 클라이언트 어플리케이션은 인증서버에 리프레쉬 토큰을 보내며 새 액세스 토큰을 요청함
  4. 리프레쉬 토큰이 적절하다면 인증서버는 새 액세스 토큰과 리프레쉬 토큰을 반환한다.
  5. 새 리프레쉬 토큰은 브라우저나 데이터베이스에 저장한다.
  6. 새 액세스 토큰을 활용하여 리소스 서버에 리소스를 요청한다.
  • 클라이언트 어플리케이션에서 인증서버에 보내야하는 정보
    • client id, client secret, refresh token, scope, grant type
    • grnat type에는 refresh_token 이라고 적어서 보내야 인증 서버에서 액세스 토큰과 리프레쉬 토큰을 새로 발급해준다.

액세스 토큰이 적절한지 리소스 서버는 어떻게 확인할까?

  1. 인증 서버와 리소스 서버가 api 소통을 하는 것이다. 액세스 토큰이 들어오면 해당 토큰을 인증 서버에 보내며 유효성을 검사한다. (액세스 토큰이 들어올 때마다 인증 서버와 소통해야 하므로 효율이 약간 떨어진다.)
  2. 인증 서버와 리소스 서버가 동일한 데이터베이스를 공유하는 것이다. 인증서버에서 액세스 토큰을 발급해주면 데이터베이스에 활성화 표시를 해주고, 액세스 토큰이 리소스 서버에 들어오면 리소스 서버는 쿼리를 날려 활성화 상태인지 아닌지를 판단하면 된다.
  3. 어플리케이션이 시작할 때 리소스 서버는 인증 서버에게 공개 인증서를 발급받아둔다. 나중에 액세스 토큰이 들어올때 마다 리소스 서버는 공개 인증서를 통하여 액세스 토큰의 진위 여부를 판단한다. (현재 가장 많이 사용되는 방법이며 맨 처음 인증 서버와 리소스 서버의 연결이 된 이후 지속적인 소통이 필요 없기에 효율적이다.)
  • Oauth2는 유저의 정보가 어떤지 유저의 이메일은 무엇인지에 관한 정보는 파악할 수 없다.
  • Oauth2의 구조물을 그대로 사용하면서 유저의 세부 정보를 받고 싶었던 개발자들은 Ouath2 기반에서 사용 가능한 방법인 OpenID Connect를 개발했다.

OpenID Connect 프로토콜

  • 인증 서버와 OICD로 연결을 시도하면 인증서버에선 액세스 토큰과 ID 토큰을 반환한다. (리프레쉬 토큰 제외)
  • 여기서 ID 토큰이 유저의 세부 정보에 관한 권한을 관리한다.
  • 우리가 반환값으로 ID 토큰을 받으려면 scope에 OpenID 라는 값을 보내야한다.
  • 액세스 토큰과 ID 토큰의 조합을 통해 세부적인 유저 권한 관리가 가능해졌고, 그에따라 나타난 개념이 IAM 이다.
  • OIDC를 활용하면 scope에 profile, email, address 같은 값을 넣을 수 있고, jwt 토큰에 해당 정보들을 담아 교환할 수 있으며, /userinfo라는 공개 엔드 포인트를 통해 유저 정보를 쉽게 파악할 수도 있다.
  • 인증서버에서 반환된 openid 토큰을 확인해보면 바디 부분에 base64로 우리가 scope에 담아 요청한 유저의 세부 정보가 인코딩 된 것을 확인할 수 있다.

Section 12.

Oauth2 구현

  • 우선 spring-boot-starter-oauth2-client 다음과 같은 의존성을 추가해야 한다. 이를 통해 클라이언트 어플리케이션이라는 사실을 알려주는것이다.
  • 그리고 config의 defalutSecurityFilterChain에서 http.authorizeRequests().anyRequest().authenticated().and().oauth2Login();
  • 을 통해서 ouath2 방식의 로그인을 지원한다. 모든 url에 대하여 인증 서버로 이동 시켜준다.
/*@Bean
    public ClientRegistrationRepository clientRepository() {
        ClientRegistration clientReg = clientRegistration();
        return new InMemoryClientRegistrationRepository(clientReg);
    }

    private ClientRegistration clientRegistration() {
		return CommonOAuth2Provider.GITHUB.getBuilder("github").clientId("8cf67ab304dc500092e3")
	           .clientSecret("6e6f91851c864684af2f91eaa08fb5041162768e").build();
	 }*/
  • 다음과 같이 clientRegistration을 통해 client 정보를 리턴해주고 리턴된 정보는 ClientRegistrationRepository에 저장시켜 클라이언트 정보를 인증 서버에 전달할 수 있게 만들어준다.
  • CommonOAuth2Provider은 열거형 클래스로 내부의 getBuilder를 통해 scope,autorizationUrl, tokenUrl등등의 정보를 우리가 직접 입력하지않고 활용할 수 있다.
  • 우리는 단순히 getBuolder를 호출하면서 어떤 인증 서버인지와 클라이언트 정보만 넘겨주면 된다.
  • 위의 방법 말고 더 간단한 방법도 있다. application.properties에
spring.security.oauth2.client.registration.github.client-id=8cf67ab304dc500092e3
spring.security.oauth2.client.registration.github.client-secret=6e6f91851c864684af2f91eaa08fb5041162768e
  • 위와 같이 클라이언트 정보를 넣어두면 자동으로 클라이언트 정보를 파악해서 인증 서버에 넘겨준다.
  • 깃허브 말고 페이스북이나 구글을 활용하고 싶다면, 위 코드의 github 부분만 바꿔주면 된다.