← 지나패스 홈
Developer Guide

지나패스 연동 가이드

"지나패스로 로그인"은 표준 OpenID Connect (OAuth 2.0) 그대로예요. 쓰던 OIDC/OAuth2 라이브러리에 발급기관(issuer) 하나만 바꿔 끼우면 바로 붙습니다. 로그인 후 넘어오는 건 안정적인 익명 식별자(sub)별명(name)뿐 — 얼굴·생체정보는 절대 넘어오지 않아요.

지나패스가 뭔가요

지나패스는 지나월드의 무자각 얼굴인증 신원 제공자(IdP)예요. 사용자는 얼굴로 자기 자신을 증명하지만, 여러분의 서비스에는 그 얼굴이 오지 않습니다. 대신 표준 OIDC 토큰(id_token)이 넘어오고, 그 안에는 이 사용자를 안정적으로 식별하는 익명 sub가 담겨요. 같은 사람은 여러분 서비스에서 언제나 같은 sub로 돌아옵니다.

OpenID Connect를 처음 붙이더라도, 이 문서의 3스텝과 코드 예시만 따라 하면 됩니다.

빠른 시작 · 3스텝

1

클라이언트 등록 요청

먼저 지나월드에 클라이언트 등록을 요청하세요. 아래 정보를 전달하면 서버의 OIDC_CLIENTS 목록에 등재됩니다.

항목설명
client_id여러분 서비스 식별자 (예: ticketfarm)
redirect_uris콜백으로 허용할 URL 정확히 — 여기 등록된 것만 리다이렉트 허용(오픈 리다이렉트 방지)
client_secret (선택)서버 사이드 앱이면 발급. SPA·모바일 등 공개 클라이언트면 생략하고 PKCE만 사용
name동의 화면에 보일 서비스 이름

등록 예시(서버 .envOIDC_CLIENTS JSON 배열 한 항목):

{
  "client_id": "ticketfarm",
  "redirect_uris": ["https://ticketfarm.example.com/callback"],
  "name": "티켓팜"
  // client_secret 없으면 공개 클라이언트 → PKCE 필수
}
2

/authorize로 리다이렉트

로그인 버튼을 누르면 PKCE code_verifier를 만들어 저장하고, 그 SHA-256 해시(code_challenge)를 붙여 /authorize로 보냅니다.

const ISSUER = 'https://pass.jina.world';
const CLIENT_ID = 'ticketfarm';
const REDIRECT = 'https://ticketfarm.example.com/callback';

// PKCE verifier 생성 → 세션에 보관
const verifier = rand() + rand();          // 충분히 긴 랜덤
sessionStorage.setItem('pkce_verifier', verifier);
const state = rand();
sessionStorage.setItem('oidc_state', state);
const challenge = await sha256b64(verifier); // S256

const u = new URL(ISSUER + '/authorize');
u.search = new URLSearchParams({
  response_type: 'code',
  client_id:     CLIENT_ID,
  redirect_uri:  REDIRECT,
  scope:         'openid profile',
  state,
  nonce:         rand(),
  code_challenge:        challenge,
  code_challenge_method: 'S256',
}).toString();
location.href = u.toString();
3

콜백에서 code/token으로 교환

지나패스가 인증을 마치면 redirect_uricodestate를 돌려줘요. state를 검증한 뒤, 저장해 둔 code_verifier와 함께 /token에 교환 요청을 보냅니다.

const q = new URL(location.href).searchParams;
if (q.get('state') !== sessionStorage.getItem('oidc_state'))
  throw new Error('state 불일치(위조 의심)');

const body = new URLSearchParams({
  grant_type:    'authorization_code',
  code:          q.get('code'),
  redirect_uri:  REDIRECT,
  client_id:     CLIENT_ID,
  code_verifier: sessionStorage.getItem('pkce_verifier'),
  // client_secret 있으면 여기 함께(client_secret_post) — 서버에서만!
});
const r = await fetch(ISSUER + '/token', {
  method: 'POST',
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
  body,
}).then(r => r.json());

// r.id_token 을 JWKS 공개키로 검증 후 클레임 사용
const claims = verifyIdToken(r.id_token); // sub, name, iss, aud, exp ...
console.log(claims.sub, claims.name);

토큰 응답: { access_token, token_type:"Bearer", expires_in:3600, id_token, scope }. 필요하면 access_token으로 /userinfo를 호출해 sub/name을 다시 받을 수도 있어요.

엔드포인트 레퍼런스

모든 경로의 기준(issuer)은 https://pass.jina.world 입니다.

엔드포인트메서드설명
/.well-known/openid-configuration GET Discovery 문서. 아래 엔드포인트·지원 스펙을 자동 발견. 대부분의 OIDC 라이브러리가 이것만으로 설정 완료
/.well-known/jwks.json GET JWKS 공개키(RS256). id_token 서명 검증용. kid로 키 매칭
/authorize GET 인가 요청. 사용자를 여기로 리다이렉트 → 인증 후 code 반환. response_type=code만 지원
/token POST 토큰 교환. authorization_code 그랜트로 codeid_token+access_token. x-www-form-urlencoded
/userinfo GET Authorization: Bearer <access_token>sub(+profile이면 name) 반환

