Vertueal Docs
Authentic Profile

OAuth 2.0 Integration & Trust Links

Complete RFC 6749 OAuth 2.0 specification, endpoints, scopes, and production-ready React/Next.js integration code.

OAuth 2.0 Integration & Trust Links

Authentic Profile provides a production-grade RFC 6749 OAuth 2.0 Identity Provider (IdP). Third-party web applications, recruitment portals, marketplaces, and developer platforms can securely authenticate users and verify their credentials without handling passwords or scraping HTML.


1. Endpoints & Base URLs

EnvironmentAuthorization EndpointToken Exchange EndpointUserInfo Endpoint
Productionhttps://authenticprofile.com/oauth/authorize/consenthttps://authenticprofile.com/api/oauth/tokenhttps://authenticprofile.com/api/oauth/userinfo
Local Devhttp://localhost:3000/oauth/authorize/consenthttp://localhost:3000/api/oauth/tokenhttp://localhost:3000/api/oauth/userinfo

2. OAuth 2.0 Authorization Flow

Loading diagram...

3. Scopes & Permissions

When initiating an authorization request, specify the space-separated list of scopes needed by your application:

ScopeCategoryDescription
profileBasic IdentityUser's full name, profile picture, bio, and handle.
emailCommunicationVerified primary email address.
work_experienceVerified CredentialsEmployment history, roles, companies, and verification badges.
educationVerified CredentialsAcademic institutions, degrees, fields of study, and verified proofs.
skillsVerified CredentialsProfessional skills, issuer certificates, and honors.
profile:readExtended ReadRead access to public and verified profile data.
profile:verifyExtended VerifyDetailed verification audit trails and cryptographic proof references.

4. REST API Specification

Step 1: Authorization Request

Redirect the user's browser to initiate the consent flow:

GET /oauth/authorize/consent?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code&scope=profile%20email%20work_experience&state=RANDOM_CSRF_STRING

Query Parameters

  • client_id (required): Your registered application Client ID.
  • redirect_uri (required): Must match one of your whitelisted redirect URIs.
  • response_type (required): Must be code.
  • scope (required): Space-separated permissions (e.g. profile email).
  • state (required): Cryptographically random string for anti-CSRF validation.

Step 2: Token Exchange

Exchange the short-lived authorization code for access and refresh tokens:

POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded

Form Payload

grant_type=authorization_code
&code=AUTH_CODE_FROM_CALLBACK
&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Response (200 OK)

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 86400,
  "refresh_token": "f8a92b3c4d5e6f7a8b9c0d1e2f3a4b5c",
  "scope": "profile email work_experience"
}

Step 3: Fetch Protected User Profile

Query the authenticated user's verified claims:

GET /api/oauth/userinfo
Authorization: Bearer YOUR_ACCESS_TOKEN
Accept: application/json

Response (200 OK)

{
  "sub": "user_64f1a2b3c4d5e6f7a8b9c0d1",
  "email": "alex@authenticprofile.com",
  "email_verified": true,
  "name": "Alex Mercer",
  "picture": "https://kfysrcdqszgkmusaiigh.supabase.co/storage/v1/object/public/files/avatars/alex.webp",
  "updated_at": 1726700000,
  "workExperience": [
    {
      "id": "exp_01",
      "company": "Enterprise Tech Corp",
      "position": "Principal Architect",
      "verified": true,
      "verificationStatus": "verified"
    }
  ]
}

5. Frontend & React SDK Integration

This section provides complete, production-tested React and Next.js App Router code for drop-in OAuth 2.0 integration.

5.1. Environment Variables (.env.local)

NEXT_PUBLIC_CLIENT_ID=your_client_id
CLIENT_ID=your_client_id
NEXT_PUBLIC_CLIENT_SECRET=your_client_secret
NEXT_PUBLIC_REDIRECT_URL=https://yourdomain.com/auth/callback
NEXT_PUBLIC_AUTHENTIC_PROFILE_OAUTH_BASE_URL=https://authenticprofile.com

5.2. Custom React Hook (hooks/useAuthenticProfileAuth.ts)

Encapsulates state generation, redirect, anti-CSRF verification, code exchange, and token persistence:

import { useState, useCallback, useRef } from "react";

interface AuthConfig {
  clientId: string;
  redirectUri: string;
  scopes?: string[];
}

interface UserInfo {
  sub: string;
  email: string;
  email_verified: boolean;
  name?: string;
  picture?: string;
  updated_at: number;
  workExperience?: any[];
  education?: any[];
}

