# [ 살펴보기 ] Auth - OAuth2

OAuth는 Open Authorization의 약자로 third-party application ( user가 현재 사용하는 application )이 user가 사용하는 다른 service ( google, kakao 등 )가 관리하는 user의 resource에 접근할 수 있도록 도와주는 protocol을 말한다.

우리가 여러 service를 사용하며 볼 수 있는 google 로그인, kakao 로그인 등이 각 service에서 제공하는 OAuth protocol 기반 API를 통해 이루어 진다. 만약 user가 google login을 통해 인증을 완료하면 user가 사용하는 service ( application )는 완료된 인증을 위임 받아 google이 관리하는 user의 이름, email 주소 등의 resource를 요청할 수 있게 되는 것이다.

OAuth protocol은 version별로 특징이 다르며 해당 포스트는 OAuth 2에 대해 설명한다. OAuth2의 자세한 사항을 설명하기 전에 우선 OAuth 2 관련 용어를 먼저 살펴보자.

## Terminology

* Resource Owner : 이용하는 service가 필요로 하는 data의 소유자. 즉 service를 이용하는 user를 말한다.
    
* Client : Resource Server가 가지고 있는 data를 요청하는 주체를 말한다. 보통 user가 사용하는 service( application )를 말한다.
    
* Resource Server : OAuth protocol을 통해 auth 작업을 수행할 때 필요한 user data를 가지고 있는 server를 말한다. Client가 특정 data를 요청하면 요청에 필요한 access token을 검사하고 요청한 data를 제공하는 역할을 담당한다.
    
* Authorization Server : Client가 Resource Server를 통해 user data를 요청하려면 access token이 필요한데 이 때 필요한 access token을 발급 해주는 server를 말한다.
    
* Access Token : client ( application )이 resource server의 data를 요청하기 위해 필요한 token
    
* Refresh Token : access token이 만료되었을 때 새로운 access token을 발급 받기 위해 사용하는 token
    
* Scope : Client가 Resource server에 대해 어떠한 작업을 할 수 있는 지에 대한 범위를 정의 한다. Client는 정해진 scope내에서 resource를 요청할 수 있다.
    

## OAuth 2.0

Google login, KaKao Login, Naver Login 등 현재 대부분의 social login은 OAuth 2.0을 기반으로 구현된다. OAuth 2.0은 token을 기반으로 authentification을 수행하며 authorization을 위해 다양한 방법을 사용할 수 있다.

보안을 위해 요청은 HTTPS를 통해서 이루어 지고 OAuth 1 version대에 비해 구현하기가 상대적으로 쉽다.

## Grant Types

Grant란 client( application )가 resource server의 data를 요청하기 위해 필요한 authorization을 취득하기 위한 방법을 말한다. OAuth 2.0 protocol의 grant type은 여러가지가 있지만 대표적인 grant type은 다음과 같다.

### *Authorization code*

Authorization server가 access token 발급을 위해 사용할 수 있는 authorization code를 발급하는 방식이다. 예를 들어 service ( application ) user가 authorization server를 통해 authentification ( login ) 과정을 성공적으로 수행하면 authorization server는 authorization code와 함께 user를 client( application )의 특정 url로 redirection한다. Client는 해당 authorization code를 통해 access token을 발급 받아 Resource Server에 특정 data를 요청할 수 있다.

### *Client Credentials*

Client ( an applications server )가 authorization server를 통해 직접 access token을 취득하는 방식이다. 예를 들어 Cient ( an application server )에서 OAuth를 제공하기 위해 각 OAuth provider ( google, kakao 등 )에 등록한 appication 정보, configuration과 같이 특정 user와는 관계없는 data를 요구할 때는 별도의 user ( resource owner )의 개입이 필요하지 않으므로 client가 authorization server에 access token을 요청하고 해당 access token을 통해 resource server에 필요 data를 요청할 수 있다. Client는 client ID와 client secret를 통해 access token 발급에 필요한 authentication을 수행한다.

### *Implicit*

Single Page application에서 주로 사용되던 방식이며 user가 authorization server를 통해 authentication을 수행하고 성공하면 authorization server는 user를 다시 client ( application )의 특정 url로 redirect하고 access token은 redirect되는 url에 포함되어 전달된다. Access token이 URL에 바로 노출되어 보안에 취약하며 현재는 deprecated 되었다.

