# 패스워드 해싱을 이용한 OAuth2, JWT 토큰을 사용하는 Bearer 인증

모든 보안 흐름을 구성했으므로, 이제 <abbr title="JSON Web Tokens">JWT</abbr> 토큰과 패스워드 해싱을 사용해 애플리케이션을 안전하게 만들 것입니다.

이 코드는 실제로 애플리케이션에서 패스워드를 해싱하여 DB에 저장하는 등의 작업에 활용할 수 있습니다.

이전 장에 이어서 시작해 봅시다.

## JWT

JWT 는 "JSON Web Tokens" 을 의미합니다.

JSON 객체를 공백이 없는 긴 문자열로 인코딩하는 표준이며, 다음과 같은 형태입니다:

```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
```

JWT는 암호화되지 않아 누구든지 토큰에서 정보를 복원할 수 있습니다.

하지만 JWT는 서명되어 있습니다. 그래서 자신이 발급한 토큰을 받았을 때, 실제로 자신이 발급한게 맞는지 검증할 수 있습니다.

만료 기간이 일주일인 토큰을 발행했다고 가정해 봅시다. 다음 날 사용자가 토큰을 가져왔을 때, 그 사용자가 시스템에 여전히 로그인되어 있다는 것을 알 수 있습니다.

일주일 뒤에는 토큰이 만료될 것이고, 사용자는 인가되지 않아 새 토큰을 받기 위해 다시 로그인해야 할 것입니다. 만약 사용자(또는 제3자)가 토큰을 수정하거나 만료일을 변경하면, 서명이 일치하지 않기 때문에 알아챌 수 있을 것입니다.

만약 JWT 토큰을 다뤄보고, 작동 방식도 알아보고 싶다면 <a href="https://jwt.io/" class="external-link" target="_blank">https://jwt.io</a> 을 확인하십시오.

## `PyJWT` 설치

파이썬으로 JWT 토큰을 생성하고 검증하려면 `PyJWT` 를 설치해야 합니다.

[가상환경](../../virtual-environments.md){.internal-link target=_blank} 을 만들고 활성화한 다음 `pyjwt` 를 설치하십시오:

<div class="termy">

```console
$ pip install pyjwt

---> 100%
```

</div>

/// info | 참고

RSA나 ECDSA 같은 전자 서명 알고리즘을 사용하려면, `pyjwt[crypto]`라는 암호화 라이브러리 의존성을 설치해야 합니다.

더 자세한 내용은 <a href="https://pyjwt.readthedocs.io/en/latest/installation.html" class="external-link" target="_blank">PyJWT 설치</a> 에서 확인할 수 있습니다.

///

## 패스워드 해싱

"해싱(Hashing)"은 어떤 내용(여기서는 패스워드)을 해석할 수 없는 일련의 바이트 집합(단순 문자열)으로 변환하는 것을 의미합니다.

동일한 내용(똑같은 패스워드)을 해싱하면 동일한 문자열을 얻습니다.

하지만 그 문자열을 다시 패스워드로 되돌릴 수는 없습니다.

### 패스워드를 해싱하는 이유

데이터베이스를 탈취당하더라도, 침입자는 사용자의 평문 패스워드 대신 해시 값만 얻을 수 있습니다.

따라서 침입자는 훔친 사용자 패스워드를 다른 시스템에서 활용할 수 없습니다. (대다수 사용자가 여러 시스템에서 동일한 패스워드를 사용하기 때문에 평문 패스워드가 유출되면 위험합니다.)

## `passlib` 설치

PassLib는 패스워드 해시를 다루는 훌륭한 파이썬 패키지입니다.

많은 안전한 해시 알고리즘과 도구들을 지원합니다.

추천하는 알고리즘은 "Bcrypt"입니다.

[가상환경](../../virtual-environments.md){.internal-link target=_blank} 을 만들고 활성화한 다음 PassLib와 Bcrypt를 설치하십시오:

<div class="termy">

```console
$ pip install "passlib[bcrypt]"

---> 100%
```

</div>

/// tip | 팁

`passlib`를 사용하여, **Django**, **Flask** 의 보안 플러그인이나 다른 도구로 생성한 패스워드를 읽을 수 있도록 설정할 수도 있습니다.

