Skip to main content

Authentication Guide

Comprehensive guide to implementing authentication with the Renown SDK.

Table of Contents

Overview

The Renown SDK provides a complete authentication system that:

  1. Auto-initializes the Renown SDK and ConnectCrypto (zero configuration!)
  2. Manages user sessions across page reloads
  3. Handles authentication redirects from Renown portal
  4. Fetches user profile data automatically
  5. Provides React hooks, components, and context for easy integration

Authentication Flow

1. Initial Load

User visits app

RenownUserProvider initializes

Check sessionStorage for existing session

├─ Session found → Restore user → Authorized
└─ No session → Check URL params → Initialize SDK → Not Authorized

2. Login Flow

User clicks "Login"

openRenown() called

Redirect to Renown Portal

User authenticates

Redirect back to app with DID

handleRenownReturn() processes

login() called

Fetch user profile

Store in sessionStorage

Update auth state → Authorized

In-page sign-in (wallet adapters)

The redirect above is the default. A host app can instead sign in in-page by configuring a Switchboard URL and one or more wallet adapters (RainbowKit for external wallets, Privy for social/email). The user picks a method inside the app, signs the credential with the resulting wallet session, and renown.signIn(session) writes and logs it in — no redirect. It falls back to the redirect flow when no switchboard/adapter is available. See Renown authentication flow → In-page sign-in in Connect and the @renown/sdk/wallet section of the SDK README for configuration.

3. Session Restoration

User refreshes page

RenownUserProvider checks sessionStorage

Valid session found

Fetch latest profile data

Restore auth state → Authorized

Setup

Step 1: Wrap Your App with RenownUserProvider

The RenownUserProvider automatically initializes the Renown SDK - no manual setup required!

// app/layout.tsx (Next.js App Router)
import { RenownUserProvider } from '@renown/sdk'

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<RenownUserProvider>
{children}
</RenownUserProvider>
</body>
</html>
)
}

That's it! The SDK is now initialized and ready to use.

Optional: Customize Configuration

You can customize the Renown URL, network, and chain:

<RenownUserProvider
renownUrl={process.env.NEXT_PUBLIC_RENOWN_URL || 'https://www.renown.id'}
networkId="eip155"
chainId="1"
>
{children}
</RenownUserProvider>

Optional: Add Loading/Error Screens

Provide custom UI for initialization states:

<RenownUserProvider
loadingComponent={
<div className="loading-screen">
<Spinner />
<p>Initializing authentication...</p>
</div>
}
errorComponent={(error, retry) => (
<div className="error-screen">
<h2>Authentication Failed</h2>
<p>{error.message}</p>
<button onClick={retry}>Try Again</button>
</div>
)}
>
{children}
</RenownUserProvider>

Implementation

Using the RenownAuthButton Component

The simplest way to add authentication is to use the built-in RenownAuthButton component:

'use client'

import { RenownAuthButton } from '@renown/sdk'

export function Header() {
return (
<header>
<h1>My App</h1>
<RenownAuthButton showLogoutButton showUsername />
</header>
)
}

Custom Login Component with useUser Hook

For full control, build your own component using the useUser hook:

'use client'

import { useUser } from '@renown/sdk'

export function CustomRenownAuthButton() {
const { user, loginStatus, isLoading, openRenown, logout } = useUser()

if (isLoading) {
return (
<button disabled>
<Spinner />
Loading...
</button>
)
}

if (loginStatus === 'authorized' && user) {
return (
<div className="flex items-center gap-2">
{user.avatar && (
<img
src={user.avatar}
alt={user.name || 'User'}
className="w-8 h-8 rounded-full"
/>
)}
<span>{user.name || user.did.slice(0, 15)}...</span>
<button onClick={logout}>Logout</button>
</div>
)
}

return (
<button onClick={openRenown}>
Login with Renown
</button>
)
}

Protected Route Component

'use client'

import { useUser } from '@renown/sdk'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, loginStatus, isLoading, openRenown } = useUser()
const router = useRouter()

useEffect(() => {
if (!isLoading && loginStatus !== 'authorized') {
// Redirect to login or show login prompt
router.push('/login')
}
}, [isLoading, loginStatus, router])

if (isLoading) {
return <div>Loading...</div>
}

if (loginStatus !== 'authorized' || !user) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h1>Authentication Required</h1>
<p>Please log in to access this page</p>
<button onClick={openRenown}>Login</button>
</div>
)
}

return <>{children}</>
}

User Profile Display

'use client'

import { useUser } from '@renown/sdk'

export function UserProfile() {
const { user, logout } = useUser()

if (!user) return null

return (
<div className="profile-card">
<header>
{user.avatar && (
<img
src={user.avatar}
alt={user.name || 'User avatar'}
className="avatar"
/>
)}
<h2>{user.name || 'Anonymous User'}</h2>
</header>

<dl className="profile-details">
<dt>DID</dt>
<dd>{user.did}</dd>

{user.ethAddress && (
<>
<dt>Ethereum Address</dt>
<dd>{user.ethAddress}</dd>
</>
)}

{user.email && (
<>
<dt>Email</dt>
<dd>{user.email}</dd>
</>
)}
</dl>

<button onClick={logout}>Logout</button>
</div>
)
}

