Skip to content

Widget SDK (embed.1pass.dev)

UMD JavaScript 한 줄과 <div> 한 개로 RP 페이지 안에 logi QR 로그인 위젯이 떠오릅니다. iframe 으로 격리되어 RP 페이지의 cookie / DOM 과 분리됩니다.

UI 통제가 더 필요하면 Device Flow, 페이지 redirect 가 가능하면 표준 Code Flow 가 더 단순합니다.

백엔드 작업 필요

Widget SDK 는 confidential client + PKCE 흐름입니다. RP 백엔드는 다음을 구현해야 합니다:

  1. PKCE code_verifier 생성 + 서버 측 저장 (세션/캐시, key=state)
  2. code_challenge 를 위젯에 내려주는 엔드포인트 (예: /api/auth/1pass/challenge)
  3. 위젯이 받은 codeclient_secret + code_verifier 와 함께 /oauth/token 으로 교환하는 엔드포인트

client_secret 은 RP 백엔드에서만 사용. 브라우저로 절대 내려보내지 마세요.

PKCE 셋업 (RP 백엔드 — 필수 선행 작업)

위젯은 PKCE code_challenge 없이는 마운트되지 않습니다.

규칙:

  • code_verifier: 43~128자 unreserved 문자열 (RFC 7636 §4.1). 권장: 32바이트 랜덤 → BASE64URL.
  • code_challenge: BASE64URL(SHA256(code_verifier)) (S256 method).
  • code_verifier 는 절대 브라우저로 보내지 말 것. 서버 세션/Redis 등에 state 키로 저장.
  • 1회용. 토큰 교환 후 즉시 폐기.

Verifier / Challenge 생성 예시

Ruby (Rails):

ruby
require "securerandom"
require "digest"
require "base64"

def generate_pkce_pair
  verifier  = SecureRandom.urlsafe_base64(32) # ~43 chars, no padding
  challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
  [verifier, challenge]
end

# In your controller:
verifier, challenge = generate_pkce_pair
state = SecureRandom.hex(16)
session[:logi_pkce] = { state => verifier } # short-lived, e.g. 10 min TTL
render json: { state: state, code_challenge: challenge }

Node.js:

javascript
import { randomBytes, createHash } from "node:crypto";

function generatePkcePair() {
  const verifier  = randomBytes(32).toString("base64url");
  const challenge = createHash("sha256").update(verifier).digest("base64url");
  return { verifier, challenge };
}

curl (셸 검증용):

bash
VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43)
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')
echo "verifier=$VERIFIER"
echo "challenge=$CHALLENGE"

두 가지 마운트 모드

모드data-* 속성UX언제
버튼 + 모달 (권장)data-logi-button버튼 클릭 → 풀 사이즈 모달에 QR (360×540)대부분의 RP
인라인 iframedata-logi-qrRP 페이지에 작은 iframe (320×460) 박힘대시보드 등 항상 노출 필요

버튼 + 모달은 QR 가독성 / iframe 차단 fallback / 레이아웃 충돌 회피 측면에서 우월합니다 (Auth0 Lock, Kakao SDK, Stripe Checkout 동일 패턴).

Quick Start

RP 백엔드가 /api/auth/1pass/challenge{ state, code_challenge } 를 발급하고, /api/auth/1pass/exchangecode 를 토큰으로 교환한다고 가정.

권장: 버튼 + 모달

html
<!DOCTYPE html>
<html>
<head>
  <script src="https://embed.1pass.dev/widget.js" defer></script>
</head>
<body>
  <div id="logi-mount"
       data-logi-button
       data-client-id="logi_xxxxxxxxxxxx"
       data-redirect-uri="https://your-app.com/callback"
       data-on-success="handleLogin"
       data-on-error="handleError"></div>

  <script>
    // 1) RP 백엔드에서 state + code_challenge 받아오기
    fetch("/api/auth/1pass/challenge", { method: "POST" })
      .then((r) => r.json())
      .then(({ state, code_challenge }) => {
        const mount = document.getElementById("logi-mount");
        mount.setAttribute("data-state", state);
        mount.setAttribute("data-code-challenge", code_challenge);
        if (window.LogiWidget) window.LogiWidget.mountButton(mount);
      });

    // 2) 모달 안의 위젯이 logi.success 이벤트로 code 를 부모에게 전달
    function handleLogin({ code, state }) {
      fetch("/api/auth/1pass/exchange", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ code, state }),
      }).then(() => location.assign("/dashboard"));
    }

    function handleError({ error, error_description }) {
      console.error("logi error:", error, error_description);
    }
  </script>
</body>
</html>

인라인 iframe

data-logi-button 대신 data-logi-qr 마커를 쓰고, LogiWidget.mountButton 대신 LogiWidget.mountWidget(mount) 를 호출. 나머지 속성/콜백은 위와 동일. 사용자 취소는 콜백 속성이 없으므로 mount.addEventListener("logi.cancelled", ...) 로 받음.