예를 들자면, FastAPI 애플리케이션과 Django 애플리케이션이 같은 데이터베이스에서 데이터를 공유할 수 있습니다. 또는 같은 데이터베이스를 사용하여 Django 애플리케이션을 점진적으로 마이그레이션 할 수도 있습니다.

그리고 사용자는 FastAPI 애플리케이션과 Django 애플리케이션에 동시에 로그인할 수 있습니다.

///

## 패스워드의 해시와 검증

필요한 도구를 `passlib`에서 임포트합니다.

PassLib "컨텍스트(context)"를 생성합니다. 이것은 패스워드를 해싱하고 검증하는데 사용합니다.

/// tip | 팁

PassLib 컨텍스트는 다양한 해싱 알고리즘을 사용할 수 있는 기능을 제공하며, 더 이상 사용이 권장되지 않는 오래된 해싱 알고리즘을 검증하는 기능도 포함되어 있습니다.

예를 들어, 다른 시스템(Django 같은)에서 생성한 패스워드를 읽고 검증할 수 있으며, 새로운 패스워드를 Bcrypt 같은 다른 알고리즘으로 해싱할 수도 있습니다.

그리고 동시에 그런 모든 알고리즘과 호환성을 유지합니다.

///

사용자로부터 받은 패스워드를 해싱하는 유틸리티 함수를 생성합니다.

그리고 받은 패스워드가 저장된 해시와 일치하는지 검증하는 또 다른 유틸리티 함수도 생성합니다.

그리고 사용자를 인증하고 반환하는 또 다른 함수도 생성합니다.

{* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,56:57,60:61,70:76] *}

/// note

새로운 (가짜) 데이터베이스 `fake_users_db`를 확인하면, 해시 처리된 패스워드가 어떻게 생겼는지 볼 수 있습니다: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`.

///

## JWT 토큰 처리

설치된 모듈을 임포트 합니다.

JWT 토큰 서명에 사용될 임의의 비밀키를 생성합니다.

안전한 임의의 비밀키를 생성하려면 다음 명령어를 사용하십시오:

<div class="termy">

```console
$ openssl rand -hex 32

