1. 깃헙으로 로그인하는 방법을 배워보자
* GitHub Authorizing OAuth apps
다른 사용자가 OAuth app에 권한을 부여하도록 설정할 수 있습니다.
https://docs.github.com/ko/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
아래 url 접속하여, OAuth 계정을 생성하자.
https://github.com/settings/applications/new
GitHub: Let’s build from here
GitHub is where over 100 million developers shape the future of software, together. Contribute to the open source community, manage your Git repositories, review code like a pro, track bugs and fea...
github.com
생성 단계에서 홈페이지주소와 콜백 url은 아래와 같이 세팅했다.
그 후, client id와 secret부분은 env파일에 저장하자
이제 내 서비스에서 깃헙 로그인 버튼을 누르면, /github/start 로 이동하게 된다. 이 url로 이동하게 된 후, http method가 발동 되어야 하는데, 이는 아래와 같이 route 파일에서 관리가 가능하다
*app/github/start/route.ts
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",
};
const formattedParams = new URLSearchParams(params).toString();
const finalUrl = `${baseURL}?${formattedParams}`;
return Response.redirect(finalUrl);
}
1) scope란 해당 서비스에 어떤 권한까지 주는지에 대한 범위에 대한 것이다. (타 0auth서비스에서는 permission라고도함)
2) params는 공식문서 보고 정하면 됨
*middleware.ts
import { NextRequest, NextResponse } from "next/server";
import getSession from "./lib/session";
interface Routes {
[key: string]: boolean;
}
const publicOnlyUrls: Routes = {
"/": true,
"/login": true,
"/sms": true,
"/create-account": true,
"/github/start": true,
"/github/complete": true,
};
export async function middleware(request: NextRequest) {
const session = await getSession();
const exists = publicOnlyUrls[request.nextUrl.pathname];
if (!session.id) {
if (!exists) {
return NextResponse.redirect(new URL("/", request.url));
}
} else {
if (exists) {
return NextResponse.redirect(new URL("/products", request.url));
}
}
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
1) 퍼블릭 접근을 위해, github/start와 github/complete을 추가해 주었다.
이어서, start화면에서 github 로그인 승인이 되면 github/complete로 리다이렉팅되게 해두었다. (
*app/github/complete/route.ts
import { notFound } from "next/navigation";
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
const code = request.nextUrl.searchParams.get("code");
if (!code) {
return notFound();
}
const accessTokenParams = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID!,
client_secret: process.env.GITHUB_CLIENT_SECRET!,
code,
}).toString();
const accessTokenURL = `https://github.com/login/oauth/access_token?${accessTokenParams}`;
const accessTokenResponse = await fetch(accessTokenURL, {
method: "POST",
headers: {
Accept: "application/json",
},
});
const accessTokenData = await accessTokenResponse.json();
if ("error" in accessTokenData) {
return new Response(null, {
status: 400,
});
}
return Response.json({ accessTokenData });
}
1) url 마지막 부분에 code 값을 주는데, 이것은 유효기간이 10분이다(공식홈페이지 내용에 있음)
2) 공식 홈페이지 내용대로, POST로 데이터를 보내보고 토큰값을 얻어오자.
위 로직을 이용하여, github 데이터를 가지고 온 후, 해당 데이터를 db에 저장 및 최종 로그인 처리해주자.
*/github/complete.ts
import db from "@/lib/db";
import getSession from "@/lib/session";
import { notFound, redirect } from "next/navigation";
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
const code = request.nextUrl.searchParams.get("code");
if (!code) {
return new Response(null, {
status: 400,
});
}
const accessTokenParams = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID!,
client_secret: process.env.GITHUB_CLIENT_SECRET!,
code,
}).toString();
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();
if (error) {
return new Response(null, {
status: 400,
});
}
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();
const user = await db.user.findUnique({
where: {
github_id: id + "",
},
select: {
id: true,
},
});
if (user) {
const session = await getSession();
session.id = user.id;
await session.save();
return redirect("/profile");
}
const newUser = await db.user.create({
data: {
username: login,
github_id: id + "",
avatar: avatar_url,
},
select: {
id: true,
},
});
const session = await getSession();
session.id = newUser.id;
await session.save();
return redirect("/profile");
}
1) https://api.github.com/user 대신에 https://api.github.com/emails 를 하면, scope에 email을 허용했기 때문에 email 주소를 가지고 올 수 있다.
2) 만약 github에서 가지고온 username (login) 이 이미 db에 있다면, 중복 체크를 하고 중복이 된다면 에러 메시지를 보여주거나, 아이디 + gh 같이 추가로 아이디명을 강제로 변경 시켜주면 된다. (위 코드에는 적용 안됨)
3) const session = await getSession();
session.id = newUser.id;
await session.save();
이 코드 부분이 계속 중복 된다. 추 후 실제 서비스에서는 별도 함수로 만들어줘서 처리해주자.
4) 코드가 길어지니, 추 후 실제 서비스에서는 토큰값 가져오는거, 유저프로필 가져오는거, 이메일 가져오는거 같은 것들을 별도 lib 폴더에 넣어두자.
2. SMS Token
twilio 서비스를 이용해서 sms 토큰 서비스를 이용한 로그인을 진행해보자.
twilio는 체험판으로 일부 돈을 준다. 해당 돈으로, 지정된 번호로 메시지를 보내보는 테스트를 할 수 있다.
(발신자 번호: 사야함 / 수신자 번호 : 지정번호(테스트용))
현재 내 계정은 체험판으로 아래와 같은 계정정보가 있다. 해당 정보를 .env에 담아두자
*sms/actions.ts
"use server";
import twilio from "twilio";
import crypto from "crypto";
import { z } from "zod";
import validator from "validator";
import { redirect } from "next/navigation";
import db from "@/lib/db";
import getSession from "@/lib/session";
const phoneSchema = z
.string()
.trim()
.refine(
(phone) => validator.isMobilePhone(phone, "ko-KR"),
"Wrong phone format"
);
async function tokenExists(token: number) {
const exists = await db.sMSToken.findUnique({
where: {
token: token.toString(),
},
select: {
id: true,
},
});
return Boolean(exists);
}
const tokenSchema = z.coerce
.number()
.min(100000)
.max(999999)
.refine(tokenExists, "This token does not exist.");
interface ActionState {
token: boolean;
}
async function getToken() {
const token = crypto.randomInt(100000, 999999).toString();
const exists = await db.sMSToken.findUnique({
where: {
token,
},
select: {
id: true,
},
});
if (exists) {
return getToken();
} else {
return token;
}
}
export async function smsLogIn(prevState: ActionState, formData: FormData) {
const phone = formData.get("phone");
const token = formData.get("token");
if (!prevState.token) {
const result = phoneSchema.safeParse(phone);
if (!result.success) {
return {
token: false,
error: result.error.flatten(),
};
} else {
await db.sMSToken.deleteMany({
where: {
user: {
phone: result.data,
},
},
});
const token = await getToken();
await db.sMSToken.create({
data: {
token,
user: {
connectOrCreate: {
where: {
phone: result.data,
},
create: {
username: crypto.randomBytes(10).toString("hex"),
phone: result.data,
},
},
},
},
});
const client = twilio(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);
await client.messages.create({
body: `Your Karrot verification code is: ${token}`,
from: process.env.TWILIO_PHONE_NUMBER!,
to: process.env.MY_PHONE_NUMBER!,
});
return {
token: true,
};
}
} else {
const result = await tokenSchema.spa(token);
if (!result.success) {
return {
token: true,
error: result.error.flatten(),
};
} else {
const token = await db.sMSToken.findUnique({
where: {
token: result.data.toString(),
},
select: {
id: true,
userId: true,
},
});
const session = await getSession();
session.id = token!.userId;
await session.save();
await db.sMSToken.delete({
where: {
id: token!.id,
},
});
redirect("/profile");
}
}
}
1) 코드 플로우
모바일 번호 입력 부분
1. 화면에 입력한 모바일 넘버에 대해 token db 조회 및 모바일 번호가 연결된 유저가 있다면, db에서 해당 토큰 값 삭제
(모바일 넘버 까지 입력하면 해당 유저 모델 db에 저장됨, 토큰 값 잘못입력해도 해당 유저는 토큰값이 남아 있는 상태로 계속 저장되어 있음 / 토큰 값 있는 유저다 --> 모바일 넘버는 입력했으나, 토큰에서 막혀서 최종 회원 가입 성공 못함)
2. 토큰 값 가져옴, 토큰 값은 db조회 후 있으면 다시 토큰 값(난수) 생성, 없으면 해당 토큰 값 리턴
3. 해당 토큰 값 db에 저장 및 유저 모델과 연결
4. twilio 통해 토큰값 보내기 및 해당 함수 리턴값 token: true
토큰값 입력 부분
5. zod를 활용해서 토큰값 db에 있는지 조회, 없으면 에러 메시지 보여주기
6. 토큰값 있으면, 세션에 저장
7. 토큰 데이터 삭제 및 로그인 처리 (리다이렉트)
**위 로직에서 문제는, 토큰값을 입력할 때 다른 모바일 번호로 등록되어 있는 토큰값을 사용하는 것인데, 사실 이럴 가능성은 매우 낮다. 만약 이게 불안하면, 토큰 값 db조회 직전, 입력된 모바일 넘버에 대해 db를 조회하는 로직을 추가해주면 된다. (안해도 될듯)
'코딩강의 > [풀스택]캐럿마켓 클론코딩(노마드코더)' 카테고리의 다른 글
#11 Product Upload (0) | 2024.07.31 |
---|---|
#10 Products (0) | 2024.07.29 |
#8 Authentication (0) | 2024.07.28 |
#7 Prisma (0) | 2024.07.26 |
#6 Validation (0) | 2024.07.25 |