토큰 교환 (RP 백엔드)

bash
# RP 백엔드에서 호출 (브라우저 ❌)
curl -X POST https://api.1pass.dev/oauth/token \
  -d "grant_type=authorization_code" \
  -d "code=$CODE_FROM_WIDGET" \
  -d "redirect_uri=https://your-app.com/callback" \
  -d "client_id=$LOGI_CLIENT_ID" \
  -d "client_secret=$LOGI_CLIENT_SECRET" \
  -d "code_verifier=$STORED_VERIFIER"   # 세션에서 state 키로 꺼낸 값

SPA 단독 통합도 가능

백엔드 없이 브라우저에서 직접 /oauth/token 호출하려면 client_secret 없는 public client 로 등록하세요. PKCE-only 로 동작합니다. 자세한 절차는 Public Clients 가이드.

data-* 속성

속성필수설명
data-logi-qr / data-logi-button마커 속성. 마운트 모드 결정
data-client-idRP 의 OAuth Application client_id
data-redirect-uri콜백 URL. application 등록 시 입력한 값과 정확히 일치해야 함
data-code-challengePKCE code_challenge (BASE64URL(SHA256(verifier))). 누락 시 마운트 중단
data-code-challenge-methodPKCE method. 기본 S256 (현재 S256 만 지원)
data-scope공백 구분 scope (예: openid profile email)
data-stateCSRF 방지용 state. 생략 시 자동 생성. verifier 매핑 키로 쓰려면 RP 가 직접 발급 권장
data-on-success성공 시 호출할 전역 함수 이름 (window[name])
data-on-error에러 시 호출할 전역 함수 이름

미구현 속성

  • 사용자 취소 콜백 (data-on-cancelled) 미지원 → logi.cancelled CustomEvent 로 받으세요.
  • data-theme / data-lang 미지원 (v1.x roadmap).

postMessage 프로토콜

위젯은 window.postMessage 로 부모 창에 이벤트를 전달하고, 동시에 마운트 컨테이너에서 동일 이름의 CustomEvent 도 dispatch 합니다 (bubbles: true).

javascript
// 방식 1: window.postMessage 직접 리스닝
window.addEventListener("message", (event) => {
  // ✅ origin 검증 필수
  if (event.origin !== "https://embed.1pass.dev") return;

  const { type, ...payload } = event.data;
  switch (type) {
    case "logi.success":   handleLogin(payload); break;   // { code, state }
    case "logi.error":     handleError(payload); break;   // { error, error_description }
    case "logi.cancelled": handleCancel();       break;   // {}
  }
});

// 방식 2: 마운트 컨테이너의 CustomEvent (origin 검증을 위젯이 대신 해줌)
const mount = document.querySelector("[data-logi-qr]");
mount.addEventListener("logi.success",   (e) => handleLogin(e.detail));
mount.addEventListener("logi.error",     (e) => handleError(e.detail));
mount.addEventListener("logi.cancelled", (e) => handleCancel());
typepayload의미
logi.success{ code, state }사용자 승인 완료. RP 백엔드에서 /oauth/token 교환
logi.error{ error, error_description }OAuth 에러. error 값은 표준 OAuth 에러 코드
logi.cancelled{}사용자가 위젯 닫기 / 취소 클릭

RP 등록 (콘솔)

logi 콘솔(/console)에서 application 의 Widget Origins 에 RP 도메인을 추가하세요.

widget_origins:
  - https://your-app.com
  - https://staging.your-app.com
  - http://localhost:5173

이 값이 iframe 응답의 Content-Security-Policy: frame-ancestors 헤더에 박힙니다. 미등록 origin 은 브라우저가 iframe 렌더를 차단합니다.

프레임워크 통합 예시

PKCE 셋업 (/api/auth/1pass/challenge{ state, code_challenge } 발급) 이 RP 백엔드에 있다고 가정.

React

jsx
// LogiQrButton.jsx
import { useEffect, useRef, useState } from "react";

export function LogiQrButton({ clientId, redirectUri, onSuccess, onError }) {
  const ref = useRef(null);
  const [pkce, setPkce] = useState(null);

  useEffect(() => {
    fetch("/api/auth/1pass/challenge", { method: "POST" })
      .then((r) => r.json())
      .then(setPkce);
  }, []);

  useEffect(() => {
    if (!pkce || !ref.current || !window.LogiWidget) return;
    window.__logiOnSuccess = onSuccess;
    window.__logiOnError = onError;
    window.LogiWidget.mountWidget(ref.current);
    return () => {
      delete window.__logiOnSuccess;
      delete window.__logiOnError;
    };
  }, [pkce, onSuccess, onError]);

  if (!pkce) return null;
  return (
    <div
      ref={ref}
      data-logi-qr
      data-client-id={clientId}
      data-redirect-uri={redirectUri}
      data-code-challenge={pkce.code_challenge}
      data-state={pkce.state}
      data-on-success="__logiOnSuccess"
      data-on-error="__logiOnError"
    />
  );
}

