본문으로 건너뛰기

Auth.js (Next Auth) 애플리케이션에 인증 (Authentication)을 추가하세요

이 가이드는 Auth.js (이전에는 Next Auth로 알려짐)를 사용하여 Logto를 Next.js 애플리케이션에 통합하는 방법을 보여줍니다.

팁:
  • 이 가이드에서는 Next.js 프로젝트에 Next Auth를 설정했다고 가정합니다. 설정하지 않았다면, Next Auth 문서를 확인하여 시작하세요.

사전 준비 사항​

설치​

선호하는 패키지 관리자를 통해 Auth.js를 설치하세요:

npm i next-auth@beta

자세한 내용은 Auth.js 문서 를 참조하세요.

통합​

Auth.js provider 설정​

팁:

"App Secret"은 Admin Console의 애플리케이션 세부 정보 페이지에서 찾고 복사할 수 있습니다:

App Secret

Auth.js의 API 경로 구성을 수정하고, Logto를 OIDC provider로 추가하세요:

환경 변수를 설정하세요:

AUTH_LOGTO_ISSUER=https://xxxx.logto.app/oidc
AUTH_LOGTO_ID=your-logto-app-id
AUTH_LOGTO_SECRET=your-logto-app-secret
./app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';
export const { GET, POST } = handlers;
./auth.ts
import NextAuth from 'next-auth';
import Logto from 'next-auth/providers/logto';

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [Logto],
});

그런 다음 세션을 유지하기 위해 선택적으로 미들웨어를 추가할 수 있습니다:

./middleware.ts
export { auth as middleware } from '@/auth';

자세한 내용은 Auth.js 문서를 참조하세요.

로그인 리디렉션 URI 구성​

자세한 내용을 살펴보기 전에, 최종 사용자 경험에 대한 간단한 개요를 소개합니다. 로그인 과정은 다음과 같이 단순화할 수 있습니다:

  1. 귀하의 앱이 로그인 메서드를 호출합니다.
  2. 사용자는 Logto 로그인 페이지로 리디렉션됩니다. 네이티브 앱의 경우, 시스템 브라우저가 열립니다.
  3. 사용자가 로그인하면, 다시 귀하의 앱(리디렉션 URI로 설정됨)으로 리디렉션됩니다.

리디렉션 기반 로그인에 관하여​

  1. 이 인증 과정은 OpenID Connect (OIDC) 프로토콜을 따르며, Logto는 사용자 로그인을 보호하기 위해 엄격한 보안 조치를 시행합니다.
  2. 여러 앱이 있는 경우, 동일한 아이덴티티 제공자 (Logto)를 사용할 수 있습니다. 사용자가 한 앱에 로그인하면, Logto는 사용자가 다른 앱에 접근할 때 자동으로 로그인 과정을 완료합니다.

리디렉션 기반 로그인에 대한 이론적 배경과 이점에 대해 더 알고 싶다면, Logto 로그인 경험 설명을 참조하세요.


노트:

다음 코드 스니펫에서는, 여러분의 앱이 http://localhost:3000/ 에서 실행되고 있다고 가정합니다.

Logto 콘솔의 애플리케이션 세부 정보 페이지로 전환하세요. 리디렉션 URI http://localhost:3000/api/auth/callback/logto를 추가하고 "변경 사항 저장"을 클릭하세요.

Logto 콘솔의 리디렉션 URI

로그인 및 로그아웃 구현​

로그인 및 로그아웃 버튼 구현​

app/components/sign-in.tsx
import { signIn } from '@/auth';

export default function SignIn() {
return (
<form
action={async () => {
'use server';
await signIn('logto');
}}
>
<button type="submit">Sign In</button>
</form>
);
}
app/components/sign-out.tsx
import { signOut } from '@/auth';

export function SignOut() {
return (
<form
action={async () => {
'use server';
await signOut();
}}
>
<button type="submit">Sign Out</button>
</form>
);
}

페이지에 로그인 및 로그아웃 버튼 표시​

