문서 / 인증과 Secret
인증과 Secret
플랫폼 CLI 인증과 앱 사용자 로그인을 구분하고 세션·비밀 키를 보호합니다.
플랫폼 인증과 앱 로그인
CLI 연결은 개발자가 프로젝트를 관리할 권한입니다. 앱을 방문하는 고객의 로그인·매장 소속·관리자 권한이 자동으로 생기는 것은 아닙니다. 고객 인증은 앱이 사용하는 검증된 로그인 제공자와 연결하세요.
아래 예제는 로그인 제공자의 검증이 끝난 뒤 사용할 서버 세션 계층입니다. OAuth 콜백의 state·PKCE·토큰 검증을 생략하거나 브라우저가 보낸 userId만으로 세션을 발급하지 마세요. 비밀번호 로그인을 직접 구현한다면 별도의 검증된 비밀번호 해시·복구·시도 제한이 필요합니다.
세션 확인 미들웨어
app_sessions 스키마와 AppEnv 타입을 먼저 추가하세요. 원본 세션은 DB에 저장하지 않고 HMAC 해시로 조회합니다.
import { createMiddleware } from 'hono/factory';
import { getCookie } from 'hono/cookie';
import type { AppEnv } from '../types';
export async function sessionHash(token: string, salt: string) {
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(salt),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const hash = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(token));
return Array.from(new Uint8Array(hash), b => b.toString(16).padStart(2, '0')).join('');
}
export const requireSession = createMiddleware<AppEnv>(async (c, next) => {
const token = getCookie(c, '__Host-app_session');
if (!token || !/^[a-f0-9]{64}$/.test(token)) return c.json({ error: 'login_required' }, 401);
const identity = await c.env.DB.prepare(
'SELECT user_id,session_hash FROM app_sessions WHERE session_hash=? AND expires_at>?'
).bind(await sessionHash(token, c.env.SESSION_HASH_SALT), Date.now())
.first<{ user_id: string; session_hash: string }>();
if (!identity) return c.json({ error: 'login_required' }, 401);
// 쿠키 인증을 사용하는 브라우저 변경 요청은 같은 Origin만 허용합니다.
if (!['GET', 'HEAD'].includes(c.req.method) &&
c.req.header('Origin') !== new URL(c.req.url).origin)
return c.json({ error: 'origin_not_allowed' }, 403);
c.set('identity', identity);
c.header('Cache-Control', 'private, no-store');
await next();
});joripspace secret generate --project PROJECT --name SESSION_HASH_SALTSESSION_HASH_SALT는 자동 생성된 프로젝트 Secret을 사용합니다. 로컬 개발용 값은 Git 제외된 .dev.vars에 별도로 두고 운영 값을 복사하지 마세요. 세션이 있다고 모든 리소스에 접근할 수 있는 것은 아니므로 각 DB 조회에 사용자·매장 소속 조건을 추가합니다.
본인 확인 후 세션 발급
// OAuth 등 본인 확인에 성공한 서버 처리에서만 호출합니다.
// 사용자가 보낸 userId를 그대로 전달하는 공개 엔드포인트를 만들지 마세요.
import { setCookie } from 'hono/cookie';
import { sessionHash } from './middleware/session';
import type { Context } from 'hono';
import type { AppEnv } from './types';
export async function issueSession(c: Context<AppEnv>, verifiedUserId: string) {
if (!/^[a-zA-Z0-9_-]{1,80}$/.test(verifiedUserId)) throw new Error('Invalid internal user ID');
const bytes = crypto.getRandomValues(new Uint8Array(32));
const token = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
const maxAge = 3600; // 앱의 세션 정책 예시
await c.env.DB.prepare('INSERT INTO app_sessions(session_hash,user_id,expires_at) VALUES(?,?,?)')
.bind(await sessionHash(token, c.env.SESSION_HASH_SALT), verifiedUserId, Date.now() + maxAge * 1000).run();
setCookie(c, '__Host-app_session', token, {
httpOnly: true, secure: true, sameSite: 'Lax', path: '/', maxAge,
});
// 응답 JSON·로그·URL에는 token을 포함하지 않습니다.
}이 함수는 기존 로그인 제공자의 서버 검증 성공 흐름에서 호출합니다. 로그인 화면이나 OAuth 제공자를 새로 구성해주는 예제는 아닙니다. HttpOnly·Secure 쿠키로 원본 세션을 전달하며 PG 페이지에서 돌아오는 최상위 이동을 고려해 SameSite=Lax를 사용합니다.
로그아웃은 같은 Origin의 POST 요청에서 현재 session_hash 행을 삭제하고 같은 이름·Path의 쿠키를 만료시킵니다. 실시간 v2 연결이 있다면 해당 세션 접속권도 회수하세요. 세션 Secret 교체 시 기존 세션의 처리 정책을 함께 정합니다.
비밀 키와 API 보안
외부 PG·메일 제공자의 비밀 키는 프로젝트 설정의 Secret 등록 기능을 사용합니다. CLI 인증 파일, 사용자 세션, 외부 제공자 Secret은 용도가 다릅니다. Secret을 소스·브라우저 번들·공개 저장소·로그에 넣지 마세요.
- 서버에서 사용자 권한·입력 형식·본문 크기를 검증합니다.
- 쿠키 인증의 변경 요청은 CSRF 방어를 적용하고 불필요하게 CORS를 열지 않습니다.
- HTML은 이스케이프하고 브라우저에는 textContent로 출력합니다.
- 이메일·전화번호·결제키 등 민감한 데이터는 암호화하거나 조회용 해시로 관리합니다.
- 상태와 오류 ID만 기록하고 쿠키·요청 본문·PG 응답 전체를 로그에 복제하지 않습니다.