export const useAuthenticProfileAuth = (config: AuthConfig) => {
  const [isLoading, setIsLoading] = useState(false);
  const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
  const [error, setError] = useState<string | null>(null);
  const activeRequest = useRef<Promise<any> | null>(null);

  const generateState = useCallback(() => {
    return (
      Math.random().toString(36).substring(2, 15) +
      Math.random().toString(36).substring(2, 15)
    );
  }, []);

  const initiateAuth = useCallback(() => {
    const state = generateState();
    const scope = config.scopes?.join(" ") || "profile email";

    const authBase =
      process.env.NEXT_PUBLIC_AUTHENTIC_PROFILE_OAUTH_BASE_URL ||
      "https://authenticprofile.com";

    const authUrl = new URL(`${authBase}/oauth/authorize/consent`);
    authUrl.searchParams.set("client_id", config.clientId);
    authUrl.searchParams.set("redirect_uri", config.redirectUri);
    authUrl.searchParams.set("response_type", "code");
    authUrl.searchParams.set("scope", scope);
    authUrl.searchParams.set("state", state);

    // Persist state in localStorage for CSRF verification
    localStorage.setItem("oauth_state", state);

    window.location.href = authUrl.toString();
  }, [config, generateState]);

  const handleCallback = useCallback(
    async (code: string, state: string) => {
      if (activeRequest.current) {
        return activeRequest.current;
      }

      setIsLoading(true);
      setError(null);

      const requestPromise = (async () => {
        try {
          const storedState = localStorage.getItem("oauth_state");
          if (state !== storedState) {
            throw new Error("Invalid state parameter: Potential CSRF attempt.");
          }
          localStorage.removeItem("oauth_state");

          const authBase =
            process.env.NEXT_PUBLIC_AUTHENTIC_PROFILE_OAUTH_BASE_URL ||
            "https://authenticprofile.com";

          // Exchange authorization code for token
          const tokenResponse = await fetch(`${authBase}/api/oauth/token`, {
            method: "POST",
            headers: {
              "Content-Type": "application/x-www-form-urlencoded",
            },
            body: new URLSearchParams({
              grant_type: "authorization_code",
              code,
              redirect_uri: config.redirectUri,
              client_id: config.clientId,
              client_secret: `${process.env.NEXT_PUBLIC_CLIENT_SECRET}`,
            }),
          });

          if (!tokenResponse.ok) {
            const errorText = await tokenResponse.text();
            throw new Error(`Token exchange failed: ${tokenResponse.status} ${errorText}`);
          }

          const tokenData = await tokenResponse.json();

          // Retrieve verified user profile
          const userResponse = await fetch(`${authBase}/api/oauth/userinfo`, {
            headers: {
              Authorization: `Bearer ${tokenData.access_token}`,
            },
          });

          if (!userResponse.ok) {
            throw new Error(`Failed to fetch user info: ${userResponse.status}`);
          }

          const userData = await userResponse.json();
          setUserInfo(userData);
          localStorage.setItem("access_token", tokenData.access_token);
          return userData;
        } catch (err) {
          const errorMessage = err instanceof Error ? err.message : "Authentication failed";
          setError(errorMessage);
          throw err;
        } finally {
          setIsLoading(false);
          activeRequest.current = null;
        }
      })();

      activeRequest.current = requestPromise;
      return await requestPromise;
    },
    [config]
  );

  const logout = useCallback(() => {
    setUserInfo(null);
    localStorage.removeItem("access_token");
  }, []);

  return {
    isLoading,
    userInfo,
    error,
    initiateAuth,
    handleCallback,
    logout,
  };
};

5.3. Prebuilt Login UI Component (components/AuthenticProfileLogin.tsx)

A branded, accessible login button and permission summary card:

"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Shield, User, Mail, Briefcase } from "lucide-react";
import { useAuthenticProfileAuth } from "@/hooks/useAuthenticProfileAuth";

interface AuthenticProfileLoginProps {
  clientId: string;
  redirectUri: string;
  scopes?: string[];
  onSuccess?: (userInfo: any) => void;
  onError?: (error: string) => void;
}

export const AuthenticProfileLogin: React.FC<AuthenticProfileLoginProps> = ({
  clientId,
  redirectUri,
  scopes = ["profile", "email"],
  onSuccess,
  onError,
}) => {
  const { initiateAuth, isLoading } = useAuthenticProfileAuth({
    clientId,
    redirectUri,
    scopes,
  });

  const handleLogin = async () => {
    try {
      initiateAuth();
    } catch (error) {
      onError?.(error instanceof Error ? error.message : "Login failed");
    }
  };

  const getScopeDescription = (scope: string) => {
    const scopeDescriptions: Record<string, string> = {
      profile: "Your basic profile information (name, photo)",
      email: "Your email address",
      work_experience: "Your work history and employment details",
      education: "Your educational background",
      skills: "Your professional skills and certifications",
    };
    return scopeDescriptions[scope] || scope;
  };

  const getScopeIcon = (scope: string) => {
    const scopeIcons: Record<string, React.ReactNode> = {
      profile: <User className="w-4 h-4" />,
      email: <Mail className="w-4 h-4" />,
      work_experience: <Briefcase className="w-4 h-4" />,
      education: <User className="w-4 h-4" />,
      skills: <Shield className="w-4 h-4" />,
    };
    return scopeIcons[scope] || <Shield className="w-4 h-4" />;
  };

  return (
    <Card className="w-full max-w-md mx-auto shadow-md">
      <CardHeader className="text-center">
        <div className="flex justify-center mb-3">
          <div className="w-12 h-12 bg-blue-50 rounded-full flex items-center justify-center">
            <Shield className="w-6 h-6 text-blue-600" />
          </div>
        </div>
        <CardTitle className="text-xl">Continue with Authentic Profile</CardTitle>
        <CardDescription>
          Securely authenticate with your verified professional credentials
        </CardDescription>
      </CardHeader>

      <CardContent className="space-y-4">
        <div className="bg-slate-50 rounded-lg p-3 border border-slate-100">
          <h4 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">
            This app will receive:
          </h4>
          <div className="space-y-1.5">
            {scopes.map((scope) => (
              <div key={scope} className="flex items-center gap-2.5 text-sm text-slate-700">
                <div className="text-blue-600">{getScopeIcon(scope)}</div>
                <span>{getScopeDescription(scope)}</span>
              </div>
            ))}
          </div>
        </div>

        <Button
          onClick={handleLogin}
          disabled={isLoading}
          className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium"
          size="lg"
        >
          {isLoading ? "Connecting..." : "Sign in with Authentic Profile"}
        </Button>

        <p className="text-xs text-slate-400 text-center">
          Verified under RFC 6749 OAuth 2.0 Identity Protocol
        </p>
      </CardContent>
    </Card>
  );
};

