전통적인 웹 애플리케이션에 인증 (Authentication)을 추가하세요
이 가이드는 Admin Console에서 "Traditional web" 유형의 애플리케이션을 생성했다고 가정합니다.
귀하의 앱은 Django, Laravel 등과 같은 프레임워크를 사용하여 브라우저 대신 서버 측에서 실행될 수 있습니다. 이를 전통적인 웹 앱이라고 합니다. 이 페이지에서 적절한 SDK를 찾을 수 없다면, 수동으로 통합해야 할 것입니다.
이 문서는 단계별로 완료하는 방법을 안내합니다. Node.js의 Express를 예로 들어 설명합니다.
이 문서는 Express에만 국한되지 않으며, 다른 프레임워크나 심지어 다른 언어를 사용하는 경우에도 @logto/js
를 해당 언어의 코어 SDK로 대체하고 일부 단계를 조정할 수 있습니다.
소스 코드 가져오기
이 가이드의 최종 코드는 GitHub에서 확인할 수 있습니다.
Express 프로젝트 시작하기
express-generator
를 사용하여 Express 프로젝트를 빠르게 시작할 수 있습니다.
mkdir express-logto
cd express-logto
npx express-generator
종속성 설치하기
데모 앱에는 4개의 종속성이 필요합니다:
- @logto/js: Logto의 JavaScript용 코어 SDK.
- node-fetch: Node.js 런타임에서
window.fetch
와 호환되는 API를 위한 최소 코드. - express-session: 세션 미들웨어로, 사용자 토큰을 저장하기 위해 세션을 사용할 것입니다.
- js-base64: 또 다른 Base64 트랜스코더.
- npm
- Yarn
- pnpm
npm i @logto/js node-fetch@v2 express-session js-base64
yarn add @logto/js node-fetch@v2 express-session js-base64
pnpm add @logto/js node-fetch@v2 express-session js-base64
세션 사용하기
사용자가 로그인하면 액세스 토큰, ID 토큰, 리프레시 토큰과 상호작용 데이터를 받게 되며, 세션은 이를 저장하기에 훌륭한 장소입니다.
이전 단계에서 express-session을 설치했으므로, 이제 다음 코드를 추가하여 설정해 보겠습니다:
// app.js
const session = require('express-session');
app.use(
session({
secret: 'keyboard cat', // 자신의 비밀 키로 변경하세요
cookie: { maxAge: 86400 },
})
);
사용자를 인증하는 함수 구현하기
다음 코드 스니펫에서는 애플리케이션이 http://localhost:3000
에서 실행된다고 가정합니다.
이 단계에서는 다음 인증 함수를 구현해야 합니다:
getSignInUrl
: 사용자가 리디렉션될 Logto 인가 서버의 완전한 URL을 생성하고 반환합니다.handleSignIn
: 인증 과정이 완료된 후 콜백 URL을 파싱하고, 코드 쿼리 매개변수를 가져와 토큰 (액세스 토큰, 리프레시 토큰, ID 토큰)을 가져와 로그인 과정을 완료합니다.refreshTokens
: 리프레시 토큰을 사용하여 새로운 액세스 토큰을 교환합니다.
"App Secret"은 Admin Console의 애플리케이션 세부 정보 페이지에서 찾고 복사할 수 있습니다:

