Express 세션
@keenauth/node/bff는 Express 4 또는 5 애플리케이션에 로그인 기능을 추가합니다. 서버가 OAuth 클라이언트이며 모든 토큰을 서버에 보관합니다. 브라우저는 암호화되고 HttpOnly 속성이 설정된 세션 쿠키만 받으며, 액세스 토큰, 리프레시 토큰, ID 토큰은 전혀 보지 않습니다. 이는 BFF(backend-for-frontend) 패턴입니다. 이 패키지는 Express를 가져오지 않으므로 추가 종속성이 생기지 않습니다. 아직 npm에 게시되지 않았습니다.
대신 베어러 토큰을 받는 API를 보호하려면 API 보호를 참조하십시오. 관리 API를 호출하려면 Node.js 관리 SDK를 참조하십시오.
시작하기 전에
- 대시보드에서 환경에 웹 애플리케이션을 만드십시오.
- 클라이언트 인증 방식을 선택하십시오. 클라이언트 시크릿(
client_secret_basic)을 사용하거나, 공개 키를 애플리케이션에 등록한 키 쌍으로private_key_jwt를 사용할 수 있습니다. 공유 시크릿이 서버 밖으로 나가지 않으므로private_key_jwt를 권장합니다. - 리디렉션 URI
https://app.acme.example/auth/callback과 로그아웃 후 리디렉션 URIhttps://app.acme.example/를 추가하십시오. - 32자 이상의 쿠키 시크릿을 생성하십시오(예:
openssl rand -base64 32). 저장소가 아닌 시크릿 저장소에 보관하십시오.
설정
import express from "express";
import { createBff, expressRequireAuth, expressSession } from "@keenauth/node/bff";
const bff = createBff({
issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
clientAuth: { method: "client_secret_basic", clientSecret: process.env.KEENAUTH_CLIENT_SECRET! },
appBaseUrl: "https://app.acme.example",
secret: process.env.KEENAUTH_COOKIE_SECRET!,
});
const app = express();
app.use(expressSession(bff));
app.get("/orders", expressRequireAuth(bff), (req, res) => {
const { session } = req.keenauth!;
res.send(`Signed in as ${session!.user.email}`);
});
private_key_jwt를 사용하는 경우 시크릿 대신 개인 키를 전달하십시오. SDK는 토큰 및 PAR 엔드포인트에 대한 요청마다 새로운 클라이언트 어서션을 서명합니다(aud는 발급자, jti는 무작위 값, 유효 기간 120초, kid는 키의 RFC 7638 썸프린트). 알고리즘은 키를 따릅니다. RSA에는 RS256(또는 alg: "PS256"), P-256에는 ES256, Ed25519에는 EdDSA를 사용합니다.
import { readFileSync } from "node:fs";
const bff = createBff({
issuer: "https://acme.eu.keenauth.app",
clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
clientAuth: { method: "private_key_jwt", privateKey: readFileSync("bff-key.pem", "utf8") },
appBaseUrl: "https://app.acme.example",
secret: process.env.KEENAUTH_COOKIE_SECRET!,
});
키가 KMS 또는 HSM에 있는 경우 clientAuth: { method: "private_key_jwt", assertion }을 대신 전달하십시오. 여기서 assertion()은 호출할 때마다 새로 서명된 클라이언트 어서션을 반환합니다. privateKey와 assertion 중 하나만 전달하고 둘 다 전달하지 마십시오.
appBaseUrl은 애플리케이션의 공개 주소입니다. SDK는 리디렉션 URI와 모든 리디렉션을 이 값으로부터 생성하며, 요청의 Host 또는 X-Forwarded-* 헤더로부터는 생성하지 않습니다. 따라서 trust proxy 설정의 영향을 받지 않습니다. 일반 http는 localhost에서만 허용됩니다.
옵션은 createBff가 실행될 때 검사되므로, 설정 오류가 있으면 시작 시점에 애플리케이션이 중지됩니다.
라우트
expressSession은 세 개의 라우트를 제공하고, 그 밖의 모든 요청에서 세션을 읽습니다.
| 라우트 | 기능 |
|---|---|
GET /auth/login |
로그인을 시작합니다. 선택 쿼리 매개변수: returnTo(사용자가 도착할 위치)와 organization(조직으로 로그인). |
GET /auth/callback |
로그인을 마무리하고 세션 쿠키를 설정합니다. |
GET 또는 POST /auth/logout |
KeenAuth에서 로그아웃하고 애플리케이션으로 돌아갑니다. |
로그인에는 PKCE(S256)를 사용하는 인증 코드 흐름을 사용합니다. 환경이 지원하는 경우 요청은 서버로 푸시됩니다(PAR). 콜백에서 SDK는 state, iss, ID 토큰(nonce 포함)을 확인합니다. 로그인이 성공할 때마다 새 세션이 생성되며, 이전 세션은 폐기됩니다.
returnTo는 /orders?tab=open과 같이 애플리케이션 자체의 경로여야 합니다. //evil.example이나 전체 URL 같은 다른 값은 무시되며 사용자는 /로 이동합니다.
라우트 보호
expressRequireAuth(bff)는 세션이 있는 요청만 통과시킵니다.
- 세션 없이 페이지 요청(
GET또는HEAD)을 보내면returnTo와 함께/auth/login으로 리디렉션됩니다. /api/…에 대한 요청이거나 다른 메서드를 사용하는 요청은 문제 문서와 함께401을 받습니다. 모든 경로에서401로 응답하려면{ api: true }를 전달하십시오.
POST, PUT, PATCH, DELETE 등 안전하지 않은 메서드의 경우, 브라우저는 요청이 애플리케이션에서 왔음을 나타내야 합니다. 즉 Sec-Fetch-Site: same-origin이거나 Origin 헤더가 애플리케이션의 출처와 같아야 합니다. 그 밖의 요청은 403을 받습니다. 이는 SameSite=Lax에 더해 사이트 간 요청 위조를 방지합니다.
세션 사용 및 API 호출
req.keenauth에는 다음이 들어 있습니다.
session:null또는 로그인한 사용자.session.user에는subject,email,name,orgId,orgRoles같은 타입이 지정된 ID 토큰 클레임이 있으며, 모든 클레임은session.user.claims에 있습니다. 세션 객체에는 토큰이 절대 포함되지 않습니다.accessToken: 서버 측 호출에 사용할 수 있는 유효한 액세스 토큰, 또는null.
app.get("/api/orders", expressRequireAuth(bff), async (req, res) => {
const upstream = await fetch("https://api.acme.example/v1/orders", {
headers: { Authorization: `Bearer ${req.keenauth!.accessToken}` },
});
res.status(upstream.status).json(await upstream.json());
});
TypeScript에서는 속성을 한 번 선언하십시오.
import type { SessionContext } from "@keenauth/node/bff";
declare module "express-serve-static-core" {
interface Request {
keenauth?: SessionContext | null;
}
}
액세스 토큰이 60초 안에 만료되면 SDK는 핸들러가 실행되기 전에 이를 갱신합니다. 갱신할 때마다 리프레시 토큰이 교체되고, 응답에 새 세션 쿠키가 설정됩니다. 서버가 리프레시 토큰을 거부하면 세션이 종료됩니다. 일시적인 서버 오류가 발생하면 세션은 변경되지 않은 상태로 유지됩니다.
액세스 토큰을 브라우저로 보내지 마십시오.
조직 전환
bff.switchOrganizationPath("org_01j9…", "/dashboard")는 /auth/login에 대한 링크를 반환합니다. 이 링크는 사용자가 해당 조직으로 다시 로그인하도록 합니다. KeenAuth가 멤버십을 확인합니다. 로그인이 끝나면 session.user.orgId에 조직이 저장됩니다.
로그아웃
양식에서 /auth/logout으로 POST 요청을 보내십시오. SDK는 다음을 수행합니다.
- 세션 쿠키를 삭제합니다(저장소를 사용하는 경우 저장된 세션도 삭제합니다).
- 리프레시 토큰을 취소합니다.
- 브라우저를 환경의 로그아웃 엔드포인트로 보내며, 이 엔드포인트가 브라우저를 애플리케이션으로 돌려보냅니다.
브라우저가 다른 사이트에서 온 요청으로 표시한 요청은 거부됩니다. Sec-Fetch-Site를 보내지 않는 브라우저는 자체 페이지에서 보낸 POST로만 로그아웃할 수 있습니다.
세션 저장소
기본적으로 전체 세션은 쿠키에 들어 있으며, secret에서 파생된 키를 사용해 AES-256-GCM으로 암호화되고 인증됩니다. 변경되었거나 복사된 쿠키는 거부됩니다. 쿠키 이름은 __Host-keenauth_session.0이며, Secure, HttpOnly, SameSite=Lax, Path=/로 설정됩니다. http://localhost에서는 __Host- 접두사와 Secure가 생략됩니다. 큰 세션은 최대 세 개의 쿠키로 나뉩니다.
토큰을 서버에 보관하려면 저장소를 전달하십시오.
import { createBff, type SessionStore } from "@keenauth/node/bff";
const store: SessionStore = {
get: async (id) => JSON.parse((await redis.get(`sess:${id}`)) ?? "null"),
set: async (id, session, ttl) => void (await redis.set(`sess:${id}`, JSON.stringify(session), { EX: ttl })),
delete: async (id) => void (await redis.del(`sess:${id}`)),
};
const bff = createBff({ /* … */ store });
그러면 쿠키에는 암호화된 무작위 세션 ID만 들어갑니다. 로그인할 때마다 새 ID가 생성되고 이전 레코드는 삭제됩니다. 저장소에는 토큰이 들어 있으므로 접근을 제한하십시오. MemorySessionStore는 개발과 테스트 전용입니다.
세션은 최대 7일(sessionMaxAge) 동안 유지되며, 갱신해도 이 기간은 연장되지 않습니다. 시크릿을 변경하려면 목록을 전달하십시오. 예: secret: [newSecret, oldSecret]. 이전 시크릿은 sessionMaxAge 이후에 제거하십시오.
현재 제한 사항
- DPoP에 바인딩된 토큰과 백채널 로그아웃은 아직 처리되지 않습니다.
- 갱신은 하나의 서버 프로세스 안에서만 공유됩니다. 여러 인스턴스를 사용하는 경우 애플리케이션 설정
refresh_reuse_interval_s(기본값 5초)를 통해 병렬 갱신이 성공할 수 있습니다. 이 값을0으로 설정하면 두 번째 갱신이 세션을 종료합니다.