09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
```

</div>

그리고 생성한 비밀키를 복사해 변수 `SECRET_KEY`에 대입합니다. (이 예제의 변수 값을 그대로 사용하지 마십시오.)

JWT 토큰을 서명하는 데 사용될 알고리즘을 위한 변수 `ALGORITHM` 을 생성하고 `"HS256"` 으로 설정합니다.

토큰 만료 기간을 위한 변수를 생성합니다.

응답을 위한 토큰 엔드포인트에 사용될 Pydantic 모델을 정의합니다.

새 액세스 토큰을 생성하기 위한 유틸리티 함수를 생성합니다.

{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *}

## 의존성 수정

`get_current_user` 함수를 이전과 동일한 토큰을 받도록 수정하되, 이번에는 JWT 토큰을 사용하도록 합니다.

받은 토큰을 디코딩하여 검증한 후 현재 사용자를 반환합니다.

토큰이 유효하지 않다면 HTTP 오류를 반환합니다.

{* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *}

## `/token` 경로 작업 수정

토큰의 만료 시각을 설정하기 위해 `timedelta` 를 생성합니다.

실제 JWT 액세스 토큰을 생성하여 반환합니다.

{* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *}

### JWT "주체(subject)" `sub`에 대한 기술 세부 사항

JWT 명세에 따르면 토큰의 주체를 포함하는 `sub`라는 키가 있습니다.

사용 여부는 선택사항이지만, 사용자의 식별 정보를 저장할 수 있으므로 여기서는 이를 사용합니다.

JWT는 사용자를 식별하고 사용자가 API를 직접 사용할 수 있도록 허용하는 것 외에도 다른 용도로 사용될 수도 있습니다.

예를 들어 "자동차"나 "블로그 게시물"을 식별하는 데 사용할 수 있습니다.

그리고 "자동차를 운전하다"나 "블로그 게시물을 수정하다"처럼 해당 엔터티에 대한 권한을 추가할 수 있습니다.

그 후 이 JWT 토큰을 사용자(또는 봇)에게 제공하면, 그들은 계정을 따로 만들 필요 없이 API가 생성한 JWT 토큰만으로 작업(자동차 운전 또는 블로그 게시물 편집)을 수행할 수 있습니다.

이러한 개념을 활용하면 JWT는 훨씬 더 복잡한 시나리오에도 사용할 수 있습니다.

이 경우 여러 엔터티가 동일한 ID를 가질 수 있습니다. 예를 들어 foo라는 ID를 가진 사용자, 자동차, 블로그 게시물이 있을 수 있습니다.

그래서 ID 충돌을 방지하기 위해, 사용자의 JWT 토큰을 생성할 때 접두사로 `sub` 키를 추가할 수 있습니다. 예를 들어 `username:` 을 붙이는 방식입니다. 이 예제에서는 `sub` 값이 `username:johndoe`이 될 수 있습니다.

가장 중요한 점은 `sub` 키는 전체 애플리케이션에서 고유한 식별자가 되어야 하며 문자열이어야 한다는 점입니다.

## 확인해봅시다

서버를 실행하고 문서로 이동하십시오: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.

다음과 같은 사용자 인터페이스를 볼 수 있습니다:

<img src="/img/tutorial/security/image07.png">

이전과 같은 방법으로 애플리케이션에 인증하십시오.

다음 인증 정보를 사용하십시오:

Username: `johndoe`
Password: `secret`

/// check

코드 어디에도 평문 패스워드 "`secret`" 이 없다는 점에 유의하십시오. 해시된 버전만 있습니다.

///

<img src="/img/tutorial/security/image08.png">

`/users/me/` 를 호출하면 다음과 같은 응답을 얻을 수 있습니다:

```JSON
{
  "username": "johndoe",
  "email": "johndoe@example.com",
  "full_name": "John Doe",
  "disabled": false
}
```

<img src="/img/tutorial/security/image09.png">

개발자 도구를 열어보면 전송된 데이터에 토큰만 포함된 것을 확인할 수 있습니다. 패스워드는 사용자를 인증하고 액세스 토큰을 받기 위한 첫 번째 요청에만 전송되며, 이후에는 전송되지 않습니다:

<img src="/img/tutorial/security/image10.png">

/// note

`Bearer `로 시작하는 `Authorization` 헤더에 주목하십시오.

///

## `scopes` 의 고급 사용법

OAuth2는 "스코프(scopes)" 라는 개념을 갖고 있습니다.

이를 사용하여 JWT 토큰에 특정 권한 집합을 추가할 수 있습니다.

그 후 이 토큰을 사용자에게 직접 제공하거나 제3자에게 제공하여, 특정 제한사항 하에있는 API와 통신하도록 할 수 있습니다.

**FastAPI** 에서의 사용 방법과 통합 방식은 **심화 사용자 안내서** 에서 자세히 배울 수 있습니다.

## 요약

지금까지 살펴본 내용을 바탕으로, OAuth2와 JWT 같은 표준을 사용하여 안전한 **FastAPI** 애플리케이션을 만들 수 있습니다.

거의 모든 프레임워크에서 보안 처리는 상당히 복잡한 주제입니다.

이를 단순화하는 많은 패키지는 데이터 모델, 데이터베이스, 사용 가능한 기능들에 대해 여러 제약이 있습니다. 그리고 지나치게 단순화하는 일부 패키지들은 심각한 보안 결함을 가질 수도 있습니다.

---

**FastAPI** 는 어떤 데이터베이스, 데이터 모델, 도구도 강요하지 않습니다.

프로젝트에 가장 적합한 것을 선택할 수 있는 유연성을 제공합니다.

그리고 `passlib` 와 `PyJWT` 처럼 잘 관리되고 널리 사용되는 패키지들을 바로 사용할 수 있습니다. **FastAPI** 는 외부 패키지 통합을 위해 복잡한 메커니즘이 필요하지 않기 때문입니다.

그러나 유연성, 견고성, 보안성을 해치지 않으면서 과정을 단순화할 수 있는 도구들을 제공합니다.

그리고 OAuth2와 같은 표준 프로토콜을 비교적 간단한 방법으로 구현하고 사용할 수 있습니다.

더 세분화된 권한 체계를 위해 OAuth2의 "스코프"를 사용하는 방법은 **심화 사용자 안내서**에서 더 자세히 배울 수 있습니다. OAuth2의 스코프는 제3자 애플리케이션이 사용자를 대신해 그들의 API와 상호작용하도록 권한을 부여하기 위해, Facebook, Google, GitHub, Microsoft, X (Twitter) 등의 많은 대형 인증 제공업체들이 사용하는 메커니즘입니다.