Advanced Patterns

Conditional Navigation Based on Auth

'use client'

import { useUser } from '@renown/sdk'
import Link from 'next/link'

export function Navigation() {
const { user, loginStatus } = useUser()

const isAuthorized = loginStatus === 'authorized'

return (
<nav>
<Link href="/">Home</Link>

{isAuthorized ? (
<>
<Link href="/dashboard">Dashboard</Link>
<Link href="/profile">Profile</Link>
<Link href="/settings">Settings</Link>
</>
) : (
<>
<Link href="/about">About</Link>
<Link href="/pricing">Pricing</Link>
</>
)}
</nav>
)
}

Auth State Listener

"use client";

import { useUser } from "@renown/sdk";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { toast } from "react-hot-toast";

export function AuthStateListener() {
const { user, loginStatus } = useUser();
const router = useRouter();

useEffect(() => {
if (loginStatus === "authorized" && user) {
// User just logged in
toast.success(`Welcome back, ${user.name || "User"}!`);

// Track analytics
analytics.identify(user.did, {
name: user.name,
ethAddress: user.ethAddress,
});

// Redirect to dashboard
router.push("/dashboard");
}
}, [loginStatus, user, router]);

useEffect(() => {
if (loginStatus === "not-authorized") {
// User logged out
toast.info("You have been logged out");

// Clear analytics
analytics.reset();

// Redirect to home
router.push("/");
}
}, [loginStatus, router]);

return null; // This is a listener component
}

Custom Auth Hook with Additional Logic

"use client";

import { useUser as useRenownAuth } from "@renown/sdk";
import { useEffect, useState } from "react";

interface ExtendedAuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
hasCompletedProfile: boolean;
login: () => void;
logout: () => Promise<void>;
}

export function useUser(): ExtendedAuthState {
const auth = useRenownAuth();
const [hasCompletedProfile, setHasCompletedProfile] = useState(false);

useEffect(() => {
if (auth.user) {
// Check if user has completed their profile
const isComplete = !!(
auth.user.name &&
auth.user.avatar &&
auth.user.email
);
setHasCompletedProfile(isComplete);
} else {
setHasCompletedProfile(false);
}
}, [auth.user]);

return {
user: auth.user,
isAuthenticated: auth.loginStatus === "authorized",
isLoading: auth.isLoading,
hasCompletedProfile,
login: auth.openRenown,
logout: auth.logout,
};
}

Role-Based Access Control

'use client'

import { useUser } from '@renown/sdk'
import { ReactNode } from 'react'

interface RBACProps {
children: ReactNode
allowedRoles?: string[]
fallback?: ReactNode
}

export function RoleBasedAccess({
children,
allowedRoles = [],
fallback = <div>Access Denied</div>
}: RBACProps) {
const { user, loginStatus } = useUser()

if (loginStatus !== 'authorized' || !user) {
return fallback
}

// Check user roles (you'd fetch this from your backend)
const userRoles = getUserRoles(user.did) // Implement this

const hasAccess = allowedRoles.length === 0 ||
allowedRoles.some(role => userRoles.includes(role))

return hasAccess ? <>{children}</> : fallback
}

// Usage
function AdminPanel() {
return (
<RoleBasedAccess allowedRoles={['admin', 'moderator']}>
<h1>Admin Panel</h1>
{/* Admin content */}
</RoleBasedAccess>
)
}

Security Considerations

1. Token Storage

The SDK stores session data in sessionStorage (not localStorage) for security:

  • ✅ Session data clears when tab closes
  • ✅ Not accessible across tabs
  • ✅ Not persisted across browser sessions
  • ✅ Protected from XSS via httpOnly (for API tokens)

2. DID Validation

Always validate DIDs before processing:

function isValidDID(did: string): boolean {
// Must start with did:pkh:
if (!did.startsWith("did:pkh:")) return false;

// Must have correct number of parts
const parts = did.split(":");
if (parts.length !== 5) return false;

// Validate ethereum address format
const address = parts[4];
if (!address.match(/^0x[a-fA-F0-9]{40}$/)) return false;

return true;
}

3. Secure Communication

Always use HTTPS for Renown URLs:

const RENOWN_URL = process.env.NEXT_PUBLIC_RENOWN_URL;

if (RENOWN_URL && !RENOWN_URL.startsWith("https://")) {
console.warn("WARNING: Renown URL should use HTTPS");
}

4. Session Timeout

Implement session timeout for security:

const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours

function isSessionValid(timestamp: number): boolean {
const now = Date.now();
const age = now - timestamp;
return age < SESSION_TIMEOUT;
}