app/page.tsx
import SignIn from './components/sign-in';
import SignOut from './components/sign-out';
import { auth } from '@/auth';

export default function Home() {
const session = await auth();

return <div>{session?.user ? <SignOut /> : <SignIn />}</div>;
}

위는 간단한 예제이며, 자세한 내용은 Auth.js 문서를 참조하세요.

체크포인트​

이제 애플리케이션을 테스트하여 인증이 예상대로 작동하는지 확인할 수 있습니다.

사용자 정보 가져오기​

사용자 정보 표시​

사용자가 로그인하면, auth()의 반환 값은 사용자의 정보를 포함하는 객체가 됩니다. 이 정보를 앱에서 표시할 수 있습니다:

app/page.tsx
import { auth } from '@/auth';

export default async function Home() {
const session = await auth();

return (
<main>
{session?.user && (
<div>
<h2>클레임 (Claims):</h2>
<table>
<thead>
<tr>
<th>이름</th>
<th>값</th>
</tr>
</thead>
<tbody>
{Object.entries(session.user).map(([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>{String(value)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
);
}

추가 클레임 요청​

auth()에서 반환된 객체에 일부 사용자 정보가 누락된 것을 발견할 수 있습니다. 이는 OAuth 2.0 및 OpenID Connect (OIDC)가 최소 권한 원칙 (PoLP)을 따르도록 설계되었기 때문이며, Logto는 이러한 표준을 기반으로 구축되었습니다.

기본적으로 제한된 클레임 (Claim)만 반환됩니다. 더 많은 정보를 원하시면, 추가적인 스코프 (Scope)를 요청하여 더 많은 클레임에 접근할 수 있습니다.

정보:

"클레임 (Claim)"은 주체에 대해 주장하는 내용이며, "스코프 (Scope)"는 클레임의 그룹입니다. 현재의 경우, 클레임은 사용자에 대한 정보입니다.

다음은 스코프 - 클레임 관계의 비규범적 예시입니다:

팁:

"sub" 클레임은 "주체"를 의미하며, 이는 사용자의 고유 식별자 (즉, 사용자 ID)입니다.

Logto SDK는 항상 세 가지 스코프를 요청합니다: openid, profile, 그리고 offline_access.

추가 스코프를 요청하려면, Logto provider의 매개변수를 구성할 수 있습니다:

./auth.ts
import NextAuth from 'next-auth';
import Logto from 'next-auth/providers/logto';

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Logto({
// ...
authorization: {
params: {
scope: 'openid offline_access profile email',
},
},
// ...
}),
],
});

네트워크 요청이 필요한 클레임​

ID 토큰의 비대화를 방지하기 위해, 일부 클레임은 네트워크 요청을 통해 가져와야 합니다. 예를 들어, custom_data 클레임은 스코프에서 요청하더라도 사용자 객체에 포함되지 않습니다. 이러한 클레임에 접근하려면, 사용자 정보를 가져오기 위해 네트워크 요청을 해야 합니다.

액세스 토큰 가져오기​

액세스 토큰을 얻을 수 있도록 NextAuth 구성을 업데이트하세요:

./auth.ts
export const { handlers, signIn, signOut, auth } = NextAuth({
// ...
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
}
return token;
},
async session({ session, token }) {
// 세션 객체에 액세스 토큰을 주입합니다
session.accessToken = token.accessToken;
return session;
},
},
});

사용자 정보 가져오기​

이제 액세스 토큰을 사용하여 OIDC 사용자 정보 엔드포인트에 접근하세요:

./app/page.tsx
// ...

export default async function Home() {
const session = await auth();
// URL을 Logto 엔드포인트로 교체하세요, `/oidc/me`로 끝나야 합니다
const response = await fetch('https://xxx.logto.app/oidc/me', {
headers: {
Authorization: `Bearer ${session?.accessToken}`,
},
});
const user = await response.json();
console.log(user);

// ...
}

위는 간단한 예제입니다. 오류 사례를 처리하는 것을 잊지 마세요.

액세스 토큰 갱신​

액세스 토큰은 짧은 기간 동안만 유효합니다. 기본적으로, Next.js는 세션이 생성될 때만 하나를 가져옵니다. 자동 액세스 토큰 갱신을 구현하려면, 리프레시 토큰 회전을 참조하세요.

스코프와 클레임​

Logto는 OIDC 스코프 (Scope) 및 클레임 (Claim) 규칙을 사용하여 ID 토큰 및 OIDC userinfo 엔드포인트에서 사용자 정보를 가져오기 위한 스코프 (Scope)와 클레임 (Claim)을 정의합니다. "스코프 (Scope)"와 "클레임 (Claim)" 모두 OAuth 2.0 및 OpenID Connect (OIDC) 명세에서 온 용어입니다.

표준 OIDC 클레임 (Claim)의 경우, ID 토큰에 포함되는지는 요청된 스코프 (Scope)에 의해 엄격하게 결정됩니다. 확장 클레임 (예: custom_data 및 organizations)은 커스텀 ID 토큰 설정을 통해 ID 토큰에 추가로 포함되도록 구성할 수 있습니다.

지원되는 스코프와 해당 클레임(Claim)의 목록은 다음과 같습니다:

표준 OIDC 스코프​

openid (기본값)

클레임(Claim) 이름타입설명
substring사용자의 고유 식별자

profile (기본값)

클레임(Claim) 이름타입설명
namestring사용자의 전체 이름
usernamestring사용자의 사용자명
picturestring최종 사용자의 프로필 사진 URL. 이 URL은 이미지 파일(예: PNG, JPEG, GIF 이미지 파일)을 가리켜야 하며, 이미지를 포함한 웹 페이지가 아니어야 합니다. 이 URL은 최종 사용자를 설명할 때 표시하기에 적합한 프로필 사진을 명확히 참조해야 하며, 최종 사용자가 임의로 촬영한 사진이 아니어야 합니다.
created_atnumber최종 사용자가 생성된 시간. 시간은 Unix epoch (1970-01-01T00:00:00Z) 이후 밀리초로 표시됩니다.
updated_atnumber최종 사용자의 정보가 마지막으로 업데이트된 시간. 시간은 Unix epoch (1970-01-01T00:00:00Z) 이후 밀리초로 표시됩니다.

기타 표준 클레임(Claim)에는 family_name, given_name, middle_name, nickname, preferred_username, profile, website, gender, birthdate, zoneinfo, locale 등이 있으며, 이들은 userinfo 엔드포인트를 요청하지 않아도 profile 스코프에 포함됩니다. 위의 클레임과의 차이점은, 이 클레임들은 값이 비어 있지 않을 때만 반환되며, 위의 클레임들은 값이 비어 있으면 null을 반환합니다.

노트:

표준 클레임(Claim)과 달리, created_at 및 updated_at 클레임은 초 단위가 아닌 밀리초 단위를 사용합니다.

email

클레임(Claim) 이름타입설명
emailstring사용자의 이메일 주소
email_verifiedboolean이메일 주소가 인증되었는지 여부

phone

클레임(Claim) 이름타입설명
phone_numberstring사용자의 전화번호
phone_number_verifiedboolean전화번호가 인증되었는지 여부

address

주소 클레임(Claim)의 세부 사항은 OpenID Connect Core 1.0 을 참조하세요.

정보:

**(기본값)**으로 표시된 스코프는 항상 Logto SDK에서 요청합니다. 표준 OIDC 스코프의 클레임(Claim)은 해당 스코프가 요청될 때 항상 ID 토큰 (ID token)에 포함되며, 비활성화할 수 없습니다.

확장 스코프​

다음 스코프는 Logto에서 확장한 것으로, userinfo 엔드포인트를 통해 클레임(Claim)을 반환합니다. 이 클레임들은 Console > Custom JWT를 통해 ID 토큰 (ID token)에 직접 포함되도록 설정할 수도 있습니다. 자세한 내용은 커스텀 ID 토큰을 참고하세요.

custom_data

클레임(Claim) 이름타입설명기본적으로 ID 토큰에 포함됨
custom_dataobject사용자의 커스텀 데이터

identities

클레임(Claim) 이름타입설명기본적으로 ID 토큰에 포함됨
identitiesobject사용자의 연결된 아이덴티티
sso_identitiesarray사용자의 연결된 SSO 아이덴티티

roles

클레임(Claim) 이름타입설명기본적으로 ID 토큰에 포함됨
rolesstring[]사용자의 역할 (Role)✅

urn:logto:scope:organizations

클레임(Claim) 이름타입설명기본적으로 ID 토큰에 포함됨
organizationsstring[]사용자가 속한 조직 (Organization) ID✅
organization_dataobject[]사용자가 속한 조직 (Organization) 데이터
노트:

이러한 조직 (Organization) 클레임(Claim)은 불투명 토큰 (Opaque token)을 사용할 때도 userinfo 엔드포인트를 통해 조회할 수 있습니다. 그러나 불투명 토큰 (Opaque token)은 조직 토큰 (Organization token)으로 사용되어 조직별 리소스에 접근할 수 없습니다. 자세한 내용은 불투명 토큰 (Opaque token)과 조직 (Organization)을 참고하세요.

urn:logto:scope:organization_roles

클레임(Claim) 이름타입설명기본적으로 ID 토큰에 포함됨
organization_rolesstring[]사용자가 속한 조직 (Organization)의 역할 (Role), 형식: <organization_id>:<role_name>✅

API 리소스​

먼저 🔐 역할 기반 접근 제어 (RBAC)를 읽어 Logto RBAC의 기본 개념과 API 리소스를 적절히 설정하는 방법을 이해하는 것을 권장합니다.

Logto 제공자 구성​

API 리소스를 설정한 후, 애플리케이션에서 Logto를 구성할 때 이를 추가할 수 있습니다:

./auth.ts
import NextAuth from 'next-auth';
import Logto from 'next-auth/providers/logto';

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Logto({
// ...
authorization: {
params: {
scope: 'openid offline_access profile email',
resource: 'https://shopping.your-app.com/api',
},
},
// ...
}),
],
});

