Authenticated App
Build a complete application with OAuth authentication, protected routes, and session management using the Deva SDK.
What You'll Build
A React application demonstrating:
OAuth 2.0 + OIDC authentication with PKCE security
Protected routes requiring authentication
Public routes accessible to everyone
Session persistence across page refreshes
Auto-redirect to intended destination after login
User profile display and management
Logout handling with token revocation
Example Structure
example-authenticated-app/
├── src/
│ ├── components/
│ │ └── ProtectedRoute.tsx # Route protection wrapper
│ ├── pages/
│ │ ├── LandingPage.tsx # Public home page
│ │ ├── AboutPage.tsx # Public about page
│ │ ├── LoginPage.tsx # Login page
│ │ ├── DashboardPage.tsx # Protected dashboard
│ │ ├── ProfilePage.tsx # Protected profile
│ │ └── SettingsPage.tsx # Protected settings
│ ├── App.tsx # Main app with routing
│ └── main.tsx # App entry point
└── package.jsonRoutes
Public Routes (accessible without login):
/- Landing page/about- About page/login- Login page
Protected Routes (require authentication):
/dashboard- Personal dashboard/profile- User profile/settings- Account settings
Code for Each Page
App.tsx - Main Application
import "@bitplanet/deva-sdk/style.css";
import { DevaProvider } from "@bitplanet/deva-sdk";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { LandingPage } from "./pages/LandingPage";
import { AboutPage } from "./pages/AboutPage";
import { LoginPage } from "./pages/LoginPage";
import { DashboardPage } from "./pages/DashboardPage";
import { ProfilePage } from "./pages/ProfilePage";
import { SettingsPage } from "./pages/SettingsPage";
export default function App() {
return (
<DevaProvider
clientId={import.meta.env.VITE_DEVA_CLIENT_ID}
redirectUri={window.location.origin}
env={import.meta.env.VITE_DEVA_ENV}
>
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/" element={<LandingPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/login" element={<LoginPage />} />
{/* Protected routes */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<ProfilePage />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<SettingsPage />
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
</DevaProvider>
);
}ProtectedRoute.tsx - Route Protection
import { ReactNode } from "react";
import { useDeva } from "deva-sdk";
import { Navigate, useLocation } from "react-router-dom";
export function ProtectedRoute({ children }: { children: ReactNode }) {
const { isReady, isAuthenticated } = useDeva();
const location = useLocation();
if (!isReady) {
return (
<div className="flex items-center justify-center h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4" />
<p className="text-gray-600">Loading...</p>
</div>
</div>
);
}
if (!isAuthenticated) {
sessionStorage.setItem("redirectAfterLogin", location.pathname);
return <Navigate to="/login" replace />;
}
return <>{children}</>;
}LandingPage.tsx - Home Page
import { useDeva } from "@bitplanet/deva-sdk";
export function LandingPage() {
const { isAuthenticated, user } = useDeva();
return (
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white">
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-xl font-bold">My App</h1>
<div className="flex items-center gap-4">
<a href="/about" className="text-gray-600 hover:text-gray-900">
About
</a>
{isAuthenticated ? (
<a
href="/dashboard"
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
>
Dashboard
</a>
) : (
<a
href="/login"
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
>
Sign In
</a>
)}
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto px-4 py-16 text-center">
<h2 className="text-5xl font-bold mb-6">Welcome to My App</h2>
<p className="text-xl text-gray-600 mb-8">
Build amazing applications with Deva SDK
</p>
{isAuthenticated && user && (
<p className="text-gray-700">
Welcome back, <strong>{user.persona?.display_name || user.name}</strong>!
</p>
)}
</main>
</div>
);
}
AboutPage.tsx - About Page
export function AboutPage() {
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-xl font-bold">About</h1>
<a href="/" className="text-gray-600 hover:text-gray-900">
Home
</a>
</div>
</nav>
<main className="max-w-3xl mx-auto px-4 py-8">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold mb-4">About My App</h2>
<p className="text-gray-700 mb-4">
This is a demonstration of authentication patterns using the @10planet/deva-sdk.
</p>
<p className="text-gray-700">
Features include:
</p>
<ul className="list-disc list-inside text-gray-700 mt-2 space-y-1">
<li>OAuth 2.0 + OIDC authentication with PKCE</li>
<li>Protected and public routes</li>
<li>Session persistence across page refreshes</li>
<li>Auto-redirect after login</li>
<li>User profile display</li>
<li>Logout handling</li>
</ul>
</div>
</main>
</div>
);
}
LoginPage.tsx - Login Page
import { useDeva } from "@bitplanet/deva-sdk";
import { Navigate } from "react-router-dom";
export function LoginPage() {
const { isReady, isAuthenticated, login } = useDeva();
if (isAuthenticated) {
const redirectTo = sessionStorage.getItem("redirectAfterLogin") || "/dashboard";
sessionStorage.removeItem("redirectAfterLogin");
return <Navigate to={redirectTo} replace />;
}
const handleLogin = async () => {
await login();
};
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold mb-2">Sign In</h1>
<p className="text-gray-600">
Sign in with your Deva account to continue
</p>
</div>
<button
onClick={handleLogin}
disabled={!isReady}
className="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition"
>
{isReady ? "Sign in with Deva" : "Loading..."}
</button>
<p className="text-center text-sm text-gray-600 mt-6">
Don't have an account?{" "}
<a
href="https://deva.me"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
Sign up on Deva
</a>
</p>
</div>
</div>
);
}
DashboardPage.tsx - Protected Dashboard
import { useDeva } from "@bitplanet/deva-sdk";
export function DashboardPage() {
const { user, logout } = useDeva();
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-xl font-bold">Dashboard</h1>
<div className="flex items-center gap-4">
<a href="/profile" className="text-gray-600 hover:text-gray-900">
Profile
</a>
<a href="/settings" className="text-gray-600 hover:text-gray-900">
Settings
</a>
<button
onClick={logout}
className="text-gray-600 hover:text-gray-900"
>
Logout
</button>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto px-4 py-8">
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h2 className="text-2xl font-bold mb-4">
Welcome, {user?.persona?.display_name || user?.name}!
</h2>
<p className="text-gray-600">
This is your personal dashboard.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="font-semibold mb-2">Total Posts</h3>
<p className="text-3xl font-bold text-blue-600">24</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="font-semibold mb-2">Followers</h3>
<p className="text-3xl font-bold text-green-600">1,234</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="font-semibold mb-2">Following</h3>
<p className="text-3xl font-bold text-purple-600">567</p>
</div>
</div>
</main>
</div>
);
}ProfilePage.tsx - User Profile
import { useDeva } from "@bitplanet/deva-sdk";
export function ProfilePage() {
const { user, logout } = useDeva();
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-xl font-bold">Profile</h1>
<div className="flex items-center gap-4">
<a href="/dashboard" className="text-gray-600 hover:text-gray-900">
Dashboard
</a>
<button
onClick={logout}
className="text-gray-600 hover:text-gray-900"
>
Logout
</button>
</div>
</div>
</nav>
<main className="max-w-3xl mx-auto px-4 py-8">
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center gap-4 mb-6">
<img
src={user?.persona?.avatar || user?.picture || ''}
alt={user?.persona?.username || user?.preferred_username || 'User avatar'}
className="w-20 h-20 rounded-full"
/>
<div>
<h2 className="text-2xl font-bold">
{user?.persona?.display_name || user?.name}
</h2>
<p className="text-gray-600">@{user?.persona?.username || user?.preferred_username}</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<p className="text-gray-900">
{user?.email || "Not provided"}
{user?.email_verified && (
<span className="ml-2 text-green-600">✓ Verified</span>
)}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
User ID
</label>
<p className="text-gray-900 font-mono text-sm">{user?.id}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<p className="text-gray-900">
{user?.persona?.description || user?.description || "No description provided"}
</p>
</div>
</div>
</div>
</main>
</div>
);
}SettingsPage.tsx - Account Settings
import { useDeva } from "@bitplanet/deva-sdk";
export function SettingsPage() {
const { logout } = useDeva();
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-xl font-bold">Settings</h1>
<div className="flex items-center gap-4">
<a href="/dashboard" className="text-gray-600 hover:text-gray-900">
Dashboard
</a>
<button
onClick={logout}
className="text-gray-600 hover:text-gray-900"
>
Logout
</button>
</div>
</div>
</nav>
<main className="max-w-3xl mx-auto px-4 py-8">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold mb-6">Account Settings</h2>
<div className="space-y-6">
<div>
<h3 className="font-semibold mb-2">Notifications</h3>
<label className="flex items-center">
<input type="checkbox" className="mr-2" />
<span>Email notifications</span>
</label>
</div>
<div>
<h3 className="font-semibold mb-2">Privacy</h3>
<label className="flex items-center">
<input type="checkbox" className="mr-2" />
<span>Private profile</span>
</label>
</div>
<div className="pt-6 border-t">
<button
onClick={logout}
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700"
>
Sign Out
</button>
</div>
</div>
</div>
</main>
</div>
);
}Setup & Installation
Prerequisites
Node.js 18+ installed
pnpm package manager
A Deva account at deva.me
Get Deva Credentials
Visit deva.me and sign in
Go to Settings → Apps
Click Create New Application
Fill in:
Name: My Authenticated App
Redirect URIs:
http://localhost:5174Origin URIs:
http://localhost:5174
Copy your Client ID
Install and Run
# Clone or navigate to example
cd example-authenticated-app
# Install dependencies
pnpm install
# Create .env file
cp .env.example .env
# Edit .env and add your Client ID
VITE_DEVA_CLIENT_ID=your_client_id_here
# Start dev server
pnpm run devOpen http://localhost:5174 in your browser.
Test the App
Click Sign In button
Authenticate with your Deva account
Explore:
Dashboard - View your stats
Profile - See your user information
Settings - Manage preferences
Click Logout to end session
Notes
OAuth flow uses PKCE for security
Tokens stored in sessionStorage (cleared on browser close)
Authentication state persists across page refreshes within same session
isReadyprevents flash of incorrect auth state during initializationLogout revokes tokens server-side and clears local storage
Redirect URI must match exactly what's configured in Deva app settings
Related Documentation
OAuth Integration - OAuth 2.0 + OIDC details
useDeva Hook - Authentication hook reference
Session Persistence - Token storage
Login Implementation - Login patterns
Logout Handling - Logout patterns
Last updated