프레임워크/next.js

깃허브 소셜 로그인 구현하기

Jake Seo 2024. 4. 13. 21:30

GitHub 로그인, 회원가입 구현하기

  • 깃헙 소셜 로그인은 OAuth 표준을 따른다.
  • 다른 소셜 로긴도 OAuth 를 따르니 비슷하게 구현이 가능하다.

이 포스팅에 쓰인 코드는 노마드코더-캐럿마켓 클론코딩 에서 배운 코드를 참고했다.
단, 비슷하긴 하나, 완전히 동일하지 않고 몇몇 부분이 다르다.

GitHub 로그인, 회원가입 절차 살펴보기

picture 5

시퀀스 다이어그램으로 살펴보기

picture 0

sequenceDiagram
    participant User as 사용자
    participant App as 애플리케이션
    participant Google as 구글 서버

    User->>App: 구글 로그인 요청
    App->>Google: 인증 코드 요청 (client_id, redirect_uri, scope)
    Google->>User: 로그인 및 권한 승인 화면 표시
    User->>Google: 로그인 및 권한 승인
    Google->>App: 인증 코드 전달 (redirect_uri로)
    App->>Google: 액세스 토큰 요청 (client_id, client_secret, 인증 코드)
    Google->>App: 액세스 토큰 및 리프레시 토큰 전달
    App->>Google: 사용자 정보 요청 (액세스 토큰 사용)
    Google->>App: 사용자 정보 전달
    App->>User: 로그인 완료 및 서비스 제공

깃헙 홈페이지에서 애플리케이션 등록하기

홈페이지 링크

picture 0

여기서 등록한 앱은 추후에 Settings / Developer Settings 에서 확인할 수 있다.

OAuth 의 흐름

홈페이지 링크

사실 흐름 자체는 맨 위에 있는 APP, GITHUB 과 화살표가 있는 이미지로 이해하는 게 더 이해하기 쉽다.

  1. 사용자가 애플리케이션에 접속함
  2. 사용자를 GitHub 으로 이동시키고 인증을 요청함
  3. 사용자가 GitHub 아이디를 입력하고 인증을 완료함
  4. 사용자는 GitHub 에 의해 다시 해당 애플리케이션으로 리다이렉션됨
  5. 이제 애플리케이션은 사용자의 액세스 토큰으로 GitHub API 에 엑세스함

picture 1

1단계: 사용자를 GitHub 으로 이동시키고 인증 요청하기

  • 사용자를 깃허브의 인증 페이지로 보낸다.
  • 인증페이지로 보내면서, 내 앱의 Client ID, Scope, 기타 옵션들을 파라미터로 함께 전달한다.
GET https://github.com/login/oauth/authorize

라우터 구현하기

  • 라우터를 구현하여 사용자를 GitHub 으로 이동시키고 인증을 요청할 것이다.
    • 라우터를 구현 안하고 그냥 파라미터와 함께 사용자를 넘겨버려도 어차피 callback URL 로 돌아오게 되어있으니 사실 상관없다.
  • client_id, scope, allow_signup 을 파라미터로 제공함과 동시에 사용자를 깃허브 인증페이지로 보낸다.
  • 인증이 완료되면 깃허브 애플리케이션을 생성할 때 세팅해둔 callback URL 로 돌아오게 된다.
    • 나의 경우 아래 스크린샷에 보이듯 사용자는 http://localhost:3000/github/complete 로 돌아오게 된다.

picture 2

import { NextResponse } from "next/server";

export function GET() {
  const baseUrl =
    "https://github.com/login/oauth/authorize";

  // 파라미터의 프로퍼티와 관련 정보 링크
  // https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
  const params = {
    client_id: process.env.GITHUB_CLIENT_ID!,
    // redirect_uri: Github 페이지에서 이미 세팅했기 때문에 필요 없음,
    scope: "read:user,user:email", // 필요한 부분만 요청하기
    // 스코프 설명 관련 링크: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps
    allow_signup: "true", // GitHub 비회원이더라도 바로 가입해서 서비스를 이용할 수 있도록하기
  };

  const formattedParams = new URLSearchParams(
    params
  );

  const redirectUrl = `${baseUrl}?${formattedParams}`;

  return NextResponse.redirect(redirectUrl);
}

2단계: Code 파라미터를 AccessToken 으로 교환하기

  • 사용자는 이전에 앱에 설정해두었던 callback URL 로 돌아오게 되는데, code 라는 파라미터 값과 함께 돌아온다.

picture 3