// _app.jsx — 한 번만 로드
<script src="https://embed.1pass.dev/widget.js" defer />

Vue 3

vue
<script setup>
import { onMounted, onUnmounted, ref } from "vue";

const props = defineProps(["clientId", "redirectUri"]);
const emit = defineEmits(["success", "error", "cancelled"]);
const mountRef = ref(null);
const pkce = ref(null);

onMounted(async () => {
  pkce.value = await fetch("/api/auth/1pass/challenge", { method: "POST" }).then((r) => r.json());
  window.__logiSuccess = (p) => emit("success", p);
  window.__logiError = (p) => emit("error", p);
  await Promise.resolve();
  if (window.LogiWidget && mountRef.value) window.LogiWidget.mountWidget(mountRef.value);
  mountRef.value?.addEventListener("logi.cancelled", () => emit("cancelled"));
});
onUnmounted(() => {
  delete window.__logiSuccess;
  delete window.__logiError;
});
</script>

<template>
  <div
    v-if="pkce"
    ref="mountRef"
    data-logi-qr
    :data-client-id="clientId"
    :data-redirect-uri="redirectUri"
    :data-code-challenge="pkce.code_challenge"
    :data-state="pkce.state"
    data-on-success="__logiSuccess"
    data-on-error="__logiError"
  />
</template>

Svelte 5

svelte
<script>
  import { onMount, onDestroy } from "svelte";

  let { clientId, redirectUri, onSuccess, onError, onCancelled } = $props();
  let mountEl;
  let pkce = $state(null);

  onMount(async () => {
    pkce = await fetch("/api/auth/1pass/challenge", { method: "POST" }).then((r) => r.json());
    window.__logiSuccess = onSuccess;
    window.__logiError = onError;
    queueMicrotask(() => {
      if (window.LogiWidget && mountEl) window.LogiWidget.mountWidget(mountEl);
      mountEl?.addEventListener("logi.cancelled", () => onCancelled?.());
    });
  });
  onDestroy(() => {
    delete window.__logiSuccess;
    delete window.__logiError;
  });
</script>

{#if pkce}
  <div
    bind:this={mountEl}
    data-logi-qr
    data-client-id={clientId}
    data-redirect-uri={redirectUri}
    data-code-challenge={pkce.code_challenge}
    data-state={pkce.state}
    data-on-success="__logiSuccess"
    data-on-error="__logiError"
  />
{/if}

Vanilla / 기타

Quick Start 예시 그대로 HTML 에 박으면 됩니다. UMD 번들이라 모듈 시스템 불필요.

보안 모델

위협방어
등록되지 않은 사이트가 위젯 임베드widget_origins allowlist → Content-Security-Policy: frame-ancestors
postMessage 도청 / 사칭위젯이 부모로 보낼 때 targetOrigin 제한. 부모는 event.origin === "https://embed.1pass.dev" 검증 필수
인증 코드 가로채기PKCE (code_verifier 는 RP 서버에만 보관, code_challenge 만 위젯에 노출)
iframe 내 cookie 탈취SameSite=None; Secure cookie + HttpOnly + 별도 서브도메인(embed.1pass.dev) 으로 cookie scope 분리
Clickjackinglogi 측 frame-ancestors 로 등록 origin 만 허용. RP 측은 자체 페이지에 X-Frame-Options 검토 권장
HTTP 임베드 시도iframe URL HTTPS only. http origin 거부 (예외: http://localhost:* 개발용)

client_secret 노출 금지

브라우저 측 코드는 client_secret 을 절대 다루지 않습니다. confidential client 통합에서는 RP 백엔드가 client_secret + code_verifier 를 함께 사용해 토큰 교환. backend 없는 통합은 public client 로 등록하세요.

제한 사항

  • 모바일 웹브라우저에서 같은 기기로 자기 화면 QR 을 스캔 불가 → 모바일은 표준 Code Flow 의 Universal Link 점프 권장
  • iframe 차단 환경 (일부 기업 방화벽, 광고 차단기) 에서는 위젯이 안 뜸 → fallback 으로 표준 OAuth 버튼 병기 권장
  • 현재 v0.x: 디자인 커스터마이징 / 테마 / 언어 토큰 미지원 (v1.x roadmap)

라이브 데모

production walking-sample: https://api.1pass.dev/widget-demo.html. 실제 1pass-demo RP 로 마운트되며 /embed/demo/challenge 가 PKCE 챌린지를 발급합니다.

레퍼런스

MIT License · Identity가 제품의 신뢰를 만듭니다.