Skip to main content

Refresh the access token

Access tokens are short-lived. Use the refresh method to obtain a new one without requiring the user to log in again. The SDK handles caching and prevents concurrent refresh calls across browser tabs automatically.
The refresh flow is protected by DPoP (Demonstration of Proof-of-Possession). The SDK generates a cryptographic key pair and signs each refresh request with a proof that binds the request to the client. This protects against:
  • Token theft — A stolen refresh cookie is unusable without the private key, which is bound to the browser and never transmitted
  • Token replay — Each DPoP proof includes a unique identifier and timestamp, preventing reuse
  • Man-in-the-middle attacks — The proof binds to the HTTP method and URL, so it cannot be replayed against a different endpoint
  • Token export — The key pair is non-extractable, meaning it cannot be copied from the browser to another device
const { user } = await client.refresh();
// user.accessToken contains the new JWT
// user.profile contains the user profile data
To force the next refresh call to hit the backend (bypassing the local cache), invalidate the cache first:
await client.invalidateCache();
In the login App.jsx above, add decode to your import (import { ..., decode } from "@prelude.so/js-sdk") and replace the article block with:
<article>
  <p>Logged in</p>
  <pre style={{ textAlign: "left", whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
    {JSON.stringify(decode(user.accessToken).claims, null, 2)}
  </pre>
  <button onClick={async () => {
    await client.invalidateCache();
    const { user: refreshed } = await client.refresh();
    setUser(refreshed);
  }}>Refresh Token</button>
</article>
Each click fetches a new access token and updates the decoded token content.

Log out

When a user logs out, call logout to revoke the session and clear the local cache:
await client.logout();
Add a logout button next to the refresh button in the authenticated view:
<button className="secondary" onClick={async () => {
  await client.logout();
  setUser(null);
}}>Log Out</button>
The user is redirected back to the login form after logout.

List sessions

Retrieve all active sessions for the authenticated user:
const { sessions, total } = await client.listSessions({
  limit: 10,
  offset: 0,
});
Each session contains id, device_type, device_model, os_version, country_code, created_at, and last_seen_at.
Replace src/App.jsx with:
src/App.jsx
import "@picocss/pico";
import { useState, useEffect } from "react";
import { PrldSessionClient } from "@prelude.so/js-sdk";

const client = new PrldSessionClient({ domain: `${import.meta.env.VITE_APP_ID}.session.prelude.dev` });

export default function App() {
  const [loggedIn, setLoggedIn] = useState(false);
  const [sessions, setSessions] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    client.refresh()
      .then(() => { setLoggedIn(true); return client.listSessions(); })
      .then(({ sessions }) => setSessions(sessions))
      .catch(() => {})
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p aria-busy="true">Loading...</p>;
  if (!loggedIn) return <p>Please log in first using one of the login examples.</p>;

  return (
    <>
      <style>{`body, #root { display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; padding: 0; }`}</style>
      <main style={{ maxWidth: "800px", width: "100%" }}>
        <h1>Sessions</h1>
        <table>
          <thead>
            <tr>
              <th>Session ID</th>
              <th>Device</th>
              <th>Country</th>
              <th>Last Seen</th>
            </tr>
          </thead>
          <tbody>
            {sessions.map((s) => (
              <tr key={s.id}>
                <td><code>{s.id}</code></td>
                <td>{s.device_model || s.device_type || "-"}</td>
                <td>{s.country_code || "-"}</td>
                <td>{new Date(s.last_seen_at).toLocaleString()}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </main>
    </>
  );
}

Revoke sessions

Use revokeSessions to revoke sessions. The target parameter controls which sessions are revoked:
TargetDescription
"all"Revoke all sessions, including the current one.
"others"Revoke all sessions except the current one.
"mine"Revoke the current session only.
"session"Revoke a specific session by ID.
// Revoke all sessions
await client.revokeSessions("all");

// Revoke all other sessions
await client.revokeSessions("others");

// Revoke the current session
await client.revokeSessions("mine");

// Revoke a specific session by ID
await client.revokeSessions("session", sessionId);
Building on the list sessions example above, add revocation controls. Replace src/App.jsx with:
src/App.jsx
import "@picocss/pico";
import { useState, useEffect } from "react";
import { PrldSessionClient } from "@prelude.so/js-sdk";

const client = new PrldSessionClient({ domain: `${import.meta.env.VITE_APP_ID}.session.prelude.dev` });

export default function App() {
  const [loggedIn, setLoggedIn] = useState(false);
  const [sessions, setSessions] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchSessions = async () => {
    const { sessions } = await client.listSessions();
    setSessions(sessions);
  };

  useEffect(() => {
    client.refresh()
      .then(() => { setLoggedIn(true); return fetchSessions(); })
      .catch(() => {})
      .finally(() => setLoading(false));
  }, []);

  const handleRevoke = async (target, sessionId) => {
    setError(null);
    try {
      await client.revokeSessions(target, sessionId);
      if (target === "all" || target === "mine") {
        setLoggedIn(false);
        setSessions([]);
      } else {
        await fetchSessions();
      }
    } catch (err) {
      setError(err.message || "Failed to revoke session.");
    }
  };

  if (loading) return <p aria-busy="true">Loading...</p>;
  if (!loggedIn) return <p>Please log in first using one of the login examples.</p>;

  return (
    <>
      <style>{`body, #root { display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; padding: 0; }`}</style>
      <main style={{ maxWidth: "800px", width: "100%" }}>
        <h1>Session Revocation</h1>
        {error && <p role="alert" style={{ color: "var(--pico-color-red-500)" }}>{error}</p>}
        <div style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem" }}>
          <button onClick={() => handleRevoke("others")}>Revoke Others</button>
          <button className="secondary" onClick={() => handleRevoke("mine")}>Revoke Mine</button>
          <button className="contrast" onClick={() => handleRevoke("all")}>Revoke All</button>
        </div>
        <table>
          <thead>
            <tr>
              <th>Session ID</th>
              <th>Device</th>
              <th>Country</th>
              <th>Last Seen</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            {sessions.map((s) => (
              <tr key={s.id}>
                <td><code>{s.id}</code></td>
                <td>{s.device_model || s.device_type || "-"}</td>
                <td>{s.country_code || "-"}</td>
                <td>{new Date(s.last_seen_at).toLocaleString()}</td>
                <td>
                  <button className="outline" onClick={() => handleRevoke("session", s.id)}>
                    Revoke
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </main>
    </>
  );
}

Verify access tokens on your backend

Your backend should verify the JWT access token on each authenticated request. Retrieve the public keys from your application’s JWKS endpoint:
https://{app_id}.session.prelude.dev/.well-known/jwks.json
Use any standard JWT library to verify the token signature against these keys.