5.4. Next.js Auth Callback Route (app/auth/callback/page.tsx)

"use client";
import { AuthCallbackHandler } from "@/components/AuthCallbackHandler";
import { useRouter } from "next/navigation";

export default function CallbackPage() {
  const router = useRouter();

  const handleSuccess = (userInfo: any) => {
    localStorage.setItem("user", JSON.stringify(userInfo));
    router.push("/dashboard");
  };

  const handleError = (error: string) => {
    console.error("Authentication callback error:", error);
    router.push(`/login?error=${encodeURIComponent(error)}`);
  };

  return (
    <AuthCallbackHandler
      clientId={`${process.env.NEXT_PUBLIC_CLIENT_ID}`}
      redirectUri={`${process.env.NEXT_PUBLIC_REDIRECT_URL}`}
      onSuccess={handleSuccess}
      onError={handleError}
    />
  );
}

5.5. Callback Processing Handler (components/AuthCallbackHandler.tsx)

Renders dynamic states (loading, success, error) and handles retry actions:

"use client";
import { useEffect, useState, useRef } from "react";
import { useAuthenticProfileAuth } from "@/hooks/useAuthenticProfileAuth";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { CheckCircle, XCircle, Loader2 } from "lucide-react";

interface AuthCallbackHandlerProps {
  clientId: string;
  redirectUri: string;
  onSuccess: (userInfo: any) => void;
  onError: (error: string) => void;
}

export const AuthCallbackHandler: React.FC<AuthCallbackHandlerProps> = ({
  clientId,
  redirectUri,
  onSuccess,
  onError,
}) => {
  const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
  const [message, setMessage] = useState<string>("");
  const hasProcessed = useRef(false);
  const { handleCallback } = useAuthenticProfileAuth({ clientId, redirectUri });

  useEffect(() => {
    if (hasProcessed.current) return;
    hasProcessed.current = true;

    const processCallback = async () => {
      try {
        const urlParams = new URLSearchParams(window.location.search);
        const code = urlParams.get("code");
        const state = urlParams.get("state");
        const error = urlParams.get("error");

        if (error) throw new Error(error);
        if (!code || !state) throw new Error("Missing authorization code or state");

        const userInfo = await handleCallback(code, state);
        setStatus("success");
        setMessage("Authentication verified!");

        setTimeout(() => {
          onSuccess(userInfo);
        }, 1000);
      } catch (err) {
        const errorMessage = err instanceof Error ? err.message : "Authentication failed";
        setStatus("error");
        setMessage(errorMessage);
        onError(errorMessage);
      }
    };

    processCallback();
  }, [handleCallback, onSuccess, onError]);

  const handleRetry = () => {
    hasProcessed.current = false;
    window.location.reload();
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-slate-50 p-4">
      <Card className="w-full max-w-md shadow-lg">
        <CardContent className="pt-8 pb-8 text-center space-y-4">
          {status === "loading" && (
            <>
              <Loader2 className="w-12 h-12 animate-spin text-blue-600 mx-auto" />
              <h3 className="text-lg font-semibold">Securing Authentication</h3>
              <p className="text-sm text-slate-500">Validating authorization code and exchanging tokens...</p>
            </>
          )}

          {status === "success" && (
            <>
              <CheckCircle className="w-12 h-12 text-green-600 mx-auto" />
              <h3 className="text-lg font-semibold text-green-700">Authentication Successful</h3>
              <p className="text-sm text-slate-500">Redirecting to your dashboard...</p>
            </>
          )}

          {status === "error" && (
            <>
              <XCircle className="w-12 h-12 text-red-600 mx-auto" />
              <h3 className="text-lg font-semibold text-red-700">Authentication Failed</h3>
              <p className="text-sm text-red-500 bg-red-50 p-3 rounded-md">{message}</p>
              <Button onClick={handleRetry} className="mt-2">Try Again</Button>
            </>
          )}
        </CardContent>
      </Card>
    </div>
  );
};

On this page