Add CP auth to your own UI — @cp-platform/auth-sdk

For external products on their own domains (your product, and your team's other apps) that want to keep their own login screen while sharing the CP Platform user pool.

One install:

npm i git+https://github.com/Copark-Dev/cp-auth-sdk.git#v0.5.2

Why the SDK: products used to hand-roll PKCE, code-verifier, popup callback, and in-app browser detection. The SDK now wraps all of it, including a 30+ in-app-browser guard that catches Threads / Toss / KakaoBank / Coupang / Baemin / Karrot / Discord / etc.


1. Frontend — login / guard / OAuth

Mount the provider once at the app root:

import { CpAuthProvider } from "@cp-platform/auth-sdk/react";
 
<CpAuthProvider strict>
  <App />
</CpAuthProvider>

One line at every OAuth click:

import { useCpAuth } from "@cp-platform/auth-sdk/react";
 
function LoginButton() {
  const { guardOAuth } = useCpAuth();
  return (
    <button onClick={() => {
      if (guardOAuth("google") !== "proceed") return; // in-app handled automatically
      // ── your own OAuth flow from here ──
      window.location.href = `${CP_AUTH_URL}/oauth/authorize?client_id=...&...`;
    }}>
      Continue with Google
    </button>
  );
}

guardOAuth("google") returns:

  • "proceed" — clean browser, run your flow.
  • "redirected" — Android in-app webview; SDK already navigated the page to Chrome via intent URL. Return immediately.
  • "blocked" — iOS in-app webview; SDK mounted <InAppBrowserModal/>. User must open Safari manually (Apple OS policy).

Kakao always returns "proceed" (works inside in-app webviews).

Email / password directly

import { CpAuthClient } from "@cp-platform/auth-sdk";
 
const auth = new CpAuthClient({
  authUrl: "https://auth.cp-platform.com",
  productOrigin: "https://your-app.com",
});
 
await auth.login(email, password);
await auth.signup({ email, password, phone, phone_consent: true });
await auth.requestPasswordReset(email);
await auth.confirmPasswordReset(tokenFromEmail, newPassword);
await auth.verifyEmail(tokenFromEmail);
await auth.logout();
 
if (auth.isLoggedIn()) {
  const user = await auth.getMe();
}

Tokens land in localStorage and auto-refresh one minute before expiry. SSR / React Native callers can inject a custom adapter via the storage option (see SDK README).


2. Backend — JWT verification + OAuth code exchange

Node / Express / Next API routes / Fastify:

import { verifyCpJwt } from "@cp-platform/auth-sdk/jwt";
import { CpServerClient } from "@cp-platform/auth-sdk/server";
 
// per-request auth (middleware)
const { cpUserId, email } = await verifyCpJwt(bearerToken);
const { rows } = await db.query(
  "SELECT id FROM profiles WHERE cp_user_id = $1",
  [cpUserId]
);
 
// OAuth callback handling
const cp = new CpServerClient(); // authUrl defaults to https://auth.cp-platform.com
const tokens = await cp.exchangeOAuthCode(code, codeVerifier);
const user = await cp.getUserInfo(tokens.access_token);
// then getOrCreateLocalUser(user.sub) — see Federated identity below

verifyCpJwt runs ES256 verification against the Supabase JWKS — no JWT secrets to manage on the product side.


3. Federated identity — shared user pool, separate product DB

Keep your business data where it is. Just bridge with a mapping column:

-- in your product DB
ALTER TABLE public.profiles
  ADD COLUMN cp_user_id UUID UNIQUE;

Backend flow:

  1. Request arrives → verifyCpJwt(bearerToken) → cpUserId
  2. SELECT id FROM profiles WHERE cp_user_id = $1 for your internal user
  3. If null, auto-insert (INSERT ... (cp_user_id, email, ...))
  4. From there on, your code uses the local id as usual

A CP-platform signup becomes a product row on first request. Passwords / OAuth identities only live on the CP side.


4. In-app browser guard (automatic)

When <CpAuthProvider strict> is mounted the following just happens:

EnvironmentBehavior
Android in-app (Threads / KakaoTalk / Toss / Coupang / Baemin / Discord / …) + Google/Appleintent:// URL bounces the user to Chrome. Zero taps.
iOS in-app + Google/Apple<InAppBrowserModal/> mounts. User uses the app's "Open in Safari" menu (Apple OS won't let JS launch Safari).
Kakao OAuthAlways passes through (works in in-app webviews).
Normal mobile / desktop browserNo-op.

Apps covered:

  • Global: Threads(Barcelona codename) / Instagram / Facebook / WeChat / X / TikTok / Snapchat / Pinterest / Reddit / Discord / Telegram / Slack / LINE
  • Korea: KakaoTalk / KakaoBank / KakaoPay / KakaoStory / Toss / Coupang / Baemin / Karrot / 11st / SSG / Emart / Lotte / Gmarket / Naver (search·mail·cafe) / Daum / BAND / EveryTime / Blind / KB / Woori / Shinhan / Hana / NH / IBK
  • Long-tail: with strict: true, any mobile UA outside the whitelist (Safari / Chrome / Samsung / Firefox / Edge / Opera / Brave / DuckDuckGo) is treated as in-app.

5. Migrating from hand-rolled auth

If you wrote PKCE / popup / code-verifier handling by hand:

  1. Install SDK + mount <CpAuthProvider>
  2. Add guardOAuth("google") as the first line of your OAuth click handler — keep your existing PKCE / popup flow underneath (the guard is purely additive)
  3. (Optional) Replace your PKCE / fetch code with CpAuthClient calls
  4. Replace any custom jose.jwtVerify or httpx wrapper on the backend with verifyCpJwt / CpServerClient
  5. Most CP_JWT_* env vars can go away — SDK defaults already point at production. Override only for staging / local.

You can wire several products into shared authentication this way. Keep your migration PRs as reference examples in your team's own repository (<your-org>).


6. SDK versions

v0.1.0 — current stable. Includes everything:

  • CpAuthClient + verifyCpJwt + CpServerClient
  • <CpAuthProvider> + useCpAuth().guardOAuth()
  • 30+ in-app browser patterns + strict mode + auto Chrome intent escape (Android)

External products should always pin to v0.1.0.