### ***Resource owner password credentials ( ROPC )***

Client ( application )가 직접 Resource Owner ( service user )의 credentials ( username, password 등 )을 사용해 authorization server로 부터 token을 발급 받는 방식이다. Client ( application )을 user가 완전히 신뢰할 수 있는 상황에서는 사용할 수 있겠으나 user의 credentials 정보가 client에 직접 노출되므로 보안상의 단점이 있다.

## OAuth 2.0을 통한 Google Login

이제 OAuth 2.0 Authorization code grant type을 통해 google login을 수행하는 방법을 살펴보자. Google login을 구현하기 위해선 Google Cloud Console에서 google login이 추가될 application 정보를 우선 등록해야 한다.

Google API Console에서 프로젝트를 만들거나 선택을 클릭하면 아래와 같이 모달이 뜨며 오른쪽 상단에 새 프로젝트를 생성할 수 있는 button을 통해 새로운 project를 생성한다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1734014136862/e2dec12f-e39d-41b3-8807-5d692b64c52b.png align="center")

project의 이름은 임의의 값을 넣어도 무방하다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1734012014223/610cf87c-dc7f-48bf-850b-7b988c2df182.png align="center")

생성한 project를 선택하고 다시 google cloud api 메인 페이지로 이동 후 아래 `모든 제품 보기` 버튼을 클릭해서 모든 제품 페이지로 이동한 다음 list에서 `google 인증 플랫폼`을 선택하여 이동한다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1734014319070/674bec7b-b584-4bb4-a4fc-dfba4e6a0a01.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1734014345405/114f57fb-8497-4a2f-bff6-c13369160aca.png align="center")

우선 sidebar를 통해 `개요` page로 이동하고 시작하기 버튼을 통해 google login을 적용할 application 정보 및 기타 기본 정보를 설정해준다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1734014495654/208b6904-5377-4f75-91b1-b5566eed0e01.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1734014523661/430b9767-bd4f-426d-b37d-b049326eafa9.png align="center")

대상은 외부로 설정하고 나머지 필요 정보를 기입하고 생성 버튼을 통해 생성해준다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1734014587142/e539e911-c1ca-4b69-a5b9-f59c1a616349.png align="center")

이제 왼쪽 sidebar를 통해 `브랜딩` 페이지로 이동한다. 브랜딩 페이지에서 googel logic이 추가될 application의 domain을 설정할 수 있는 section이 존재하는데 local에서 test를 하는 상황이라면 local환경에서 application이 실행되는 domain을 기입하고 ( ex - http://localhost:3000 ) 그렇지 않고 실제 배포된 application을 연동한다면 해당 application의 domain을 기입해준다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1734014718694/626382c0-9b65-4b00-961b-afa4ea88ff8a.png align="center")

왼쪽 sidebar를 통해 `대상` page로 이동해서 application를 배포되기 전에 test 환경에서 google login을 할 수 있는 test user 계정을 추가해 준다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1734015051593/21aca9b9-f591-4a7f-b489-88c588c98f28.png align="center")

왼쪽 sidebar를 통해 `데이터 엑세스` page로 이동해서 google login을 통해 user 관련 정보를 요청할 때 email, profile 정보 등 요청할 수 있는 범위를 설정한다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1734015193434/919d9aa9-2525-4ff2-aafa-4f5c53245f92.png align="center")

이제 왼쪽 sidebar를 통해 `클라이언트` page로 이동해서 클라이언트를 생성해보자. 페이지 상단 클라이언트 만들기 버튼을 통해 새로운 클라이언트를 만들 수 있다. 해당 테스트에선 web application에 google login을 적용할 예정이므로 웹 에플리케이션을 선택한다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1734015345785/ac759efd-8708-4a67-911f-4edd1dd06ed8.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1734015401183/f9cd121a-a96f-47b4-986c-11a2086fcd8c.png align="center")

