[OAuth] Github 소셜로그인
영상
OAuth 앱 만들기
복잡해 보이지만 공식 홈페이지를 보고 천천히 따라 하면 할 수 있습니다.

해당 사이트에서 로그인 후 새로운 OAuth 앱을 만듭니다.
Application name : 앱의 이름을 작성합니다.
Homepage URL : github OAuth 로그인을 시도하는 페이지를 입력합니다.
필자는 로그인 페이지에 Github 로그인 버튼이 있으므로 '/login'으로 입력했습니다.
Authorization callback URL : Github 소셜로그인 후 다시 돌아오는 페이지를 입력합니다.
Key 발급

Generate a new client secret 버튼을 눌려서 토큰을 발급받습니다.
.env 파일에 Client ID, Client Secret를 저장하고,
github에 업로드되지 않도록 .gitignore 파일에 .env 파일을 추가합니다.
# .env
GITHUB_CLIENT_ID = "~~~"
GITHUB_CLIENT_SECRET = "~~~"
이제 설정은 마쳤으며, 공식사이트를 참고하여 차근차근 만들면 됩니다.
사용자의 Github ID 요청
// Github Login Button
import Link from "next/link";
export default function SocialLogin() {
return (
<Link href="/github/start">
<span>Continue with Github</span>
</Link>
);
}
버튼을 누르면 "/github/start"로 이동하고
'/github/start'에서 GET 요청을 보냅니다.
GET https://github.com/login/oauth/authorize

// /app/github/start/route.ts
import { redirect } from "next/navigation";
// github 소셜로그인 요청
export function GET() {
const baseURL = "https://github.com/login/oauth/authorize";
const params = {
client_id: process.env.GITHUB_CLIENT_ID!,
scope: "read:user,user:email", // 다양한 기능이 있지만, 유저정보와 이메일 정보를 읽는 용도로만 사용할 것임
allow_signup: "true",
};
// 위의 params를 묶어줌
const formattedParams = new URLSearchParams(params).toString();
const finalUrl = `${baseURL}?${formattedParams}`;
return redirect(finalUrl);
}
저는 Next.JS의 Route handlers 방법을 사용하였습니다.
scope에 다양한 기능이 있지만, 유저정보와 이메일 정보를 읽는 용도로만 사용할 것이기에 위의 코드만 사용하였습니다.
scope 목록을 확인하시고, 필요하신 내용을 적용시켜 사용하시면 되겠습니다.
요청에 성공하면, 화면과 같은 창이 나타납니다.

인증하면 Github에서 처음에 Authorization callback URL 칸에 입력하신 사이트로 리디렉션 시켜줍니다.
아직 리디렉션 되는 사이트를 만들지 않았으므로 에러가 발생합니다.
Github가 사용자를 사이트로 다시 리디렉션
유저가 요청을 수락하면 코드 매개변수의 임시 code와 함께 사이트로 다시 리디렉션 합니다.
임시 코드는 10분 후에 만료됩니다.
POST https://github.com/login/oauth/access_token

응답형식