// logto.js
const {
withReservedScopes,
fetchOidcConfig,
discoveryPath,
generateSignInUri,
verifyAndParseCodeFromCallbackUri,
fetchTokenByAuthorizationCode,
fetchTokenByRefreshToken,
} = require('@logto/js');
const fetch = require('node-fetch');
const { randomFillSync, createHash } = require('crypto');
const { fromUint8Array } = require('js-base64');
const config = {
endpoint: 'https://logto.dev',
appId: 'foo',
appSecret: '<your-app-secret-copied-from-console>',
redirectUri: 'http://localhost:3000/callback', // 앱의 프로덕션 주소로 대체해야 할 수 있습니다
scopes: withReservedScopes().split(' '),
};
const requester = (input, init) => {
const { appId, appSecret } = config;
return fetch(input, {
...init,
headers: {
...init?.headers,
authorization: `Basic ${Buffer.from(`${appId}:${appSecret}`, 'utf8').toString('base64')}`,
},
});
};
const generateRandomString = (length = 64) => {
return fromUint8Array(randomFillSync(new Uint8Array(length)), true);
};
const generateCodeChallenge = async (codeVerifier) => {
const encodedCodeVerifier = new TextEncoder().encode(codeVerifier);
const hash = createHash('sha256');
hash.update(encodedCodeVerifier);
const codeChallenge = hash.digest();
return fromUint8Array(codeChallenge, true);
};
const getOidcConfig = async () => {
return fetchOidcConfig(new URL(discoveryPath, config.endpoint).toString(), requester);
};
exports.getSignInUrl = async () => {
const { authorizationEndpoint } = await getOidcConfig();
const codeVerifier = generateRandomString();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = generateRandomString();
const { redirectUri, scopes, appId: clientId } = config;
const signInUri = generateSignInUri({
authorizationEndpoint,
clientId,
redirectUri: redirectUri,
codeChallenge,
state,
scopes,
});
return { redirectUri, codeVerifier, state, signInUri };
};
exports.handleSignIn = async (signInSession, callbackUri) => {
const { redirectUri, state, codeVerifier } = signInSession;
const code = verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
const { appId: clientId } = config;
const { tokenEndpoint } = await getOidcConfig();
const codeTokenResponse = await fetchTokenByAuthorizationCode(
{
clientId,
tokenEndpoint,
redirectUri,
codeVerifier,
code,
},
requester
);
return codeTokenResponse;
};
exports.refreshTokens = async (refreshToken) => {
const { appId: clientId, scopes } = config;
const { tokenEndpoint } = await getOidcConfig();
const tokenResponse = await fetchTokenByRefreshToken(
{
clientId,
tokenEndpoint,
refreshToken,
scopes,
},
requester
);
return tokenResponse;
};
로그인
세부 사항을 살펴보기 전에, 최종 사용자 경험에 대한 간단한 개요를 소개합니다. 로그인 과정은 다음과 같이 간소화될 수 있습니다:
- 귀하의 앱이 로그인 메서드를 호출합니다.
- 사용자는 Logto 로그인 페이지로 리디렉션됩니다. 네이티브 앱의 경우, 시스템 브라우저가 열립니다.
- 사용자가 로그인하고 귀하의 앱으로 다시 리디렉션됩니다 (리디렉션 URI로 구성됨).
리디렉션 기반 로그인에 관하여
- 이 인증 과정은 OpenID Connect (OIDC) 프로토콜을 따르며, Logto는 사용자 로그인을 보호하기 위해 엄격한 보안 조치를 시행합니다.
- 여러 앱이 있는 경우, 동일한 아이덴티티 제공자 (Logto)를 사용할 수 있습니다. 사용자가 한 앱에 로그인하면, Logto는 사용자가 다른 앱에 접근할 때 자동으로 로그인 과정을 완료합니다.
리디렉션 기반 로그인에 대한 이론적 배경과 이점에 대해 더 알고 싶다면, Logto 로그인 경험 설명을 참조하세요.
리디렉션 URI 구성하기
Logto 콘솔의 애플리케이션 세부 정보 페이지로 전환하세요. 리디렉션 URI http://localhost:3000/callback
를 추가하고 "변경 사항 저장"을 클릭하세요.
로그인 경로 구현하기
getSignInUrl()
을 호출하기 전에, Admin Console 에서 Redirect URI 를 올바르게 구성했는지
확인하세요. :::
Express에서 로그인 경로를 생성하세요:
const { getSignInUrl } = require('./logto');
app.get('/sign-in', async (req, res) => {
const { redirectUri, codeVerifier, state, signInUri } = await getSignInUrl();
req.session.signIn = { codeVerifier, state, redirectUri };
res.redirect(signInUri);
});
그리고 콜백을 처리하는 경로를 생성하세요:
app.get('/callback', async (req, res) => {
if (!req.session.signIn) {
res.send('잘못된 요청입니다.');
return;
}
const response = await handleSignIn(
req.session.signIn,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
req.session.tokens = {
...response,
expiresAt: response.expiresIn + Date.now(),
idToken: decodeIdToken(response.idToken),
};
req.session.signIn = null;
res.redirect('/');
});
로그아웃
TODO: 사용자 참조의 "세션 & 쿠키" 챕터로 링크하세요.
이 애플리케이션에서 사용자를 로그아웃하려면 세션에서 토큰을 지울 수 있습니다. "로그아웃"에 대해 더 읽으려면 이 링크를 확인하세요.
app.get('/sign-out', (req, res) => {
req.session.tokens = null;
res.send('로그아웃 성공');
});
보호된 리소스에 접근하기
withAuth
라는 미들웨어를 생성하여 req
에 auth
객체를 첨부하고 사용자가 로그인했는지 확인합니다.
// auth.js
const { decodeIdToken } = require('@logto/js');
const { refreshTokens } = require('./logto');
const withAuth =
({ requireAuth } = { requireAuth: true }) =>
async (req, res, next) => {
if (requireAuth && !req.session.tokens) {
res.redirect('/sign-in');
return;
}
if (req.session.tokens) {
if (req.session.tokens.expiresAt >= Date.now()) {
// 액세스 토큰이 만료되었으므로, 새로운 토큰으로 갱신합니다
try {
const response = await refreshTokens(req.session.tokens.refreshToken);
req.session.tokens = {
...response,
expiresAt: response.expiresIn + Date.now(),
idToken: decodeIdToken(response.idToken),
};
} catch {
// 교환 실패, 로그인으로 리디렉션
res.redirect('/sign-in');
return;
}
}
req.auth = req.session.tokens.idToken.sub;
}
next();
};
module.exports = withAuth;
index
페이지를 생성하여 게스트에게는 로그인 링크를, 이미 로그인한 사용자에게는 프로필로 이동하는 링크를 표시합니다:
router.get('/', withAuth({ requireAuth: false }), function (req, res, next) {
res.render('index', { auth: Boolean(req.auth) });
});
extends layout
block content
h1 Hello logto
if auth
p: a(href="/user") 프로필로 이동
else
p: a(href="/sign-in") 여기를 클릭하여 로그인
user
페이지를 생성하여 userId
(주체
)를 표시합니다:
app.get('/user', withAuth(), (req, res, next) => {
res.render('user', { userId: req.auth });
});
extends layout
block content
h1 Hello logto
p userId: #{userId}
추가 읽기 자료
최종 사용자 흐름: 인증 흐름, 계정 흐름, 및 조직 흐름 커넥터 구성 API 보호GPT 액션에서 사용자 인증: 개인 일정 도우미 만들기