Overview
There are three ways to implement authentication in your application, each offering different levels of customization and control:AuthButtoncomponent from@coinbase/cdp-react: Pre-built UI component (fastest integration)- React hooks from
@coinbase/cdp-hooks: For custom React UIs with state management - Direct methods from
@coinbase/cdp-core: For vanilla JavaScript/TypeScript or non-React frameworks
Important authentication considerations:
- Always check if a user is already signed in before starting a new authentication flow. Attempting to call
verifyEmailOTPor ‘verifySMSOTP` while a user is already authenticated will result in an error and may leave the application in an inconsistent state. - To sign out users, use the
signOut()method from@coinbase/cdp-core, theuseSignOut()hook from@coinbase/cdp-hooks, or theAuthButtoncomponent which handles sign out automatically.
AuthButton component (simplest)
For the fastest integration,@coinbase/cdp-react provides a pre-built AuthButton component that handles the entire authentication flow with a single line of code.
For more CDP React components and styling options, see the React Components documentation.By default, email authentication is the only method enabled. For enabling additional methods, refer to the AppConfig documentation
import { type Config, CDPReactProvider } from "@coinbase/cdp-react";
import { AuthButton } from "@coinbase/cdp-react/components/AuthButton";
const config: Config = {
projectId: "your-project-id",
ethereum: {
createOnLogin: "eoa",
},
appName: "React Library Demo",
appLogoUrl: "https://picsum.photos/64",
// Enabled authentication methods
authMethods: ["email", "sms", "oauth:google"],
};
function App() {
return (
<CDPReactProvider config={config}>
<YourApp />
</CDPReactProvider>
);
}
import { AuthButton } from "@coinbase/cdp-react/components/AuthButton";
function SignInScreen() {
return (
<main>
<h1>Welcome!</h1>
<p>Please sign in to continue.</p>
<AuthButton />
</main>
);
}
import { useIsInitialized, useIsSignedIn } from "@coinbase/cdp-hooks";
import { AuthButton } from "@coinbase/cdp-react/components/AuthButton";
function App() {
const { isInitialized } = useIsInitialized();
const { isSignedIn } = useIsSignedIn();
if (!isInitialized) {
return <div>Loading...</div>;
}
return (
<div>
<header>
<h1>My App</h1>
<AuthButton /> {/* Shows "Sign In" or "Sign Out" automatically */}
</header>
<main>
{!isSignedIn && <p>Please sign in to access your wallet</p>}
{isSignedIn && <p>Welcome! You're signed in.</p>}
</main>
</div>
);
}
import { useEvmAddress } from "@coinbase/cdp-hooks";
import { AuthButton } from "@coinbase/cdp-react/components/AuthButton";
function Header() {
const { evmAddress } = useEvmAddress();
return (
<header>
<h1>CDP App</h1>
<div className="user-info">
{evmAddress && (
<span>
{evmAddress.slice(0, 6)}...{evmAddress.slice(-4)}
</span>
)}
<AuthButton />
</div>
</header>
);
}
The
AuthButton component automatically:- Shows “Sign In” when the user is not authenticated
- Shows “Sign Out” when the user is authenticated
- Handles the entire Email or SMS OTP flow internally
- Manages loading and error states
- Follows your theme configuration
React hooks
For React applications,@coinbase/cdp-hooks provides convenient hooks that handle state management and re-renders automatically.
import { CDPHooksProvider } from "@coinbase/cdp-hooks";
import { type Config } from "@coinbase/cdp-core";
const config: Config = {
projectId: "your-project-id",
ethereum: {
createOnLogin: "eoa",
},
};
// Wrap your app with the provider
function App() {
return (
<CDPHooksProvider config={config}>
<YourApp />
</CDPHooksProvider>
);
}
| Option | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Your CDP project ID |
ethereum.createOnLogin | "eoa" | "smart" | No | Auto-create an EVM account on sign-in |
solana.createOnLogin | boolean | No | Auto-create a Solana account on sign-in |
basePath | string | No | Override the CDP API base URL |
useMock | boolean | No | Use mock data instead of live APIs (local testing) |
debugging | boolean | No | Log all API requests and responses |
import { useSignInWithEmail, useVerifyEmailOTP } from "@coinbase/cdp-hooks";
function SignInComponent() {
const { signInWithEmail } = useSignInWithEmail();
const { verifyEmailOTP } = useVerifyEmailOTP();
const [flowId, setFlowId] = useState<string | null>(null);
const handleEmailSubmit = async (email: string) => {
try {
// Start sign in flow
const { flowId } = await signInWithEmail({ email });
setFlowId(flowId);
// Show OTP input UI
} catch (error) {
console.error("Failed to send OTP:", error);
}
};
const handleOTPSubmit = async (otp: string) => {
if (!flowId) return;
try {
// Complete sign in
const { user, isNewUser } = await verifyEmailOTP({
flowId,
otp
});
console.log("Signed in user:", user);
console.log("User EVM address:", user.evmAccounts[0]);
console.log("Is new user:", isNewUser);
} catch (error) {
console.error("Sign in failed:", error);
}
};
// Return your UI components...
}
import { useSignInWithSms, useVerifySmsOTP } from "@coinbase/cdp-hooks";
function SignInComponent() {
const { signInWithSms } = useSignInWithSms();
const { verifySmsOTP } = useVerifySmsOTP();
const [flowId, setFlowId] = useState<string | null>(null);
const handlePhoneNumberSubmit = async (phoneNumber: string) => {
try {
// Start sign in flow with E.164 formatted phone number (ex. +15554443333)
const { flowId } = await signInWithSms({ phoneNumber });
setFlowId(flowId);
// Show OTP input UI
} catch (error) {
console.error("Failed to send OTP:", error);
}
};
const handleOTPSubmit = async (otp: string) => {
if (!flowId) return;
try {
// Complete sign in
const { user, isNewUser } = await verifySmsOTP({
flowId,
otp
});
console.log("Signed in user:", user);
console.log("User EVM address:", user.evmAccounts[0]);
console.log("Is new user:", isNewUser);
} catch (error) {
console.error("Sign in failed:", error);
}
};
// Return your UI components...
}
import { useSignInWithOAuth } from "@coinbase/cdp-hooks";
function SignInComponent() {
const { signInWithOAuth } = useSignInWithOAuth();
const handleGoogleSignIn = () => {
// User will be redirected to Google to complete their login
void signInWithOAuth("google");
// Post-login, they will be redirected back to your app, and the login process
// will be completed automatically.
}
// Return your UI components...
}
import { useSignInWithOAuth } from "@coinbase/cdp-hooks";
function SignInComponent() {
const { signInWithOAuth } = useSignInWithOAuth();
const handleAppleSignIn = () => {
// User will be redirected to Apple to complete their login
void signInWithOAuth("apple");
// Post-login, they will be redirected back to your app, and the login process
// will be completed automatically.
}
// Return your UI components...
}
import { useSignInWithOAuth } from "@coinbase/cdp-hooks";
function SignInComponent() {
const { signInWithOAuth } = useSignInWithOAuth();
const handleXSignIn = () => {
// User will be redirected to X to complete their login
void signInWithOAuth("x");
// Post-login, they will be redirected back to your app, and the login process
// will be completed automatically.
}
// Return your UI components...
}
import { useCurrentUser, useIsSignedIn } from "@coinbase/cdp-hooks";
function UserProfile() {
const { user } = useCurrentUser();
const { isSignedIn } = useIsSignedIn();
if (!isSignedIn) {
return <div>Please sign in</div>;
}
return (
<div>
<h2>Welcome!</h2>
<p>User ID: {user?.userId}</p>
<p>Email: {user?.authenticationMethods.email?.email}</p>
<p>Wallet Address: {user?.evmAccounts[0]}</p>
</div>
);
}
import React, { useState } from 'react';
import { useSignInWithEmail, useVerifyEmailOTP, useIsSignedIn } from "@coinbase/cdp-hooks";
export function AuthenticationFlow() {
const { signInWithEmail, loading: emailLoading } = useSignInWithEmail();
const { verifyEmailOTP, loading: otpLoading } = useVerifyEmailOTP();
const { isSignedIn } = useIsSignedIn();
const [email, setEmail] = useState('');
const [otp, setOtp] = useState('');
const [flowId, setFlowId] = useState<string | null>(null);
const [step, setStep] = useState<'email' | 'otp'>('email');
if (isSignedIn) {
return <div>You are already signed in!</div>;
}
const handleEmailSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const result = await signInWithEmail({ email });
setFlowId(result.flowId);
setStep('otp');
} catch (error) {
console.error('Failed to send OTP:', error);
}
};
const handleOTPSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!flowId) return;
try {
const { user } = await verifyEmailOTP({ flowId, otp });
console.log('Authentication successful!', user);
} catch (error) {
console.error('Failed to verify OTP:', error);
}
};
if (step === 'email') {
return (
<form onSubmit={handleEmailSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
disabled={emailLoading}
/>
<button type="submit" disabled={emailLoading}>
{emailLoading ? 'Sending...' : 'Send OTP'}
</button>
</form>
);
}
return (
<form onSubmit={handleOTPSubmit}>
<p>Enter the code sent to {email}</p>
<input
type="text"
value={otp}
onChange={(e) => setOtp(e.target.value)}
placeholder="123456"
maxLength={6}
required
disabled={otpLoading}
/>
<button type="submit" disabled={otpLoading}>
{otpLoading ? 'Verifying...' : 'Verify'}
</button>
</form>
);
}
The React hooks automatically handle loading states, error states, and re-renders when authentication state changes. They’re the recommended approach for React applications.
Direct methods
The@coinbase/cdp-core package provides the low-level authentication primitives for maximum control over the user experience. This approach is ideal for non-React applications or when you need fine-grained control.
import { initialize, signInWithEmail, verifyEmailOTP } from '@coinbase/cdp-core';
// Step 1: Initialize the CDP SDK
await initialize({
projectId: 'your-project-id'
});
// Step 2: Initiate email authentication
const { flowId, message } = await signInWithEmail({
email: 'user@example.com'
});
console.log(message); // "OTP sent to user@example.com"
// Step 3: Verify the OTP code
const { user, isNewUser } = await verifyEmailOTP({
flowId,
otp: '123456'
});
// User is now authenticated and has access to their wallet
console.log('User ID:', user.userId);
console.log('EVM Addresses:', user.evmAccounts);
console.log('Is new user:', isNewUser);
import { initialize, signInWithSms, verifySmsOTP } from '@coinbase/cdp-core';
// Step 1: Initialize the CDP SDK
await initialize({
projectId: 'your-project-id'
});
// Step 2: Initiate SMS authentication
const { flowId, message } = await signInWithSms({
// E.164 format
phoneNumber: '+15554443333'
});
console.log(message); // "Authentication initiated successfully. Please check your phone for the verification code."
// Step 3: Verify the OTP code
const { user, isNewUser } = await verifySmsOTP({
flowId,
otp: '123456'
});
// User is now authenticated and has access to their wallet
console.log('User ID:', user.userId);
console.log('EVM Addresses:', user.evmAccounts);
console.log('Is new user:', isNewUser);
import { initialize, signInWithOAuth } from '@coinbase/cdp-core';
// Step 1: Initialize the CDP SDK
await initialize({
projectId: 'your-project-id'
});
// Step 2: Initiate Google OAuth sign-in
// User will be redirected to Google to complete their login
// After login, they will be redirected back to your app, and the login
// process will be completed automatically by the SDK
try {
void signInWithOAuth("google");
} catch (error) {
console.error("Failed to sign in with Google:", error);
}
import { initialize, signInWithOAuth } from '@coinbase/cdp-core';
// Step 1: Initialize the CDP SDK
await initialize({
projectId: 'your-project-id'
});
// Step 2: Initiate Apple OAuth sign-in
// User will be redirected to Apple to complete their login
// After login, they will be redirected back to your app, and the login
// process will be completed automatically by the SDK
try {
void signInWithOAuth("apple");
} catch (error) {
console.error("Failed to sign in with Apple:", error);
}
import { initialize, signInWithOAuth } from '@coinbase/cdp-core';
// Step 1: Initialize the CDP SDK
await initialize({
projectId: 'your-project-id'
});
// Step 2: Initiate X OAuth sign-in
// User will be redirected to X to complete their login
// After login, they will be redirected back to your app, and the login
// process will be completed automatically by the SDK
try {
void signInWithOAuth("x");
} catch (error) {
console.error("Failed to sign in with X:", error);
}
import { initialize, signInWithEmail, verifyEmailOTP, APIError } from '@coinbase/cdp-core';
try {
await initialize({
projectId: 'your-project-id'
});
const { flowId } = await signInWithEmail({
email: 'user@example.com'
});
// Handle OTP verification with retry logic
let attempts = 0;
const maxAttempts = 3;
while (attempts < maxAttempts) {
try {
const { user, isNewUser } = await verifyEmailOTP({
flowId,
otp: userProvidedOTP
});
// Success!
console.log('Authentication successful:', user);
break;
} catch (error) {
attempts++;
if (error instanceof APIError) {
console.error('API Error:', error.message);
if (attempts >= maxAttempts) {
console.error('Maximum attempts reached. Please request a new OTP.');
}
} else {
throw error; // Re-throw unexpected errors
}
}
}
} catch (error) {
if (error instanceof APIError) {
// Handle specific API errors
console.error('CDP API Error:', error.message);
console.error('Error Type:', error.errorType);
} else {
console.error('Authentication failed:', error);
}
}
import { initialize, getCurrentUser, isSignedIn, signOut, onAuthStateChange } from '@coinbase/cdp-core';
// Initialize must be called before any other methods
await initialize({
projectId: 'your-project-id'
});
// Check if user is already signed in
const signedIn = await isSignedIn();
if (signedIn) {
const user = await getCurrentUser();
console.log('User already authenticated:', user?.userId);
console.log('EVM Accounts:', user?.evmAccounts);
}
// Listen for auth state changes
onAuthStateChange((user) => {
if (user) {
console.log('User signed in:', user.userId);
} else {
console.log('User signed out');
}
});
// Sign out when needed
await signOut();
Always handle authentication errors gracefully. Common errors include:
- Invalid or expired OTP codes
- Rate limiting for too many attempts
- Network connectivity issues
- Invalid project configuration
React Native Social Login configuration
OAuth authentication in React Native requires configuring a deep link callback URL:1
Set up deep linking
Add a custom scheme to your See Expo Linking docs for details.
app.json:{
"expo": {
"scheme": "my-app"
}
}
2
Add callback URL to CDP Portal
Add your deep link (e.g.,
my-app://oauth-callback) to your allowed domains.3
Configure the SDK
Add
nativeOAuthCallback to your config:const cdpConfig: Config = {
projectId: 'your-project-id',
nativeOAuthCallback: 'my-app://oauth-callback',
ethereum: { createOnLogin: 'smart' },
};
expo-web-browser to automatically handle the OAuth redirect and callback. You don’t need to add Linking.addEventListener or other deep link handling code.
Server-side validation
Some developers take additional action (fetching additional data, starting asynchronous processes) based on a user having an active session. For security reasons, it is important that you check authentication status by validating the access token Coinbase grants a user when they log in.import { useGetAccessToken, useIsSignedIn } from "@coinbase/cdp-hooks";
import { useEffect, useState } from "react";
export default function useServerSideAuth() {
const { isSignedIn } = useIsSignedIn();
const { getAccessToken } = useGetAccessToken();
const [isServerSideAuthenticated, setIsServerSideAuthenticated] = useState<boolean>(false);
// When the user signs in, we need to check if the user is authenticated on the server side.
useEffect(() => {
async function checkAuth() {
if (!isSignedIn) {
return;
}
// Retrieve the access token
const accessToken = await getAccessToken();
// Send the access token to your server to check if the user is authenticated.
const response = await fetch("/api/check-auth", {
method: "POST",
body: JSON.stringify({
accessToken,
}),
});
const { isAuthenticated, endUser, error } = await response.json();
if (isAuthenticated) {
setIsServerSideAuthenticated(true);
console.log("endUser", endUser);
} else {
setIsServerSideAuthenticated(false);
console.log("error", error);
}
}
void checkAuth();
}, [isSignedIn, getAccessToken]);
return isServerSideAuthenticated;
}
import { initialize, getAccessToken, isSignedIn } from '@coinbase/cdp-core';
// Initialize must be called before any other methods
await initialize({
projectId: 'your-project-id'
});
// Check if user is already signed in
const signedIn = await isSignedIn();
if (signedIn) {
// Get the user's access token
const accessToken = await getAccessToken();
// Send the access token to your server to check if the user is authenticated.
const response = await fetch('/api/check-auth', {
method: 'POST',
body: JSON.stringify({
accessToken,
}),
});
const { isAuthenticated, endUser, error } = await response.json();
}
// Next.js route for /api/check-auth
import { NextRequest, NextResponse } from 'next/server';
import { CdpClient } from "@coinbase/cdp-sdk";
if (!process.env.CDP_API_KEY_ID || !process.env.CDP_API_KEY_SECRET) {
throw new Error('CDP_API_KEY_ID and CDP_API_KEY_SECRET must be set');
}
const cdpClient = new CdpClient({
apiKeyId: process.env.CDP_API_KEY_ID,
apiKeySecret: process.env.CDP_API_KEY_SECRET,
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { accessToken } = body;
if (!accessToken) {
return NextResponse.json(
{ isAuthenticated: false, error: 'Missing accessToken parameter' },
{ status: 400 }
);
}
const endUser = await cdpClient.endUser.validateAccessToken({
accessToken,
});
// If we reach here, the token is valid
return NextResponse.json({ isAuthenticated: true, endUser });
} catch (error) {
const errorMessage =
(error as { errorMessage?: string }).errorMessage ??
(error as { message?: string }).message ??
"Unknown error";
// Any error means the token is invalid or there was an authentication issue
return NextResponse.json({ isAuthenticated: false, error: errorMessage });
}
}
What to read next
- Authentication Methods: Learn about email OTP, SMS OTP, and social login options
- React Hooks: Pre-built hooks for authentication and wallet management
- React Components: Ready-to-use UI components including AuthButton
- Session Management: Understand session lifecycle and token management
- Best Practices: Security recommendations and choosing the right approach