여기서 승인된 Javascript는 URI은 user가 google login을 시도할 application의 URI을 기입한다. Local에서 test 중이라면 마찬가지로 application이 실행되는 localhost uri를 기입한다 ( ex - http://localhost:3000 )

그리고 승인된 리디렉션 uri는 user가 google login을 정상적으로 처리했을 때 리디렉션 될 uri을 설정한다. grant type에 따라 리디렉션이 발생할 때 uri에 access token 또는 access token을 요청할 수 있는 code가 포함 되므로 보안에 유의하자. 필자는 테스트를 위해 NextJS를 framework로 사용하고 있으므로 server side에서 실행되는 route handler를 리디렉션 uri로 설정하여 추후 과정을 처리한다. ( ex - `http://localhost:3000/api/oauth/google` )

위의 정보를 기입하고 client를 생성하면 생성한 client list를 확인할 수 있으며 우측 icon 중 client 다운로드 버튼을 누르면 client id와 client 비밀번호를 확인할 수 있다.

이제 대표적인 grant type인 Authorization code grant type을 통해 google login 과정을 살펴보자. 해당 포스트는 테스트를 위해 NextJS framework를 사용하며 Google login 구현의 세부 사항은 project의 요구 사항에 따라 다소 차이가 발생할 수 있으므로 작업 중인 project에 맞게 작업 해주자.

### Authorization code grant type을 통해 user profile 정보 요청

Authorization code grant type을 통해 google login을 통해 login한 user의 profile data를 요청하는 예제를 살펴보자. 간단한 테스트이므로 예제 코드는 NextJS의 route handler를 통해서 진행하며 google이 제공하는 googleapis package를 사용한다.

위의 예제에서 살펴본 바와 같이 Google cloud console에서 테스트를 위한 project를 생성했다고 가정하고 데이터 엑세스에서 user email과 profile이 요청할 수 있는 범위로 허용된 상태라고 가정한다.

테스트를 진행하는 application의 url은 `http://localhost:3000`이고 google oauth redirection uri은 `http://localhost:3000/api/oauth-redirect/google`로 설정하여 진행한다.

우선 googleapis package를 설치한다.

```typescript
npm install googleapis
```

이제 user가 google login 버튼을 눌렀을 때 해당 user를 google 계정으로 로그인할 수 있는 페이지로 이동 시켜줘야 한다. 그리고 google 계정으로 로그인할 수 있는 uri은 방금 설치한 `googleapis` library를 통해 생성할 수 있다. google 로그인 uri 생성을 위해선 google cloud console에서 생성한 project의 client id와 secret이 필요하므로 보안을 위해 server에서 실행되는 route handler를 구성하여 google login uri를 생성한다.

*app/api/oauth/google/route.ts*

```typescript
import { google } from "googleapis";

const oauth2Client = new google.auth.OAuth2(
  process.env.AUTH_GOOGLE_ID, // client id
  process.env.AUTH_GOOGLE_SECRET, // client secret
  process.env.AUTH_GOOGLE_REDIRECT // redirect uri
);

const scopes = [
  "https://www.googleapis.com/auth/userinfo.email",
  "https://www.googleapis.com/auth/userinfo.profile",
];

export async function GET() {
  const authorizationUrl = oauth2Client.generateAuthUrl({
    access_type: "offline",
    scope: scopes,
    include_granted_scopes: true,
  });
  return Response.json({ authorizationUrl });
}
```

위의 예제에서 볼 수 있듯이 google login을 위한 uri을 생성하기 위해 `google.auth.OAuth2` 객체를 우선 생성하고 해당 객체의 `generateAuthUrl` method를 통해 uri을 생성한다.

`google.auth.OAuth2` 객체를 생성할 때 첫 번째 argument로 project의 client id, 두 번째 argument로 project의 client secret, 그리고 마지막 argument로는 project의 redirect uri를 전달한다. 필자는 테스트를 위해 project root 경로에 `.env.local` 파일을 생성해 client id를 위한 환경변수 `AUTH_GOOGLE_ID`, client secret을 위한 환경변수 `AUTH_GOOGLE_SECRET`, redirect uri를 위한 환경변수 `AUTH_GOOGLE_REDIRECT`를 설정해둔 상태다.

이제 client side에서 위의 route handler로 request를 보내고 google login uri를 취득하여 해당 uri로 user롤 redirect 시켜준다.

```typescript
"use client";
import axios from "axios";
import { useRouter } from "next/navigation";
import React from "react";

const GooglePage = () => {
  const router = useRouter();

  const handleGoogleLogin = async () => {
    const result = await axios.get("/api/oauth/google");
    const authorizationUrl = result.data.authorizationUrl;
    router.push(authorizationUrl);
  };

  return (
    <div>
        <button onClick={handleGoogleLogin}>Google Login</button>
    </div>
  );
};

export default GooglePage;
```

위의 예제에서 Google Login을 버튼을 누르면 google login을 진행할 수 있는 page로 이동한다. 이제 login이 완료되었을 때 결과를 처리할 route handler를 추가한다. Google could console project에서 설정한 redirect uri은 `http://localhost:3000/api/oauth-redirect/google`였으므로 route handler 역시 이에 맞게 구성해준다. 혹시 NextJS route handler에 대한 추가 정보가 필요하다면 다음 documentation을 참고하자. ( *Reference* - [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) )

*app/api/oauth-redirect/google/route.ts*

```typescript
import { google } from "googleapis";
import { redirect } from "next/navigation";
import { NextRequest } from "next/server";

const oauth2Client = new google.auth.OAuth2(
  process.env.AUTH_GOOGLE_ID,
  process.env.AUTH_GOOGLE_SECRET,
  process.env.AUTH_GOOGLE_REDIRECT
);

export async function GET(request: NextRequest) {

  const searchParams = request.nextUrl.searchParams;
  const code = searchParams.get("code");
  const toString = searchParams.toString();

  console.log({ toString }); // query parameters

  try {
    const { tokens } = await oauth2Client.getToken(code!);
    oauth2Client.setCredentials(tokens);
    const oauth2 = google.oauth2({ version: "v2", auth: oauth2Client });
    const userinfo = await oauth2.userinfo.get(); // an object that includes the user data
     redirect("/home");
  } catch (err) {
    console.log({ err });
    redirect("/page-error");
  }

}
```

만약 login이 정상적으로 처리되면 redirection uri으로 설정한 uri로 redirect되고 redirect된 uri에 authorization code 정보가 담긴 code parameter가 포함된다. 해당 code를 통해 google authorization server에서 ( 위의 예제에선 `oauth2Client.getToken` method를 통해 ) access token을 발급 받을 수 있으며 발급 받은 access token을 통해 user data를 요청할 수 있다.

## OAuth 2.1

OAuth 2.1은 완전히 새로운 protocol이라고 하기 보단 기존 OAuth 2.0을 사용하는데 있어 보다 나은 보안을 위한 best practice를 적용한 version이라고 할 수 있다. 현재 개발이 계속 진행되고 있으며 OAuth 2.0의 일부 grant type은 deprecate될 예정이다.

OAuth 2.1에서 변경 될 대표적인 사항은 다음과 같다.

* Authorization code grant type에 Proof Key for Code Exchange ( PKCE ) 적용이 필수로 요구된다.
    
* 보안 관련 문제로 implicit grant flow는 deprecate된다.
    
* 마찬가지로 보안 관련 문제로 The Resource Owner Password Credentials grant flow도 deprecate된다.
    
* OAuth Server ( google, kakao 등 )에서 설정한 redirection uri과 OAuth server를 통해 authentication을 수행하기 위해 설정하는 redirection uri이 정확히 일치해야 한다. 예를 들어 위에서 살펴본 google login 예제를 기준으로 google cloud console에서 생성한 project에서 설정한 redirection uri와 로그인을 위한 uri를 생성하기 위해 `new google.auth.OAuth2` instance를 생성할 때 전달하는 redirection uri 정보가 정확히 일치해야 한다.
    
* Bearer Token ( access 또는 refresh token )이 uri의 query parameter에 포함되는 것이 금지 된다.
    

## Proof Key for Code Exchange ( PKCE )

PKCE flow는 user authentication 과정을 통해 authorizaton code를 요청한 client만 전달 받은 code를 통해 access token을 요청할 수 있도록 proof key라는 개념을 도입한다. 즉, PKCE를 적용하면 authorization code가 탈취 되어도 authorization code만으로 access token을 요청할 수 없게된다.

PKCE가 적용된 auth flow는 기존 authorization code grant type과 크게 다를 게 없지만 보안을 위해 몇 가지 사항이 추가된다. 대략적인 flow는 다음과 같다.

* Client는 random string을 기반으로 code verifier를 생성하고 code verifier를 기반으로 code challenge를 생성한다. code challenge를 생성할 때 사용할 수 있는 method는 S256과 plain이 있다.
    
* Authorization code를 요청할 때 ( 구글 로그인 할 때 ) Authorization server에 이전에 생성한 code challenge와 code challenge method를 함께 전달한다.
    
* 발급 받은 authorization code를 통해 access token을 요청할 때 authorization code와 code verifier를 authorization server에 함께 전달한다.
    
* Authorization server는 전달 받은 code verifier를 이전 단계에서 authorization code를 요청할 때 함께 보냈던 challenge mothod를 통해 변환하고 변환한 결과를 이전 단계에서 전달 받았던 code challenge와 비교하여 동일하면 access token을 발급한다.
    

## Authrization code with PKCE

이제 PKCE를 적용하여 authorization code grant type을 통한 google login을 다시 구현해보자.

우선 code verifier를 생성한다. code verifier는 길이 ( length )가 최소 43에서 최대 128인 random string을 통해 생성한다. 그리고 code verifier를 기반으로 code challenge를 생성한다. 위에서 언급 했듯이 code challenge를 생성할 때 사용할 수 있는 방법은 다음 두 가지가 있다.

* S256 : code verifier를 SHA256로 hash하고 Base64URL로 encoding한 값을 사용한다.
    
* Plain : code verifier와 같은 값을 그대로 사용한다.
    

Code challenge method는 보안을 위해 S256이 권장되지만 해당 예제는 간단하게 plain method를 사용한다. 먼저 code verifier와 code challenge를 생성하고 user가 로그인할 수 있는 uri를 생성하는 generateAuthUrl method option에 code\_challenge, code\_challenge\_method option을 추가한다.

*app/api/oauth/google/route.ts*

```typescript
import { google } from "googleapis";

const oauth2Client = new google.auth.OAuth2(
  process.env.AUTH_GOOGLE_ID,
  process.env.AUTH_GOOGLE_SECRET,
  process.env.AUTH_GOOGLE_REDIRECT
);

const scopes = [
  "https://www.googleapis.com/auth/userinfo.email",
  "https://www.googleapis.com/auth/userinfo.profile",
];

export const codeVerifier = process.env.AUTH_GOOGLE_CODE_VERIFIER; // random string

const codeChallenge = codeVerifier;

export async function GET() {

  const authorizationUrl = oauth2Client.generateAuthUrl({
    access_type: "offline",
    scope: scopes,
    include_granted_scopes: true,
    code_challenge: codeChallenge,
    code_challenge_method: "plain" as any,
  });

  return Response.json({ authorizationUrl });
}
```

그리고 redirect uri을 처리하는 handler에서 전달 받은 code를 통해 access token을 요청할 때 아래와 같이 code verifier도 함께 전달한다.

*app/api/oauth-redirect/google/route.ts*

```typescript
import { codeVerifier } from "@/app/api/oauth/google/route";
import { google } from "googleapis";
import { redirect } from "next/navigation";
import { NextRequest } from "next/server";

const oauth2Client = new google.auth.OAuth2(
  process.env.AUTH_GOOGLE_ID,
  process.env.AUTH_GOOGLE_SECRET,
  process.env.AUTH_GOOGLE_REDIRECT
);

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const code = searchParams.get("code");

  try {
    const { tokens } = await oauth2Client.getToken({
      code: code!,
      codeVerifier: codeVerifier, // code verifier
    });
    oauth2Client.setCredentials(tokens);
    const oauth2 = google.oauth2({ version: "v2", auth: oauth2Client });
    const userinfo = await oauth2.userinfo.get();
    console.log({ userinfo });
    redirect("/home");
  } catch (err) {
    console.log({ err });
    redirect("/page-error");
  }

}
```

위와 같이 수정하고 google login을 다시 시도해보면 redirect uri handler에서 code를 통해 acess token을 정상적으로 발급 받고 해당 access token을 통해 user 정보 역시 정상적으로 요청하는 것을 확인할 수 있다.

만약 redirect uri handler에서 access token을 요청할 때 code verifier 값을 gogole login을 위한 uri을 생성할 때 사용했던 code verifier와 다른 값을 사용하면 invalid\_grant라는 에러가 발생하며 access token 발급이 거부되는 것을 확인할 수 있다.
