Network

JWT, OAuth란 ? + 구조, 예시

woo0doo 2023. 3. 30. 22:27

서론

 

프로젝트 중 회원가입 및 로그인 구현을 맡게되어서 JWT와 OAuth에 대해 알아보게 되었다. 

 


 

OAuth

  • OAuth(Open Authentication 또는 Open Authorizaion)는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준이다.
  • OAuth의 기본 핵심은 인증 과정을 제 3자에게 위임하는 것이다. 따라서 제 3자의 인증 정보를 내 서비스에서 이용한다고 볼 수 있다. 여기서 제 3자는 카카오, 구글, 네이버 등이 있다.
  • OAuth가 사용되기 전에는 외부 사이트와 인증 기반의 데이터를 연동할 때 인증 방식의 표준이 없었기 때문에, 기존의 기본 인증인 아이디와 비밀번호를 사용했는데, 이는 보안상 비밀번호 노출에 취약한 구조였다. 그래서 이 것을 보안하기 위해 OAuth의 인증은 API를 제공하는 서버에서 진행하고, 유저가 인증되었다는 Access Token을 발급하였다. 발급된 Access Token으로 Third party 애플리케이션에서는 Service Provider의 API를 안전하고 쉽게 사용할 수 있게 되었다.

OAuth2 용어 정리

1. Resource Owner(자원 소유자)

Resource server(구글, 페이스북, 카카오 등)의 계정을 소유하고 있는 사용자를 의미한다.

 

2. Client

구글, 페이스북, 카카오 등의 API 서비스를 이용하는 제 3의 서비스

 

3. Authorization Server(권한 서버)

권한을 관리해 주는 서버, Access Token, Refresh Token을 발급, 재발급 해주는 역할을 한다.

 

4. Resource Server

OAuth2 서비스를 제공하고, 자원을 관리하는 서버이다.

 

5. Access Token

Authorization Server로부터 발급 받은 인증 토큰, Reource Server에 전달하여 서비스를 제공 받을 수 있다.

 

 

OAuth2 인증 과정

 

  • Authorization Code Grant
  • Implicit Grant
  • Resource Owner Password Credentials Grant
  • Client Credentials Grant

OAuth 2.0에는 이 4가지 인증 방식이 존재하나 대표적으로 많이 사용되는 Authorization Code Grant를 사진으로 보겠다.

 

쉽게 요약하면 다음과 같다.

1. 어플리케이션이 OAuth 서버에 요청해 브라우저를 열어 사용자가 인증을 진행하게 한다.

2. 사용자는 브라우저에서 나오는 인증 프롬포트로 인증 후 어플리케이션의 요청을 승인한다.

3. 사용자는 OAuth 서버에서 인증코드를 받아 어플리케이션으로 돌아온다.

4. 어플리케이션은 인증 코드를 access token 으로 교환해준다.

 

Request & Response Example

1. 클라이언트 정보와 함께 OAuth 서버에 요청

 

GET /auth
    ?response_type=code
    &client_id=s6BhdRkqt3&state=xyz
    &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
    &scope=create+delete
    &state=xcoiv98y3md22vwsuye3kch
Host: server.example.com
  • response_type=code : 어플리케이션이 Authorization Code 인증을 시작하고 있음을 OAuth 서버에 알린다.
  • client_id : 개발자가 어플리케이션을 처음 등록 할 때 얻은 어플리케이션의 공개 식별자.
  • redirect_uri : 요청을 승인 한 후 사용자를 다시 보낼 위치를 OAuth 서버에 알립니다.
  • scope (optional) : 어플리케이션이 요청한 사용 권한을 나타낸다.
  • state (optional) : 어플리케이션에서 임의의 문자열을 생성하고 요청에 포함시킵니다. 그런 다음 사용자가 권한을 인증 한 후에 동일한 값이 반환되는지 확인해야한다. 이것은 CSRF 공격을 막는 데 사용된다.

client_id 는 어플리케이션을 등록하면 받는 어플리케이션의 고유 식별값이다. client_password 와 함께 인증 코드를 access token으로 교환할 때 제대로된 곳에서 인증을 했는지 확인하는 데 사용된다.

 

