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.json

Routes

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

  1. Visit deva.me and sign in

  2. Go to Settings → Apps

  3. Click Create New Application

  4. Fill in:

    • Name: My Authenticated App

    • Redirect URIs: http://localhost:5174

    • Origin URIs: http://localhost:5174

  5. 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 dev

Open http://localhost:5174 in your browser.

Test the App

  1. Click Sign In button

  2. Authenticate with your Deva account

  3. Explore:

    • Dashboard - View your stats

    • Profile - See your user information

    • Settings - Manage preferences

  4. 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

  • isReady prevents flash of incorrect auth state during initialization

  • Logout revokes tokens server-side and clears local storage

  • Redirect URI must match exactly what's configured in Deva app settings


Last updated