// /app/github/complete/route.ts
import { NextRequest } from "next/server";
import getGithubAccessToken from "../getGithubAccessToken";
// github 소셜로그인 요청이 성공적으로 되면 url에 코드를 가져온다.
// ex) http://localhost:3000/github/complete?code=fsafsdafas
// 코드 유효기간 : 10분
export async function GET(request: NextRequest) {
// url에 있는 code를 가져온다.
const code = request.nextUrl.searchParams.get("code");
// 코드가 없는 경우 (= 잘못된 경로로 접속하는 경우)
if (!code) {
return new Response(null, {
status: 400,
});
}
// accessToken을 가져온다.
// 유효한 코드인 경우 : 토큰 발급
// 만료된 코드인 경우 : 에러 발생
const { error, access_token } = await getGithubAccessToken(code);
if (error) {
return new Response(null, {
status: 400,
});
}
...
}
코드가 있는지 확인하고 코드가 있으면
해당 getGithubAccessToken 함수를 실행하여 유효한 코드인지 확인한 후
유효한 코드이면 토큰 발급하고, 유효하지 않은 코드이면 에러를 발생합니다.
// /app/github/getGithubAccessToken.ts
export default async function getGithubAccessToken(code: string) {
const accessTokenParams = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID!,
client_secret: process.env.GITHUB_CLIENT_SECRET!,
code,
}).toString();
// github가 사용자를 사이트로 다시 리디렉션
// 유효한 코드로 접근한 경우 : 토큰 발급
// 만료된 코드로 접근한 경우 : 에러 발생
const accessTokenURL = `https://github.com/login/oauth/access_token?${accessTokenParams}`;
const accessTokenResponse = await fetch(accessTokenURL, {
method: "POST",
headers: {
Accept: "application/json",
},
});
const { error, access_token } = await accessTokenResponse.json();
return { error, access_token };
}
AccessToken을 사용하여 API에 접근
발급받은 토큰을 사용하여, 이제 원하는 요청을 보내면 됩니다.
Authorization: Bearer OAUTH-TOKEN
GET https://api.github.com/user
github 이메일 정보 가져오기
// /app/github/getGithubEmail.ts
export default async function getGithubEmail(access_token: string) {
// fetch는 메모리에 자동으로 캐시를 저장하므로 저장되지 않게 no-cache 사용하기
const userEmailResponse = await fetch("https://api.github.com/user/emails", {
headers: {
Authorization: `Bearer ${access_token}`,
},
cache: "no-cache",
});
const data = await userEmailResponse.json();
let email = "";
for (let el of data) {
if (el.primary && el.verified) {
email = el.email;
break;
}
}
return email;
}
github 유저 프로필 정보 가져오기
// /app/github/getGithubProfile.ts
interface GithubProfileResponse {
id: number;
avatar_url: string;
login: string; // <-- username과 같다.
}
export default async function getGithubProfile(
access_token: string
): Promise<GithubProfileResponse> {
// fetch는 메모리에 자동으로 캐시를 저장하므로 저장되지 않게 no-cache 사용하기
const userProfileResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${access_token}`,
},
cache: "no-cache",
});
const { id, avatar_url, login } = await userProfileResponse.json();
return { id, avatar_url, login };
}
리디렉션 되는 페이지 ('/github/complete')
// /app/github/complete/route.ts
import db from "@/lib/db";
import UpdateSession from "@/lib/session/updateSession";
import { NextRequest } from "next/server";
import getGithubAccessToken from "../getGithubAccessToken";
import getGithubProfile from "../getGithubProfile";
import getGithubEmail from "../getGithubEmail";
import { redirect } from "next/navigation";
// github 소셜로그인 요청이 성공적으로 되면 url에 코드를 가져온다.
// ex) http://localhost:3000/github/complete?code=fsafsdafas
// 코드 유효기간 : 10분
export async function GET(request: NextRequest) {
// url에 있는 code를 가져온다.
const code = request.nextUrl.searchParams.get("code");
// 코드가 없는 경우 (= 잘못된 경로로 접속하는 경우)
if (!code) {
return new Response(null, {
status: 400,
});
}
// accessToken을 가져온다.
const { error, access_token } = await getGithubAccessToken(code);
if (error) {
return new Response(null, {
status: 400,
});
}
// github 유저 프로필 데이터 가져오기
const { id, avatar_url, login } = await getGithubProfile(access_token);
// github 유저 이메일 데이터 가져오기
const email = await getGithubEmail(access_token);
// 기존에 github로 가입한 유저인지 확인한다.
const user = await db.user.findUnique({
where: {
github_id: id + "",
},
select: {
id: true,
},
});
// 기존에 github로 가입한 유저라면
if (user) {
// 로그인 후 프로필 페이지로 이동한다.
await UpdateSession(user.id);
redirect("/profile");
}
// github 소셜로그인으로 처음 가입한 유저라면
// 계정을 새로 등록한다.
else {
const newUser = await db.user.create({
data: {
username: login + "-gh",
email,
github_id: id + "",
avatar: avatar_url,
},
select: {
id: true,
},
});
// 로그인 후 프로필 페이지로 이동한다.
await UpdateSession(newUser.id);
redirect("/profile");
}
}