2. 어플리케이션에 돌아오는 응답

https://example-app.com/redirect
    ?code=g0ZGZmNjVmOWIjNTk2NTk4ZTYyZGI3
    &state=xcoiv98y2kd22vusuye3kch
  • code : OAuth 서버에서 만들어준 인증 코드.
  • state : 요청할 때 똑같이 왔는지 어플리케이션에서 확인해야한다.

3. 인증 코드를 access token으로 교환

 

요청

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
  • grant_type=authorization_code : 어플리케이션이 authorization_code 유형을 사용하고 있음을 알린다.
  • code - 리다이렉션에 제공된 인증 코드.
  • redirect_uri- 코드를 요청할 때 사용 된 동일한 리다이렉션 URI이다. 필요하지 않을 경우도 있다.
  • client_id - .어플리케이션의 고유 식별 값.
  • client_secret- 이 값을 사용하면, access token을 얻으려는 요청이 어플리케이션에서만 이루어지며 인증 코드를 가로채는 잠재적인 공격자가 아닌 것이 보장된다.

응답

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
  "token_type":"bearer",
  "expires_in":3600,
  "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk",
  "scope":"create delete"
}

노출되어선 안되는 정보이기 때문에 캐시에 저장되어선 안되기 떄문에 헤더에 꼭 아래의 코드를 명시하길 권장한다.

Cache-Control: no-store
Pragma: no-cache

JWT

JWT(Json Web Token)은 JSON 객체를 사용하여 가볍고  self-contained 방식으로 정보를 안정적으로 전달해주는 방식이다.

※ self-contained : 필요한 모든 정보를 자체적으로 가지고 있다는 뜻으로, 토큰에 대한 기본 정보, 전달할 유저 정보, 토큰이 검증됐다는 것을 증명해주는 signature를 포함하고 있다.

 

유저가 서버에 요청할 때 헤더에 JWT를 포함하여 전달하면 서버는 그 토큰을 검증하고 권한을 처리한다.

 

장점

  • 따로 세션(서버에 저장하는 활성화된 접속 정보)을 유지할 필요가 없다. (Stateless)
  • 쿠키를 전달하지 않아도 되므로 쿠키를 사용함으로써 발생하는 취약점이 사라진다.
  • 사용자 인증에 필요한 모든 정보는 토큰 자체에 포함하기 때문에 별도의 인증 저장소가 필요 없다.

단점

  • 토큰 자체에 모든 정보를 담고 있으므로 보안이 위험할 수있다.
  • 토큰의 페이로드(Payload)에 3종류의 클레임을 저장하기 때문에, 정보가 많아질수록 토큰의 길이가 늘어나 네트워크에 부하를 줄 수 있다.
  • Stateless: JWT는 상태를 저장하지 않기 떄문에 한번 만들어지면 제어가 불가능하다. 즉, 토큰을 임의로 삭제하는 것이 불가능하므로 토큰 만료 시간을 꼭 넣어주어야 한다.

그렇다면 왜 JWT를 쓸까?

 

세션 대신 토큰을 사용하려는 이유는 확장성 때문이다. 만약 여러 서버가 돌아가는 상황이라면, 각 서버마다 세션 저장소를 두거나, 공통 세션 저장소를 만들어야한다. 따라서 비용이 발생하게 된다. 반면에 토큰은 stateless 하기 때문에 확장에 용이하다.

다만, 토큰은 한 번 발급되면 강제로 만료시킬 수 없다는 단점이 있다. 따라서 만료시간이 짧은 Access Token과 만료시간이 긴 Refresh Token을 나눠서 사용하는 것이다.

토큰은 실제로 로그인을 유지하고 있는 것이 아니라 로그인이 유지된 것 처럼 행동한다. (클라이언트가 Access Token을 저장해두고, 요청 때 마다 보내는 방식) 따라서 만약 Access Token이 만료된 것이 아니라면, 로그아웃을 했더라도 해당 Access Token만 가지고 있다면, 로그인 된 상태처럼 행동할 수 있다고 한다.

 

 

