똑같은 삽질은 2번 하지 말자
React vol.21 NextJS + App Router + edge runtime에서 next-auth와 cognito로 인증 시스템 구현하기 본문
React vol.21 NextJS + App Router + edge runtime에서 next-auth와 cognito로 인증 시스템 구현하기
곽빵 2024. 2. 24. 22:03개요
Cloudflare Pages + Workers를 이용해 edge runtime의 환경에서 next-auth + cognito를 이용해 인증 시스템을 구축했던 기록을 남기고자 한다.
next-auth v4는 edge runtime환경에서 사용할 수 없었다.
도입을 하려고 이것저것 조사를 했는데 edge runtime의 환경에서 NodeJS의 API을 사용할 수 있는게 제한적이다보니 next-auth에서 사용하는 NodeJS의 https, cryto등으로 인해 next-auth v4를 edge runtime에서 사용할 수 없었다.
하지만 next-auth v5(authjs)에서는 사용할 수 있었다.
아직 beta이긴 하지만 v5에서는 edge runtime에서 사용할 수 있게끔 대응을 해주고 있었으므로 이를 활용해 구축해보자.
https://authjs.dev/getting-started/introduction#flexible
Introduction | Auth.js
About Auth.js
authjs.dev
환경구축
npx create-next-app@latest
페이지 추가
/src/app/news/page.tsx
export default function News() {
return <div>News page</div>;
}
/src/app/login/page.tsx
export default function Login() {
return <div>Login page</div>;
}
News페이지는 로그인을 해야 접근할 수 있는 페이지이고
Login페이지는 물론 로그인을 하지 않아도 접근할 수 있는 페이지이어야 하는 점을 명심하자.
auth.js(next-auth)의 도입과 설정
npm i next-auth@beta
/.env
AUTH_SECRET=some-secret
- AUTH_SECRET: v4에서 NEXTAUTH_SECRET의 이름이 변경된 친구로 jwt-token의 암호화와 복호화등에 사용되며 필수로 요구되어지는 환경변수이므로 설정해둔다. 백엔드에게 유효한 jwt token임을 확인받으려면 당연히 이 환경변수도 백엔드와 공유되어야 한다.
AWS Cognito의 도입과 설정
cognito관련의 인증등의 처리를 손 쉽게 하기 위한 라이브러리 도입
npm i amazon-cognito-identity-js
/.env
# authjs
AUTH_SECRET=some-secret
# cognito
NEXT_PUBLIC_COGNITO_USER_POOL_ID=xxx
NEXT_PUBLIC_COGNITO_CLIENT_ID=xxx
- NEXT_PUBLIC_COGNITO_USER_POOL_ID: Cognito에서 UserPool을 생성할 때 자동으로 부여받는 ID
- NEXT_PUBLIC_COGNITO_CLIENT_ID: Cognito에서 UserPool을 생성할 때 자동으로 부여받는 ID
auth.js와 Cognito의 연계를 위한 타입설정
/src/types/next-auth.d.ts
import 'next-auth';
import { CognitoAccessToken, CognitoIdToken, CognitoRefreshToken } from 'amazon-cognito-identity-js';
declare module 'next-auth' {
interface Session {
idToken?: CognitoIdToken | string;
accessToken?: CognitoAccessToken | string;
error?: string;
}
interface User {
id?: string;
idToken?: CognitoIdToken | string;
accessToken?: CognitoAccessToken | string;
refreshToken?: CognitoRefreshToken | string;
expiresIn?: number;
}
interface JWT {
idToken?: CognitoIdToken | string;
accessToken?: CognitoAccessToken | string;
refreshToken?: CognitoRefreshToken | string;
expiresIn?: number;
error?: string;
}
}
declare module 'next-auth/jwt' {
interface JWT {
idToken?: CognitoIdToken | string;
accessToken?: CognitoAccessToken | string;
refreshToken?: CognitoRefreshToken | string;
expiresIn?: number;
error?: string;
}
}
authjs의 기존 Token들의 타입에 Cognito와의 연계를 위한 타입을 확장시켜놓는다. 여기서 cognito가 아닌 다른 Sass나 OAuth를 사용하는 경우에는 그에 맞는 타입을 확장시켜주면 된다.
authConfig의 작성과 Cognito Provider의 등록
/auth.config.ts
import {
AuthenticationDetails,
CognitoIdToken,
CognitoUser,
CognitoUserPool,
CognitoUserSession,
} from 'amazon-cognito-identity-js';
import type { NextAuthConfig } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
export const authConfig: NextAuthConfig = {
providers: [
CredentialsProvider({
credentials: {
email: { label: 'Email', type: 'text' },
password: { label: 'Password', type: 'password' },
},
authorize: async credentials => {
const email = (credentials?.email as string) ?? '';
const password = (credentials?.password as string) ?? '';
const userPool = new CognitoUserPool({
UserPoolId: process.env.NEXT_PUBLIC_COGNITO_USER_POOL_ID!,
ClientId: process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!,
});
const user = new CognitoUser({
Username: email,
Pool: userPool,
});
const authenticationDetails = new AuthenticationDetails({
Username: email,
Password: password,
});
const session: CognitoUserSession = await new Promise((resolve, reject) => {
user.authenticateUser(authenticationDetails, {
onSuccess: session => resolve(session),
onFailure: err => reject(err),
newPasswordRequired: function (userAttributes, requiredAttributes) {
user.completeNewPasswordChallenge(password, {}, this);
},
});
});
if (!session) {
return null;
}
return {
email,
idToken: session.getIdToken(),
accessToken: session.getAccessToken(),
refreshToken: session.getRefreshToken(),
};
},
}),
],
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 days
updateAge: 24 * 60 * 60, // 24 hours
},
pages: {
signIn: '/login',
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const callbackUrl = nextUrl.searchParams.get('callbackUrl');
if (isLoggedIn) {
if (callbackUrl) {
return Response.redirect(callbackUrl);
} else {
return true;
}
} else {
return false;
}
},
async session({ session, token }) {
session.user.id = (token.idToken as CognitoIdToken)?.payload?.sub ?? '';
session.user.email = token.email ?? '';
return session;
},
async jwt({ token, user, trigger }) {
if (user && trigger === 'signIn') {
token.idToken = user.idToken;
token.accessToken = user.accessToken;
token.refreshToken = user.refreshToken;
token.email = user.email;
}
return token;
},
},
};
- providers: NextAuth에서 사용할 인증 방식을 정의한다. 여기서는 CredentialsProvider를 사용해 Cognito의 이메일과 비밀번호 기반의 인증을 구현.
- authorize: 사용자가 제공한 이메일과 비밀번호를 사용하여 AWS Cognito를 통한 인증을 수행하는 비동기 함수이다. 성공 시 사용자 세션 정보를 반환하며, 실패하면 null을 반환하여 인증 실패를 나타냄.
- CognitoUserPool과 CognitoUser 객체를 생성하여 Cognito 서비스와의 연동을 설정
- authenticateUser 메서드를 사용하여 사용자 인증을 시도하고, 결과에 따라 세션 정보를 반환하거나 오류를 처리
- newPasswordRequired는 초기 패스워드의 강제변경요구의 상태인 유저가 로그인을 하면 인증됨 상태로 자동 변경을 해주는 역할이다.
- authorize: 사용자가 제공한 이메일과 비밀번호를 사용하여 AWS Cognito를 통한 인증을 수행하는 비동기 함수이다. 성공 시 사용자 세션 정보를 반환하며, 실패하면 null을 반환하여 인증 실패를 나타냄.
- session: JWT(Json Web Tokens) 기반 세션 관리 전략을 사용, 세션 유효 시간과 업데이트 주기를 설정
- pages: 로그인 페이지의 경로를 설정한다. 사용자가 인증되지 않았을 때 리디렉션될 페이지이다
- callbacks
- authorized: 요청이 인증되었는지 여부를 결정하는 함수이다. 사용자가 로그인된 상태인지 확인하고, 필요한 경우 콜백 URL로 리디렉션한다
- session: 세션 객체를 사용자 정보와 토큰으로 업데이트하는 함수이다. 사용자의 ID와 이메일 정보를 세션에 추가한다. (추후 useSession을 통해 현재 로그인한 유저정보습득을 위해 필요하다.)
- jwt: JWT 토큰을 사용자 정보로 업데이트하는 함수이다. 사용자가 로그인할 때, 토큰에 사용자의 ID, 액세스 토큰, 리프레시 토큰, 이메일 정보를 저장한다.
정의한 authConfig를 authjs에 설정한 뒤 필요한 인증 기능들을 추출
/src/auth.ts
import NextAuth from 'next-auth';
import { authConfig } from '../auth.config';
export const {
handlers: { GET, POST },
signIn,
signOut,
} = NextAuth(authConfig);
- GET, POST: 추후에 api/[...nextauth]/route.ts에 설정할 API들이다. useSession등을 사용할 때 next-auth가 내부적으로 request하는 api들이 있는데 이것들이 GET, POST 내부에 들어있는것으로 보인다.
- signIn: 로그인 함수
- signOut: 로그아웃 함수
추출한 signIn과 signUp을 Server에서 동작하도록 액션 작성
/src/app/_authAction/index.ts
"use server";
import { signIn, signOut } from "@/auth";
export async function login(
formData: { email: string; password: string },
provider?: "credentials"
) {
try {
await signIn(provider, formData);
} catch (error) {
throw error;
}
}
export async function logout() {
try {
await signOut();
} catch (error) {
throw error;
}
}
인증 미들웨어 작성
/src/middleware.ts
import NextAuth from "next-auth";
import { authConfig } from "../auth.config";
export default NextAuth(authConfig).auth;
export const config = {
// /api, /login, image파일들은 middleware(auth)の대상외
matcher: [
"/((?!api|login|_next/static|_next/image|images|icons|icon.ico).*)",
],
};
- NextAuth(authConfig).auth: authjs의 auth함수를 미들웨어 함수로써 등록한다. 이는 로그인이 안되어있는 상태라면 대상외 등록한 이외의 URL로 접근할 때 접근이 안되며 로그인 페이지로 리다이렉트 되도록 한다.
- matcher: 미들웨어를 적용할 부분과 적용안하는 부분을 정의하는 친구
이제 news페이지는 로그인 없이는 접근이 불가능하다.
로그인 화면 작성
/src/app/login/page.tsx
"use client";
import { login } from "@/app/_authAction";
import { useState } from "react";
export default function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async () => {
await login({
email,
password,
});
};
return (
<div className="flex flex-col gap-2 w-[300px]">
<label>
メールアドレス:
<input
type="email"
name="email"
onChange={(e) => setEmail(e.target.value)}
/>
</label>
<label>
パスワード:
<input
type="password"
name="password"
onChange={(e) => setPassword(e.target.value)}
/>
</label>
<button className="inline-block" onClick={handleLogin}>
로그인
</button>
</div>
);
}
이제 로그인을 할 수 있으며 로그인을 하지 않으면 접근할 수 없는 페이지등이 생긴다.
어느정도 인증 시스템이 구현되었다.
다음은 현재 로그인한 유저의 정보를 가져오는 글을 작성해보자