Express 세션

@keenauth/node/bff는 Express 4 또는 5 애플리케이션에 로그인 기능을 추가합니다. 서버가 OAuth 클라이언트이며 모든 토큰을 서버에 보관합니다. 브라우저는 암호화되고 HttpOnly 속성이 설정된 세션 쿠키만 받으며, 액세스 토큰, 리프레시 토큰, ID 토큰은 전혀 보지 않습니다. 이는 BFF(backend-for-frontend) 패턴입니다. 이 패키지는 Express를 가져오지 않으므로 추가 종속성이 생기지 않습니다. 아직 npm에 게시되지 않았습니다.

대신 베어러 토큰을 받는 API를 보호하려면 API 보호를 참조하십시오. 관리 API를 호출하려면 Node.js 관리 SDK를 참조하십시오.

시작하기 전에

  1. 대시보드에서 환경에 웹 애플리케이션을 만드십시오.
  2. 클라이언트 인증 방식을 선택하십시오. 클라이언트 시크릿(client_secret_basic)을 사용하거나, 공개 키를 애플리케이션에 등록한 키 쌍으로 private_key_jwt를 사용할 수 있습니다. 공유 시크릿이 서버 밖으로 나가지 않으므로 private_key_jwt를 권장합니다.
  3. 리디렉션 URI https://app.acme.example/auth/callback과 로그아웃 후 리디렉션 URI https://app.acme.example/를 추가하십시오.
  4. 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)는 세션이 있는 요청만 통과시킵니다.

POST, PUT, PATCH, DELETE 등 안전하지 않은 메서드의 경우, 브라우저는 요청이 애플리케이션에서 왔음을 나타내야 합니다. 즉 Sec-Fetch-Site: same-origin이거나 Origin 헤더가 애플리케이션의 출처와 같아야 합니다. 그 밖의 요청은 403을 받습니다. 이는 SameSite=Lax에 더해 사이트 간 요청 위조를 방지합니다.

세션 사용 및 API 호출

req.keenauth에는 다음이 들어 있습니다.

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는 다음을 수행합니다.

  1. 세션 쿠키를 삭제합니다(저장소를 사용하는 경우 저장된 세션도 삭제합니다).
  2. 리프레시 토큰을 취소합니다.
  3. 브라우저를 환경의 로그아웃 엔드포인트로 보내며, 이 엔드포인트가 브라우저를 애플리케이션으로 돌려보냅니다.

브라우저가 다른 사이트에서 온 요청으로 표시한 요청은 거부됩니다. 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 이후에 제거하십시오.

현재 제한 사항