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
| Environment | Authorization Endpoint | Token Exchange Endpoint | UserInfo Endpoint |
|---|---|---|---|
| Production | https://authenticprofile.com/oauth/authorize/consent | https://authenticprofile.com/api/oauth/token | https://authenticprofile.com/api/oauth/userinfo |
| Local Dev | http://localhost:3000/oauth/authorize/consent | http://localhost:3000/api/oauth/token | http://localhost:3000/api/oauth/userinfo |
2. OAuth 2.0 Authorization Flow
3. Scopes & Permissions
When initiating an authorization request, specify the space-separated list of scopes needed by your application:
| Scope | Category | Description |
|---|---|---|
profile | Basic Identity | User's full name, profile picture, bio, and handle. |
email | Communication | Verified primary email address. |
work_experience | Verified Credentials | Employment history, roles, companies, and verification badges. |
education | Verified Credentials | Academic institutions, degrees, fields of study, and verified proofs. |
skills | Verified Credentials | Professional skills, issuer certificates, and honors. |
profile:read | Extended Read | Read access to public and verified profile data. |
profile:verify | Extended Verify | Detailed 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_STRINGQuery Parameters
client_id(required): Your registered application Client ID.redirect_uri(required): Must match one of your whitelisted redirect URIs.response_type(required): Must becode.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-urlencodedForm 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_SECRETResponse (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/jsonResponse (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.com5.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>
);
};