sayren Docs

구매자 인증

로그인 화면은 스토어프론트가 그리고, 계정과 인증은 sayren이 처리하기

구매자 계정은 sayren이 보관합니다. 스토어프론트는 로그인·가입 화면만 그리고 @sayren/storefront-sdk/auth로 결과를 받습니다. 어느 방식으로 로그인해도 같은 구매자 토큰이 나오고, 이 토큰으로 /me/*·장바구니·주문 API를 호출합니다.

준비

  • Storefront SDK 설치와 상점 코드
  • 소셜 로그인을 쓰면 설정 › 로그인에 공급자 키와 스토어프론트 도메인 등록(소유자만)
import { createStorefrontAuth } from "@sayren/storefront-sdk/auth";

const auth = createStorefrontAuth({ baseUrl: "https://api.sayren.io/storefront/v1", storeCode: "{상점 코드}" });
메서드하는 일결과
anonymous()비회원 세션 발급{ cartToken, expiresAt }
session({ email, password, cartToken? })이메일·비밀번호 로그인토큰 쌍
signUp({ email, password, name, phone?, agreements, cartToken? })가입과 동시에 로그인토큰 쌍
idp(provider, { redirectUri, agreements? })소셜 로그인 시작(카카오·네이버·구글){ url, state, codeVerifier }
idpCallback({ code, state, error?, expectedState, codeVerifier, cartToken? })소셜 로그인 마무리토큰 쌍
identities(accessToken)로그인 수단 조회{ hasPassword, hasExternalAuth, identities }
linkIdp(accessToken, provider, { redirectUri })소셜 계정 연결 시작{ url, state, codeVerifier }
linkIdpCallback(accessToken, provider, { code, state, error?, expectedState, codeVerifier })연결 마무리identities()와 같음
unlinkIdp(accessToken, provider)연결 해제identities()와 같음
refresh(refreshToken)토큰 갱신새 토큰 쌍
signOut(accessToken) · withdraw(accessToken, { password? })로그아웃 · 탈퇴없음

토큰 쌍은 { accessToken, refreshToken, expiresIn }입니다. 액세스 토큰은 30분, 리프레시 토큰은 14일입니다.

토큰은 서버에 보관하기

로그인은 스토어프론트 서버(라우트 action, 서버 함수)에서 부르고 토큰은 HttpOnly 쿠키에 두십시오. 브라우저 스크립트가 읽지 못해 스크립트 삽입으로 새지 않습니다.

const tokens = await auth.session({ email, password, cartToken });
const expiresAt = Date.now() + tokens.expiresIn * 1000;
// { accessToken, refreshToken, expiresAt }를 HttpOnly 쿠키에 저장
  • 요청마다 expiresAt을 보고 만료가 가까우면 refresh()로 새 쌍을 받아 쿠키를 바꿉니다. 리프레시 토큰은 한 번 쓰면 폐기됩니다.
  • 여러 요청이 같은 토큰으로 동시에 갱신해도 됩니다. 30초 안의 3번까지는 각각 새 쌍을 줍니다.
  • refresh()가 401 INVALID_REFRESH_TOKEN이면 쿠키를 지우고 로그아웃 상태로 보여 주십시오.

로그인 방법 확인

셀러가 설정 › 로그인(소유자만)에서 켠 방법만 동작합니다. 상점 공개 설정의 loginMethods를 읽어 버튼을 그립니다.

const store = await sdk.store.get();
store.loginMethods; // 예: ["password", "kakao", "naver"]

password가 없으면 이메일·비밀번호 가입·로그인이 403 PASSWORD_LOGIN_DISABLED입니다.

소셜 로그인

공급자 키는 셀러가 설정 › 로그인에 등록합니다. 스토어프론트는 키 없이 두 번만 호출합니다.

  1. 로그인 버튼 action에서 idp()로 공급자 로그인 주소를 받습니다. state·codeVerifier는 HttpOnly 쿠키 같은 서버 세션에 보관하고 구매자를 url로 보냅니다.
  2. 구매자가 redirectUri로 돌아오면 쿼리의 code·state와 보관한 값으로 idpCallback()을 불러 토큰 쌍을 받습니다.
// 1. 로그인 버튼 action
const started = await auth.idp("kakao", { redirectUri: "https://shop.example.com/auth/callback" });
// started.state·codeVerifier를 쿠키에 보관하고 started.url로 redirect

// 2. /auth/callback loader
const url = new URL(request.url);
const tokens = await auth.idpCallback({
  code: url.searchParams.get("code"),
  state: url.searchParams.get("state"),
  error: url.searchParams.get("error"),
  expectedState: saved.state,
  codeVerifier: saved.codeVerifier,
  cartToken, // 비회원 장바구니를 합치려면
});
  • redirectUri는 설정 › 로그인의 스토어프론트 도메인 안이어야 합니다.
  • 구매자가 취소하면 ?error=access_denied로 돌아오고 idpCallback()이 IdpCallbackError(reason)를 던집니다. code는 1분 안에 한 번만 씁니다.
  • 처음 보는 공급자 계정이면 새 구매자가 생깁니다. 같은 이메일의 구매자와 합치지 않고, 그때와 공급자가 이메일을 주지 않을 때는 email: null입니다.

소셜 계정 연결

로그인한 구매자가 소셜 계정을 더 붙입니다. 흐름은 소셜 로그인과 같고 액세스 토큰을 함께 넘기는 점만 다릅니다.

// 1. 연결 버튼 action — state·codeVerifier와 "연결 중" 표시를 쿠키에 보관하고 started.url로 redirect
const started = await auth.linkIdp(accessToken, "kakao", { redirectUri: "https://shop.example.com/auth/callback" });

// 2. /auth/callback — 연결 흐름이면 linkIdpCallback()
const identities = await auth.linkIdpCallback(accessToken, "kakao", {
  code: url.searchParams.get("code"),
  state: url.searchParams.get("state"),
  error: url.searchParams.get("error"),
  expectedState: saved.state,
  codeVerifier: saved.codeVerifier,
});

흐름을 시작한 구매자만 마무리할 수 있습니다(아니면 400 INVALID_GRANT). 마지막 로그인 수단은 unlinkIdp()로 끊을 수 없습니다. MCP 템플릿의 /account 화면에 이 흐름이 들어 있습니다.

자체 회원 연동

회원 시스템이 이미 있으면 로그인은 직접 하고, 서버에서 sayren 구매자 토큰만 받습니다(구매자 위임).

  1. Sayren 계정 › 액세스 토큰에서 토큰을 만듭니다. 권한에 customer:rw를 넣고 구매자 위임을 켜고, 상점 범위에 이 상점을 넣습니다. 만든 사람의 역할에 customer:rw가 있어야 합니다(소유자·관리자).
  2. 토큰과 상점 id를 서버 환경변수에 둡니다. 토큰은 만든 직후 한 번만 보입니다.
  3. 사용자가 로그인하면 서버에서 delegateMember()로 구매자 토큰을 받습니다.
import { delegateMember } from "@sayren/store-sdk/server";

const { accessToken, memberId, expiresIn } = await delegateMember({
  baseUrl: "https://api.sayren.io",
  accessToken: process.env.SAYREN_ACCESS_TOKEN!,
  storeId: process.env.SAYREN_STORE_ID!,
  uid: user.id, // 내 회원 시스템의 사용자 id
  email: user.email, // 처음 만들 때만 씀
  name: user.name,
  authenticatedAt: session.loggedInAt, // Date — 사용자가 실제로 로그인한 시각
});

REST로는 POST /v1/customers/delegations(Authorization: Bearer <액세스 토큰>, X-Store-Id)입니다. 본문은 uid·email·name·authenticatedAt이고 응답 data는 { accessToken, expiresIn, memberId }입니다(인증과 계정 토큰).

  • 처음 보는 uid면 구매자를 만들고, 같은 uid는 이메일이 바뀌어도 같은 구매자입니다. 이메일이 다른 구매자 것이면 이메일 없이 만듭니다.
  • 토큰은 30분이고 리프레시 토큰이 없어 만료되면 같은 호출로 다시 받습니다. 계정 토큰을 쓰므로 서버에서만 부릅니다.
  • authenticatedAt이 없으면 비밀번호 없는 구매자의 탈퇴가 403 REAUTH_REQUIRED입니다. 재발급할 때도 처음 로그인한 시각을 넘기십시오.
  • 실패하면 MemberDelegationError를 던집니다. error가 API 오류 코드입니다.
코드상황
403 DELEGATION_NOT_ALLOWED액세스 토큰이나 Sayren 상점 플랫폼·CLI 로그인이 아닌 토큰(다른 앱의 로그인 토큰, AI 에이전트)
403 INSUFFICIENT_SCOPE토큰에 member:delegate(구매자 위임)가 없음
403 INSUFFICIENT_ROLE역할에 customer:rw가 없음
403 STORE_NOT_GRANTED토큰의 상점 범위 밖의 상점

uid 없이 email만 보내면 이메일로 구매자를 찾습니다. 두 방식은 같은 구매자로 이어지지 않으니 처음부터 하나로 정하십시오.

0.27.0부터 delegateMemberByUid()·delegateMemberByEmail()(Client ID·Client Secret, token-exchange)은 없어졌습니다. delegateMember()로 바꾸고 앱 대신 액세스 토큰을 쓰십시오.

비회원

로그인하지 않은 구매자는 비회원 세션을 씁니다. 장바구니와 비회원 주문만 쌓이고 /me/*에는 쓸 수 없습니다.

const guest = await auth.anonymous();
// guest.cartToken을 쿠키에 보관하고 클라이언트의 cartToken 옵션으로 보낸다
  • 따로 부르지 않아도 됩니다. 장바구니 API를 토큰 없이 부르면 세션을 발급해 X-Cart-Token 헤더로 돌려줍니다(onCartToken으로 받음).
  • 마지막 활동 후 30일이 지나면 만료되고 장바구니도 지워집니다. 쓸 때마다 기한이 늘어납니다.
  • 만료됐거나 모르는 토큰이면 새 세션이 옵니다. 담은 적 없는 세션은 하루 뒤 지워집니다.

로그인하면 장바구니 합치기

session()·signUp()·idpCallback()에 비회원 cartToken을 보내면 회원 장바구니로 합칩니다. 같은 상품·옵션은 수량을 더합니다. 합친 뒤 비회원 세션이 닫히므로 비회원 토큰 쿠키를 지우고, 이후 장바구니는 액세스 토큰으로 조회합니다.

동의와 재동의

가입 때 받은 약관·개인정보·마케팅 동의는 버전과 함께 이력으로 쌓이고 지워지지 않습니다. 셀러가 약관 버전을 올리면 이전 버전에 동의한 구매자에게 재동의를 받습니다. 조회·변경은 createStorefrontClient의 member입니다(구매자 토큰 필요).

const consents = await client.member.consents();
// {
//   terms:   { kind: "TERMS", agreed: true, agreedVersion: "1", currentVersion: "2", currentDocumentUrl: "https://…", reconsentRequired: true, updatedAt },
//   privacy: { … }, marketing: { … },
//   reconsentRequired: true,
// }

await client.member.updateConsents({ terms: true }); // 개정 약관에 다시 동의
await client.member.updateConsents({ marketing: false }); // 마케팅 철회
  • 재동의 필요 여부는 GET /me의 reconsentRequired로도 봅니다. 로그인 응답에는 없으니 화면을 열 때 조회하고, currentDocumentUrl을 링크로 보여 주십시오.
  • 필수 동의(terms·privacy)는 true만 보냅니다. 이미 현재 버전이면 이력을 더 남기지 않고, 재동의 전에도 이용은 막히지 않습니다.
  • 회원당 분당 10번입니다. 넘으면 429 TOO_MANY_REQUESTS(error.details.retryAfterSeconds)입니다. PATCH /me의 marketingAgreed도 같은 이력에 남습니다.

소셜 가입의 동의

소셜 가입은 공급자 화면을 거치므로 가입 화면에서 받은 동의를 idp()를 시작할 때 보냅니다. 그 로그인으로 가입이 될 때만 이력에 남고, 기존 회원의 로그인이면 무시합니다.

await auth.idp("kakao", { redirectUri: `${origin}/auth/callback`, agreements: { terms: true, privacy: true, marketing: false } });

terms와 privacy는 함께 true여야 합니다. 보내지 않아도 가입은 되지만 reconsentRequired가 true가 되니 첫 진입에서 updateConsents()로 받으십시오.

탈퇴

로그인한 구매자가 본인 확인 뒤 계정을 지웁니다. 되돌릴 수 없으니 화면에서 한 번 더 확인받으십시오.

await auth.withdraw(accessToken, { password });
// 성공하면 토큰 쿠키를 지운다
  • 계정·배송지·위시리스트·장바구니·적립금·동의 이력이 지워지고 토큰은 모두 무효가 됩니다. 주문·클레임·리뷰·문의 기록은 상점이 보관합니다.
  • 진행 중 주문·클레임·결제가 있으면 409 WITHDRAWAL_BLOCKED이고 error.details의 inProgressOrderItems·openClaims·pendingPayments로 안내합니다.
  • 비밀번호를 5번 연속 틀리면 10분 잠깁니다.
  • 비밀번호 없는 소셜 가입 구매자는 password를 빼고 부릅니다. 로그인 10분 안의 토큰이어야 하고, 갱신한 토큰이면 403 REAUTH_REQUIRED입니다.

에러

실패는 ApiError로 올라오고 error.code로 나눕니다.

상태코드뜻
401INVALID_CREDENTIALS이메일·비밀번호 불일치
423ACCOUNT_LOCKED5번 연속 실패로 10분 잠김
409EMAIL_ALREADY_EXISTS이미 가입한 이메일
401INVALID_REFRESH_TOKEN리프레시 토큰 만료·폐기. 다시 로그인
403PASSWORD_MISMATCH탈퇴 본인 확인 비밀번호 불일치
409WITHDRAWAL_BLOCKED진행 중 주문·클레임·결제로 탈퇴 불가
403PASSWORD_LOGIN_DISABLED이메일·비밀번호 로그인 꺼짐
403REAUTH_REQUIRED최근 로그인 필요(소셜 전용 구매자 탈퇴)
400INVALID_REDIRECT_URI스토어프론트 도메인 밖의 redirectUri
400INVALID_GRANT만료·재사용·PKCE 불일치 소셜 code
404LOGIN_PROVIDER_DISABLED켜지 않은 소셜 공급자
409IDENTITY_IN_USE다른 구매자에 연결된 소셜 계정
409PROVIDER_ALREADY_LINKED같은 공급자의 다른 계정이 이미 연결됨
409LAST_LOGIN_METHOD마지막 로그인 수단 해제 불가

다음 단계

이 페이지 목차