5. Server-Side Verification

Never trust client-side auth alone. To verify a raw bearer token (e.g. from an Authorization header), use verifyAuthBearerToken from @renown/sdk/node:

import { verifyAuthBearerToken } from "@renown/sdk/node";

export async function GET(request: Request) {
const token = request.headers.get("authorization")?.replace(/^Bearer /, "");
if (!token) return new Response("Unauthorized", { status: 401 });

const verified = await verifyAuthBearerToken(token);
if (!verified) return new Response("Invalid credentials", { status: 403 });

const { address } = verified.verifiableCredential.credentialSubject;
return Response.json({ address });
}

To render authenticated pages on the server (Next.js and similar):

  1. On login the client mints a bearer token (renown.getBearerToken()) and stores it — with a RenownSessionProfile display hint (name/avatar plus documentId/username/userImage) — in an HttpOnly cookie (name exported as RENOWN_SESSION_COOKIE, value built with serializeRenownSessionCookie). RenownProvider's session-cookie sync does this automatically when given a session. Type your route handler with the exported RenownSessionCookie instead of redeclaring the payload shape.
  2. A Data Access Layer reads the cookie and calls verifyRenownSession, memoized per request:
import { RENOWN_SESSION_COOKIE, verifyRenownSession } from "@renown/sdk/node";
import { cookies } from "next/headers";
import { cache } from "react";

export const verifySession = cache(async () => {
const cookie = (await cookies()).get(RENOWN_SESSION_COOKIE)?.value;
if (!cookie) return null;
return (await verifyRenownSession(cookie, { switchboardUrl: SWITCHBOARD_URL })) ?? null;
});

verifyRenownSession takes the cookie value, verifies the JWT (signature + expiry), and merges the display hint into user (unverified — display only), rebuilding user.ens and, when documentId is present, user.profile. Pass verifyCredential: true to also re-check the credential against the switchboard so a revoked/expired credential is rejected; it defaults to false (token-only, no network). readSessionClaims(cookieValue) gives a cheap, signature-free decode for optimistic checks only (e.g. an edge redirect) — never trust it for access control.

  1. Pass the result to RenownProvider session={await verifySession()} so the server render is authenticated (no flash), and consumers read it via useRenownAuth().

A null session is as useful as a populated one: it tells the first render the visitor is definitively signed out, so useRenownAuthAsync() reports "unauthenticated" immediately instead of spinning. The cookie seeds the server render and hydration; localStorage takes over after mount, since that holds the credential the SDK restores.

Revalidation and profile refresh on the client

On mount the client re-checks the restored credential against the switchboard and logs out if it was revoked/expired (revalidate prop on RenownProvider, default "always"). In the browser this is non-blocking and fail-open (a transient outage keeps the session); Node scripts block on it. renown.revalidate() runs the same check on demand.

Separately, renown.refreshProfile() re-reads username/userImage and updates the store only if they changed. It runs on every browser start regardless of revalidate, because it can never log anyone out — so apps that set revalidate: "never" still pick up a renamed user or a new avatar.

Best Practices

1. Handle Loading States

Always handle loading states to provide good UX:

function MyComponent() {
const { user, isLoading, loginStatus } = useUser()

if (isLoading) {
return <Skeleton /> // Show skeleton/spinner
}

// Now safe to use user/loginStatus
}

2. Graceful Degradation

Provide fallbacks for unauthenticated users:

function FeatureSection() {
const { user } = useUser()

return (
<section>
{user ? (
<PersonalizedContent user={user} />
) : (
<GenericContent />
)}
</section>
)
}

3. Cleanup on Unmount

Clean up subscriptions and listeners:

useEffect(() => {
const handleAuthChange = () => {
// Handle auth changes
};

// Subscribe to auth events
const unsubscribe = subscribeToAuthEvents(handleAuthChange);

return () => {
unsubscribe(); // Cleanup
};
}, []);

4. Error Boundaries

Wrap auth components in error boundaries:

<ErrorBoundary fallback={<AuthError />}>
<RenownUserProvider>
<App />
</RenownUserProvider>
</ErrorBoundary>

Troubleshooting

Issue: Auth state not updating

Cause: Component not re-rendering when auth changes

Solution: Ensure you're using useUser() hook, not accessing window.renown directly

// ❌ Wrong
const user = window.renown?.user;

// ✅ Correct
const { user } = useUser();

Issue: Session not persisting

Cause: SessionStorage might be disabled or cleared

Solution: Check browser settings and handle gracefully

try {
SessionStorageManager.setUserData(data);
} catch (error) {
console.warn("SessionStorage not available:", error);
// Fallback to in-memory storage
}

Issue: Multiple login popups

Cause: openRenown() called multiple times

Solution: Debounce the login button

const handleLogin = useCallback(
debounce(() => {
openRenown();
}, 1000),
[openRenown],
);

Next Steps