💡 해당 내용은 이전 블로그(Tistory)에서 이관한 글입니다.
OAuth란 무엇일까?
- 비밀번호를 제공하지 않고 타사의 사이트 혹은 애플리케이션의 접근 권한을 부여하는 수단
- 접근 위임을 위한 개방형 표준 프로토콜
OAuth의 발전
- OAtuh 1.0에서 토큰 승인 과정에서 세션 고정 공격이라는 보안 결함이 발견
- 세션을 주입할 코드를 설치 후 유도하여 인증을 통과한 유효 세션을 탈취하는 방법
- OAuth1.0의 보안 문제 등을 개선한 버전으로 OAuth 2.0 탄생 (하위 호환성 미지원)
OAuth 1.0 -> 2.0
-
API 요청 시 클라이언트 인증 방법 변경 : 서명 → HTTPS 의무화
-
Refresh Token 도입을 통한 접근 토큰(Access Token) 유효기간(Life-time) 설정으로 인한 단축
→ 보안성 개선
-
기존 서비스 제공자를 자원 서버, 권한서버로 분리하여 다수의 서버로 구성된 웹 서비스에서 발생 가능한 권한 동기화 문제를 개선
-
다양한 확장성 지원
OAuth 인증은 어떻게 이루어질까?
- OAuth 인증은 소비자 ↔ 서비스 제공자 사이에 인증 과정을 통해 이루어집니다
- ex) 소비자: 클라이언트 <-> 서비스 제공자: Facebook
📢 OAuth의 흐름을 알아보기 전에 용어부터 먼저 알아보고 가겠습니다
OAuth 관련 용어
Resource Owner (자원 소유자)
- 서비스 이용자
- 보호 자원에 접근 권한을 부여할 수 있는 개체
Resource Server (자원 서버)
- API 서버
- 보호 자원에 대한 서비스 API를 제공하는 서버
Authentication (인증)
- 사용자인지 아닌지를 확인 (로그인)
- 권한을 요청하기 전 로그인 주체가 누구인지 판별하는 단계
Authorization (인가)
- 자원에 접근할 권한이 있는지 확인 (Admin)
- 사용자인지는 확인됐으나 해당 자원에 접근할 권한이 있는지 확인하는 단계
Authorization Server(권한 서버)
- 권한 관리 및 부여 서버
- Access Token, Refresh Token 등을 발급해주는 역할
Client
- Web/App
- 자원 서버(API 서버)에서 보호 자원을 요청하고 관련 서비스를 제공하는 애플리케이션
Access Token (접근 토큰)
- 인증 후에 사용자가 서비스 제공자가 아닌 소비자를 통해 자원에 접근하기 위한 키를 포함한 값
- 만료기간이 존재 (Life-Time)
📢 이제 일반적인 인증 흐름을 간략하게 알아본 후에, 아래에서 실제 권한 부여 방식을 자세히 알아보겠습니다
일반적인 인증 흐름
이미지 출처: https://datatracker.ietf.org/doc/html/rfc6749
1. 클라이언트 -> 자원 소유자(사용자) : 인증 요청
- 해당 인증 요청은 애플리케이션의 인증임과 동시에 권한 서버로의 인증 요청이기도 합니다
- 사용자는 해당 인증을 통해 이후 작업을 따로 하지 않아도 되기 때문에, 사용자 입장에서는 권한 서버로의 인증 요청이기도 한 것입니다
- 사용자는 로그인 페이지에서 인증을 시도합니다
2. 자원 소유자(사용자) -> 클라이언트 : 인증(로그인) 완료
- 사용자가 애플리케이션으로부터 인증을 완료한 후, 클라이언트는 권한 서버로 Authorization grant (권한 요청)을 진행합니다
💡 여기까지 위 그림 1, 2번에 해당
3. 클라이언트 -> 권한 서버 : 권한 요청
- 클라이언트는 위에서 인증받은 내용을 토대로, 권한 서버에게 권한에 대한 내용을 증명하고 접근 토큰을 요청합니다
4. 권한 서버 -> 클라이언트 : 권한 부여
- 권한 서버는 해당 권한을 확인한 후(인증), 접근 토큰인 Access Token을 발급해줍니다
💡 여기까지 위 그림 3, 4번에 해당
5. 클라이언트 -> 자원 서버 : 인가 요청
- 발급받은 접근 토큰(Access Token)을 이용하여 클라이언트는 자원 서버에 인가 과정을 진행합니다
6. 자원 서버 -> 클라이언트 : 요청에 대한 응답 반환
- 자원 서버에서는 접근 토큰을 통해 요청한 리소스에 대한 권한이 있는지 인가를 진행합니다
- 권한이 있다면 인가가 성공하여 요청한 리소스(데이터)를 반환합니다
- 권한이 없다면 인가가 실패하여 403 Error를 반환합니다
💡 여기까지 위 그림 5, 6번에 해당
OAuth 2.0 권한 부여 방식
- OAuth 2.0의 권한 부여 방식은 주로 4가지가 사용된다.
- 권한 부여 승인 코드 방식 (주로 사용)
- 암묵적 승인 방식
- 자원 소유자 자격 증명 승인 방식
- 클라이언트 자격증명 승인 방식
- OAuth 2.1에서는 legacy들은 버려질 예정이다 (개발 진행중)
2. 암묵적 승인 방식
3. 자격증명 승인 방식
1. Authorization Code Grant (권한 부여 승인 코드 방식)
- 어플리케이션이 인증 서버에 요청해 브라우저를 열어서 사용자가 인증을 진행하게 하는 방식
response_type=code, grant_type = authorization_code
- Access Token 요청 시 Authorization code를 요청하는 단계가 있어서 보안에 효과적
- Refresh Token 사용 가능
- 가장 복잡하지만, 가장 많이 사용되는 방식
이미지 출처 : https://developers.payco.com/guide
이미지 출처 : http://www.msaschool.io/operation/design/design-seven/
2. Implicit Grant (암묵적 승인 방식) : Legacy (삭제예정)
-
1번 방식(권한 부여 승인코드)에서 Authorization code를 요청하는 프로세스를 제거한 방식
-
로그인(인증) 성공 시 URL로 Access Token을 즉시 반환 → 보안 위험
-
자격 증명을 안전하게 저장하기 힘든 클라이언트(JavaScript)이거나 외부에 있는 OAuth 서버가 cors를 지원하지 않을 때 사용하지만 권장하지 않음
-
따라서 신뢰성 있는 앱 또는 단말기에서만 사용해야 함
-
Refresh Token 사용 불가
이미지 출처 : https://velog.io/@kimjaejung96/OAuth2.0-개념-및-작동방식
3. Resource Owner Password Credentials Grant (자원 소유자 자격 증명 승인 방식) : Legacy (삭제예정)
4. Client Credentials Grant (클라이언트 자격증명 승인 방식)
Access Token만으로 충분한가?
- OAuth 2.0은 Access Token을 통해 애플리케이션에 비밀번호를 제공하지 않으면서 실제 서비스를 이용할 수 있게 도움을 준다.
- 하지만 API 요청이 들어올때마다 전달 받은 Access Token이 유효한지 계속 확인해야 한다면 어떨까?
- MSA 아키텍쳐로 구성되어 있는 많은 서비스에서 각각의 서버가 Access Token 유효성 검사를 매 요청마다 한다면, 자원 서버에서는 서버 부하로 인해 인가 단계에서 버티지 못하고 병목 현상 등이 발생할 것이다.
- 이를 해결하는 방법으로 JWT를 사용한다.
접근 토큰을 JWT 토큰으로 사용할 때의 이점
- 일반 토큰을 사용할 시에는 매 요청마다 토큰을 검증해야 합니다
- 토큰 검증은 인증 DB를 통해서 진행합니다 -> 서버 부하 야기
- JWT Token의 구성요소 중 하나인 Signature는 서명만으로도 바로 토큰의 무결성을 체크할 수 있습니다
- 또한 JWT Token의 특징인 Self-Contained(토큰 자체에 정보를 담고 있음) 방식으로 토큰 내 데이터를 통해 추가 작업을 실행할 수 있습니다
- 이로써 DB Connection 감소함으로써 서버 및 DB의 부하를 감소시키는 장점이 있습니다
JWT 토큰이란 무엇인가?
- JWT는 가볍고(compact) 자가 수용적인 (self-contained) 방식으로 정보를 안전성 있게 전달할 수 있는 토큰입니다
- JWT는 Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token입니다
- 비대칭키를 통한 인증 방식을 사용합니다
- JWT는 사용자 정보를 보관 → 토큰 자체를 정보로 사용하는 Self-Contained 방식 사용
💡 예시를 들어보자
토큰을 해석하면 이런 정보들이 담겨져 있다 (Claim 기반 토큰)
{
"id" : "giibeom",
"name" : "Alex",
"role" : ["백엔드노예", "취준지망생", "회사탈출 예정러"]
}
JWT 토큰 구조
- JWT는 Header, Payload, Signature로 3가지의 부분으로 이루어져 있다.
- Json 형태인 각 부분은 “.” 구분자로 나눠서 Base64로 인코딩 되어 표현된다.
💡 예시를 들어보자
해당 토큰은 실제 JWT로 만든 토큰이다
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImdpaWJlb20iLCJuYW1lIjoiQWxleCIsInJvbGUiOlsi67Cx7JeU65OcIOuFuOyYiCIsIuydtOyngSDsp4Drp53sg50iLCLrvYDroZzroZwiXX0.NGFDoN4IIcHENZ8ayO6cVvDiGIk2mszhV7CRHv_hyrw
해당 토큰을 해석하면 아래와 같다!
하나씩 하나씩 파헤쳐보자!
- JWT를 어떻게 검증(Verify)하는가에 대한 내용을 담고 있다.
- 토큰의 헤더는 두가지로 구성된다
PayLoad
- 토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨 있다.
- Json(Key/Value) 형태로 다수의 정보를 넣을 수 있다.
- 클레임은 총 3가지로 나누어 진다
1. 등록된 클레임 (Registered Claim)
- 토큰 정보를 표현하기 위해 이미 이름이 정해진 데이터 종류
- 모두 선택적 (optional)이므로 꼭 사용하지 않아도 된다
클레임 명 | 의미 |
---|
iss | 토큰 발급자 (issuer) |
sub | 토큰 제목 (subject) - 유니크한 값을 사용하므로 주로 사용자 이메일을 사용 |
aud | 토큰 대상자 (audience) |
exp | 토큰 만료 시간(expiration), NumericDate 형식으로 되어 있어야 함
|
ex) 1480849147370 | |
nbf | 토큰 활성 날짜(not before), 이 날이 지나기 전의 토큰은 활성화되지 않음 |
iat | 토큰 발급 시간(issued at), 토큰 발급 이후의 경과 시간을 알 수 있음 |
jti | JWT 토큰 식별자(JWT ID), 중복 방지를 위해 사용하며, 일회용 토큰(Access Token) 등에 사용 |
2. 공개 클레임 (Public Claim)
- 사용자 정의 클레임으로 공개용 정보를 위해 사용된다
- 충돌이 방지된 (collision-resistant) 이름을 가지고 있어야 한다.
- 따라서 충돌을 방지하기 위해서 URI 포맷을 사용한다.
{
"https://github.com/giibeom" : true
}
3. 비공개 클레임 (Private Claim)
- 마찬가지로 사용자 정의 클레임으로 일반적으로는 내부 사용자 ID와 같이 조직과 관련된 정보를 포함합니다
- 서버와 클라이언트 사이에 임의로 지정한 커스텀 정보를 저장
- 공개 클레임과는 달리 이름이 중복되어 충돌될 수 있어서 사용에 유의해야 합니다
📢 서명된 토큰의 경우 토큰 내 정보는 변조로부터는 보호되지만 누구나 읽을 수 있습니다
따라서 암호화 되지 않은 중요한 비밀 정보를 PayLoad나 Header에 넣는것은 지양해야 한다
Signature
- 서명(Signature)은 JWT의 발신처를 확인한 후 메시지가 도중에 변조되지 않았는지 확인하는 데 사용됩니다
서명 생성 과정
- 인코딩 된 Header + 인코딩 된 PayLoad + 비밀키(secretKey or privateKey)를 함께 헤더에 지정된 알고리즘으로 서명
아래에서 HS256 알고리즘으로 해싱하는 서명방식을 예로 들겠습니다
JWT 토큰 예시
- Header(빨강색).PayLoad(보라색).Signature(파란색)으로 이루어져 있다.
- 실제 토큰 사용 :
Authorization: Bearer <token>
생성된 토큰은 HTTP 통신을 할 때 Authorization이라는 key의 value로 사용된다!
일반적으로 value에는 "Bearer " 라는 prefix가 붙여진다.
{
"Authorization": "Bearer {생성된 JWT 토큰값}"
}
JWT 토큰 사용 방법
-
권한 서버(Authorization Server)에서는 Token의 인코딩 된 Header와 Payload 그리고 개인키(private key)를 합쳐 지정한 알고리즘으로 서명하여 JWT 토큰을 생성합니다
-
자원 서버(API 서버)에서는 요청 데이터에서 공개키(public key)를 이용하여 JWT 토큰을 복호화한 후 변조되지 않았는지 무결성을 검증합니다
-
클라이언트 측에서는 JWT 토큰을 localStorage 혹은 cookie에 저장하며 각자의 장단점이 존재합니다
- localStorage에 저장한다면 CSRF로부터 안전하고 Cookie에 저장한다면 XSS로부터 안전하다고 합니다
- 하지만 필자의 생각은 매 HTTP 통신마다 헤더에 담아 보내야 하기 때문에 cookie에 저장할 시 유리하다고 생각합니다
- Cookie에 담으면 자동으로 HTTP 요청 헤더에 담기기 때문입니다
- 로컬 스토리지에서 계속 불러올 경우 오버헤드 발생 위험이 있을 수 있다고 합니다
로그아웃 할 경우 필요한 작업
- 클라이언트 : 로컬 스토리지에 저장했다면, 로컬 스토리지에 저장된 JWT 데이터 제거
- 서버 : 쿠키에 저장했다면 쿠키에 토큰을 삭제 후, 사용했던 토큰을 blacklist DB 테이블에 넣어 해당 토큰의 접근 제한
이미지 출처 : https://mangkyu.tistory.com/56
JWT 단점 및 고려사항
1. Self-Contained 방식 사용
- 토큰 자체에 정보를 담고 있다는 것은 양날의 검이 될 수도 있습니다
2. 토큰 길이
- 토큰의 PayLoad에 3종류의 클레임을 저장하기 때문에, 데이터 정보가 많아질수록 토큰의 길이가 늘어납니다
- 따라서 JWT 토큰은 HTTP 매 요청마다 헤더에 들고 가기 때문에 크기가 클수록 네트워크에 부하를 줄 수 있습니다
3. Stateless
- JWT는 상태를 저장하지 않기 때문에 한번 생성되면 제어가 불가능합니다
- 따라서 토큰을 임의로 삭제하는 것은 불가능하므로 보안을 위해 토큰 만료시간은 꼭 설정해주어야 합니다
4. Store Token
- 생성된 JWT 토큰은 클라이언트 측에서 관리해야 하기 때문에 토큰을 클라이언트에 저장함으로써 보안에 취약합니다
- 로컬 스토리지에 저장하면 XSS 공격에 취약하고, 쿠키에 저장하면 CSRF 공격에 취약합니다
마무리
실무에서 OAuth 2.0과 JWT 토큰을 적용해보면서 모르는 내용을 공부해보았습니다
실제로 OAuth 4가지 동작 방식 중에 제대로 사용해본 방식은 3번 방식(자원 소유자 자격 증명 승인 방식)이지만, 특히 1번 방식이 OAuth의 본 의도와 가장 맞는 기본적인 방식인 것 같다는 생각이 드네요
또한 공부를 하면서 JWT 토큰이 비대칭 키를 이용해서 권한 서버에서는 secret key를 가지고 토큰 암호화를 진행하고
검증하는 각각의 API 서버(자원 서버)에서 public key로 복호화한다는 구조를 알게 돼서 좋았습니다
비록 실무에서는 이미 만들어져 있는 OAuth 모듈을 가져다 적용만 시켜봤지만, 토이 프로젝트를 하면서 직접 구성해봐야겠는 생각이 들었습니다 :)
Reference