각 API 리소스는 자체 권한 (스코프)을 가지고 있습니다.

예를 들어, https://shopping.your-app.com/api 리소스는 shopping:read 및 shopping:write 권한을 가지고 있으며, https://store.your-app.com/api 리소스는 store:read 및 store:write 권한을 가지고 있습니다.

이러한 권한을 요청하려면, 애플리케이션에서 Logto를 구성할 때 추가할 수 있습니다:

./auth.ts
import NextAuth from 'next-auth';
import Logto from 'next-auth/providers/logto';

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Logto({
// ...
authorization: {
params: {
scope: 'openid offline_access profile email shopping:read shopping:write',
resource: 'https://shopping.your-app.com/api',
},
},
// ...
}),
],
});

스코프가 API 리소스와 별도로 정의된 것을 알 수 있습니다. 이는 OAuth 2.0을 위한 리소스 지표가 요청의 최종 스코프가 모든 대상 서비스의 모든 스코프의 데카르트 곱이 될 것이라고 명시하기 때문입니다.

따라서 위의 경우, Logto에서 정의된 스코프를 단순화할 수 있으며, 두 API 리소스 모두 접두사 없이 read 및 write 스코프를 가질 수 있습니다. 그런 다음, Logto 구성에서:

./auth.ts
import NextAuth from 'next-auth';
import Logto from 'next-auth/providers/logto';

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Logto({
// ...
authorization: {
params: {
scope: 'openid offline_access profile read write',
resource: 'https://shopping.your-app.com/api',
},
},
// ...
}),
],
});