액세스 토큰 받기

  • https://github.com/login/oauth/access_token 에 POST 요청을 보내고 위에서 받은 CODE 를 파라미터 값으로 보내면 액세스 토큰을 얻을 수 있다.
  • 깃허브 공식 문서 에 자세한 내용이 적혀있다.

picture 4

import { notFound } from "next/navigation";
import {
  NextRequest,
  NextResponse,
} from "next/server";
import axios from "axios";

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

  if (!code) {
    return notFound();
  }

  const accessTokenUrl =
    "https://github.com/login/oauth/access_token";

  const headers = {
    Accept: "application/json",
  };

  const params = {
    client_id: process.env.GITHUB_CLIENT_ID!,
    client_secret:
      process.env.GITHUB_CLIENT_SECRET!,
    code,
  };

  const accessTokenResponse = await axios.post(
    accessTokenUrl,
    params,
    {
      headers,
    }
  );

  const accessTokenData =
    accessTokenResponse.data;

  if (accessTokenData.error) {
    return NextResponse.json(
      {
        message:
          "인증에 실패하였습니다. 다시 시도해주세요",
      },
      {
        status: 400,
      }
    );
  }

  return NextResponse.json({ accessTokenData });
}

깃허브 API 에 회원정보 요청하기

  • 발급받은 액세스 토큰을 이용하여 GitHub API 에 회원정보를 요청할 수 있다.
  • 인증한 유저의 깃허브 로그인 아이디, 닉네임, 팔로워 등의 회원정보를 얻을 수 있다.
Authorization: Bearer OAUTH-TOKEN
GET https://api.github.com/user

API 로 얻은 회원정보를 통해 로그인 혹은 회원가입 처리하기

  • 받게된 회원정보를 기반으로 로그인 혹은 회원가입 처리를 하면 된다.
    • 해당 github ID 로 이미 가입된 계정이 있다면 로그인을 수행한다.
    • 해당 github ID 로 가입된 내역이 없다면, 회원가입 후 로그인을 수행한다.
  • 단, 회원가입 시에는 반드시 정책을 잘 정해야 한다.
    • 깃허브 ID 를 그대로 유저 ID 와 동일하게 사용하면 문제가 된다.
    • 디비에 GITHUB_ID 와 같은 컬럼을 만들고 거기다가 아이디를 기록하고 유저 ID 는 깃허브를 이용해 가입했음을 알 수 있는 방식으로 하는 것이 좋다.
      • 나는 아래 소스코드에서 @GITHUB_ID 와 같은 방식으로 아이디를 저장해두었다.
import {
  notFound,
  redirect,
} from "next/navigation";
import {
  NextRequest,
  NextResponse,
} from "next/server";
import axios from "axios";
import db from "@/lib/db";
import { loginByUserId } from "@/lib/session";

interface IAccessTokenData {
  access_token?: string;
  token_type?: string;
  scope?: string;
  error?: string;
  error_description?: string;
  error_url?: string;
}

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

  if (!code) {
    return notFound();
  }

  const accessTokenUrl =
    "https://github.com/login/oauth/access_token";

  const headers = {
    Accept: "application/json",
  };

  const params = {
    client_id: process.env.GITHUB_CLIENT_ID!,
    client_secret:
      process.env.GITHUB_CLIENT_SECRET!,
    code,
  };

  const accessTokenResponse = await axios.post(
    accessTokenUrl,
    params,
    {
      headers,
    }
  );

  const { data }: { data: IAccessTokenData } =
    accessTokenResponse;

  if (!data || data.error) {
    return NextResponse.json(
      {
        message:
          "인증에 실패하였습니다. 다시 시도해주세요",
      },
      {
        status: 400,
      }
    );
  }

  // fetch 를 사용하는 경우, no-cache 옵션이 필요하다.
  const { data: gitUser } = await axios.get(
    "https://api.github.com/user",
    {
      headers: {
        Authorization: `Bearer ${data.access_token}`,
      },
    }
  );

  const { id, avatar_url, login } = gitUser;

  // 가입된 회원이 있는지 찾기
  const user = await db.user.findUnique({
    where: {
      github_id: login,
    },
    select: {
      id: true,
    },
  });

  // 가입된 회원이 있다면 로그인 처리 후 메인페이지 이동
  if (user) {
    await loginByUserId(user.id);
    return redirect("/");
  }

  // 가입된 회원이 없다면 회원가입 처리 후 profile 페이지 이동
  const newUser = await db.user.create({
    data: {
      github_id: login,
      avatar: avatar_url,
      username: `@GITHUB_${id}`,
    },
  });

  await loginByUserId(newUser.id);
  return redirect("/profile");
}
반응형