지원 스펙: response_types=["code"], grant_types=["authorization_code"], subject_types=["public"], id_token_signing_alg=["RS256"], scopes=["openid","profile"], code_challenge_methods=["S256"], token_endpoint_auth_methods=["client_secret_post","client_secret_basic","none"].

id_token 클레임

클레임포함 조건의미
sub항상여권 guestId — 이 사용자의 안정적 익명 식별자. 같은 사람은 늘 같은 값
nameprofile 스코프 + 별명 존재 시사용자 별명(닉네임)
iss항상https://pass.jina.world — 검증 시 반드시 대조
aud항상여러분의 client_id — 검증 시 반드시 대조
exp / iat항상만료·발급 시각(Unix, 유효기간 1시간)
auth_time항상사용자가 인증한 시각
nonce요청 시 보냈다면재생공격 방지용. 보낸 값과 일치하는지 확인
🔒 얼굴·생체정보는 절대 담기지 않습니다

지나패스가 여러분에게 넘기는 것은 익명 식별자 sub와 (요청 시) 별명 name이 전부입니다. 얼굴 이미지, 얼굴 임베딩, 그 어떤 생체 데이터도 id_token·userinfo 어디에도 포함되지 않아요. 인증은 지나패스 안에서만 일어나고, 여러분에게는 검증된 신원만 도착합니다.

전체 코드 예시

아래는 브라우저(공개 클라이언트) 기준 최소 구현이에요. 실제 동작하는 릴레잉파티(oidc-demo.html)에서 발췌·정리했습니다.

const ISSUER    = 'https://pass.jina.world';
const CLIENT_ID = 'ticketfarm';
const REDIRECT  = location.origin + location.pathname;

// ── PKCE / 유틸 ─────────────────────────────
const b64url = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const rand = () => { const a = new Uint8Array(32); crypto.getRandomValues(a); return b64url(a.buffer); };
async function sha256b64(s) {
  return b64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s)));
}
function decodeJwtPayload(jwt) {
  return JSON.parse(atob(jwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
}

// ── 1) 로그인 시작 → /authorize ─────────────
async function login() {
  const verifier = rand() + rand();
  sessionStorage.setItem('pkce_verifier', verifier);
  const state = rand();
  sessionStorage.setItem('oidc_state', state);
  const challenge = await sha256b64(verifier);

  const u = new URL(ISSUER + '/authorize');
  u.search = new URLSearchParams({
    response_type: 'code', client_id: CLIENT_ID, redirect_uri: REDIRECT,
    scope: 'openid profile', state, nonce: rand(),
    code_challenge: challenge, code_challenge_method: 'S256',
  }).toString();
  location.href = u.toString();
}

// ── 2) 콜백 처리 → /token 교환 ──────────────
async function handleCallback() {
  const q = new URL(location.href).searchParams;
  if (q.get('error')) throw new Error(q.get('error'));
  const code = q.get('code');
  if (!code) return null;                       // 첫 방문
  if (q.get('state') !== sessionStorage.getItem('oidc_state'))
    throw new Error('state 불일치');

  const body = new URLSearchParams({
    grant_type: 'authorization_code', code, redirect_uri: REDIRECT,
    client_id: CLIENT_ID, code_verifier: sessionStorage.getItem('pkce_verifier'),
  });
  const r = await fetch(ISSUER + '/token', {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    body,
  }).then(r => r.json());
  if (!r.id_token) throw new Error(r.error || 'token 교환 실패');

  history.replaceState({}, '', REDIRECT);        // URL에서 code 제거
  return decodeJwtPayload(r.id_token);          // { sub, name, iss, aud, ... }
}
⚠️ 프로덕션에서는 서명 검증을 반드시

decodeJwtPayload는 표시용으로 페이로드만 디코드합니다. 실제 신뢰 결정 전에는 /.well-known/jwks.json의 공개키로 RS256 서명을 검증하고 iss·aud·exp·nonce를 대조하세요. 서버 사이드라면 jose 같은 라이브러리의 jwtVerifycreateRemoteJWKSet(jwks_uri)와 함께 쓰면 자동입니다.

직접 눌러보기

실제로 동작하는 릴레잉파티 데모가 준비돼 있어요. "티켓팜"이라는 가상의 외부 서비스가 지나패스로 로그인하고, 넘어온 sub/name/iss/aud 클레임을 그대로 보여줍니다.

🎟️ 데모 체험 — 티켓팜
"지나패스로 로그인" 버튼을 눌러 전체 PKCE 흐름을 실제로 밟아보세요.

보안 노트