모든 API 리소스에 대해 read 및 write 스코프를 요청하게 됩니다.

노트:

API 리소스에 정의되지 않은 스코프를 요청해도 괜찮습니다. 예를 들어, API 리소스에 email 스코프가 없더라도 email 스코프를 요청할 수 있습니다. 사용 불가능한 스코프는 안전하게 무시됩니다.

성공적으로 로그인한 후, Logto는 사용자의 역할에 따라 API 리소스에 적절한 스코프를 발급합니다.

API 리소스를 위한 액세스 토큰 가져오기​

Auth.js는 리소스 매개변수 없이 액세스 토큰을 한 번만 가져옵니다. 우리는 액세스 토큰 가져오기를 직접 구현해야 합니다.

리프레시 토큰 얻기​

Logto 제공자 설정을 업데이트하고 "prompt" 매개변수를 추가하여 consent로 설정하고 offline_access 스코프가 포함되어 있는지 확인하세요:

./auth.ts
import NextAuth from 'next-auth';

export const { handlers, signIn, signOut, auth } = NextAuth({
// ...
authorization: {
params: {
prompt: 'consent',
scope: 'openid offline_access shopping:read shopping:write',
resource: 'https://shopping.your-app.com/api',
// ...
},
},
// ...
});

그런 다음 refresh_token을 세션에 저장하는 콜백을 추가하세요:

./auth.ts
export const { handlers, signIn, signOut, auth } = NextAuth({
// ...
callbacks: {
async jwt({ token, account }) {
if (account) {
// ...
token.refreshToken = account.refresh_token;
}
return token;
},
async session({ session, token }) {
// ...
session.refreshToken = token.refreshToken;
return session;
},
},
});

액세스 토큰 가져오기​

refresh_token을 사용하여 Logto의 OIDC 토큰 엔드포인트에서 액세스 토큰을 가져올 수 있습니다.

./app/page.tsx
// ...

export default async function Home() {
const session = await auth();

if (session?.refreshToken) {
// 앱 ID와 비밀을 자신의 것으로 교체하세요. "Integration" 섹션을 확인할 수 있습니다.
const basicAuth = Buffer.from('<logto-app-id>:<logto-app-secret>').toString('base64');

// URL을 자신의 Logto 엔드포인트로 교체하세요. `/oidc/token`으로 끝나야 합니다.
const response = await fetch('https://xxx.logto.app/oidc/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${basicAuth}`,
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: session.refreshToken,
resource: 'https://shopping.your-app.com/api',
}).toString(),
});

const data = await response.json();
console.log(data.access_token);
}

// ...
}

조직 토큰 가져오기​

조직이 처음이라면, 시작하기 위해 🏢 조직 (다중 테넌시)을 읽어보세요.

Logto 클라이언트를 구성할 때 urn:logto:scope:organizations 스코프를 추가해야 합니다:

./auth.ts
import NextAuth from 'next-auth';
import Logto from 'next-auth/providers/logto';

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Logto({
// ...
authorization: {
params: {
scope: 'openid offline_access urn:logto:scope:organizations',
},
},
// ...
}),
],
});

사용자가 로그인하면, 사용자에 대한 조직 토큰을 가져올 수 있습니다:

API 리소스를 위한 액세스 토큰과 유사하게, 리프레시 토큰을 사용하여 조직 액세스 토큰을 가져올 수 있습니다.

./app/page.tsx
// ...

export default async function Home() {
const session = await auth();

if (session?.refreshToken) {
// 앱 ID와 비밀을 자신의 것으로 교체하세요. "Integration" 섹션을 확인할 수 있습니다.
const basicAuth = Buffer.from('<logto-app-id>:<logto-app-secret>').toString('base64');

// URL을 Logto 엔드포인트로 교체하세요. `/oidc/token`으로 끝나야 합니다.
const response = await fetch('https://xxx.logto.app/oidc/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${basicAuth}`,
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: session.refreshToken,
resource: 'urn:logto:scope:organizations',
organization_id: 'organization-id',
}).toString(),
});

const data = await response.json();
console.log(data.access_token);
}

// ...
}

추가 읽을거리​

엔드유저 플로우: 인증 (Authentication) 플로우, 계정 플로우, 조직 플로우 커넥터 구성하기 인가 (Authorization)

Logto 통합을 NextAuth.js v4에서 v5로 마이그레이션하기