JWT의 구조

JWT는 Header, Payload, Signature로 구성되있다.

 

Header

JWT에서 사용할 타입(typ)과 해시 알고리즘(alg)의 종류가 담겨있다.

{
  "typ": "JWT",
  "alg": "HS512"
}

Payload

서버에서 첨부한 사용자 권한 정보와 데이터인 클레임(Claim)이 담겨있다. 클레임은 Key/Value 형태로 된 값을 가진다. 저장되는 정보에 따라 Registered Claims, Public Claims, Private Claims로 구분된다.

- 등록된 클레임(Registered Claims)

  • iss: 토큰 발급자(issuer)
  • sub: 토큰 제목(subject)
  • aud: 토큰 대상자(audience)
  • exp: 토큰 만료 시간(expiration)
  • nbf: 토큰 활성 날짜(not before), 이 날이 지나기 전의 토큰은 활성화되지 않는다.
  • iat: 토큰 발급 시간(issued at)
  • jti: JWT 토큰 식별자(JWT ID), 중복 방지를 위해 사용하며, 일회용 토큰(Access Token)등에 사용한다.
  •  
{
  "sub": "subject",
  "iss": "abc",
}

 

- 공개 클레임(Public Claims)

공개 클레임들은 충돌이 방지된 이름을 가지고 있어야 한다. 주로 URI 형식으로 짓는다.

{
  "https://woo0doo.tistory/jwt/public": true
}

 

- 비공개 클레임(Private Claims)

클라이언트와 서버 협의하에 사용되는 클레임이다. 공개 클레임과는 달리 이름이 중복되어 충돌될 수 있기 때문에 유의해야 한다.

{
  "username": "kim"
}

 

Signature

Header, Payload를 Base64 URL-safe Encode 한 이후, Header에 명시된 해시함수를 적용하고, 개인키(Private key)로 서명한 전자서명이 담겨있다. 이는 Header, Payload가 변조되었는지 확인하기 위해 사용되는 중요 정보이며, JWT를 신뢰할 수 있는 토큰으로 사용할 수 있는 근거가 된다.

※ Base64 URL-safe Encode - 웹으로 전송하기 위해 사용하는 문자가 Base64 형태의 문자이다. 일반적인 Base64 Encode에서 URL에서 오류없이 사용하도록 '+' , '/'를 각각 '-' , '_'로 표현한 것이다.

 


OAuth와 JWT 함께 활용한 설계 예시

프론트엔드 역할

  • OAuth Authorization Server로 로그인 요청 후, Authorization code를 발급 받아, 백엔드에 전달한다.
  • 백엔드에서 응답받은 access token, refresh token을 저장한다.
  • 권한이 필요한 요청마다 Authorization 헤더에 access token 같이 보내주기
  • access token이 만료되었다면, refresh token 보내서 갱신하기(프론트에서 요청 날릴 때 access token이 만료됨을 미리 판별하여 갱신 요청을 보낼 수 있음)
  • refresh token 만료 기간이 7일 이내면, refresh token 재발급 요청한다.

백엔드 역할

  • Authorization code로 Oauth 서버에 토큰 요청
  • Access token으로 이름, 이메일, 프로필 URL 정보 요청한다.
  • DB에 존재하지 않는 유저라면 새로 등록하고, DB에 존재하는 유저라면 정보 업데이트한다.
  • 유저의 primary key 값으로 JWT 토큰(access token & refresh token) 생성. 일반적으로 access token은 한 시간, refresh token은 2주로 생성한다.(본인 애플리케이션에 맞게 변경하여 사용한다.)
  • refresh token은 DB나 Redis에 저장한다.
  • 유저 정보, access token, refresh token 프론트로 전달한다.
  • access token 만료시 refresh token 검증 후, 재발급한다.

최종 설계


참조:

https://yonghyunlee.gitlab.io/temp_post/oauth-authorization-code/

https://velog.io/@max9106/OAuth

https://seungwoolog.tistory.com/95