OAuth Integration Example
This example demonstrates a complete OAuth-based Stamp integration for a fictional platform called "DevCommunity" that verifies developer reputation and activity.
This is a complete, working example you can use as a template. Replace "DevCommunity" with your platform name and customize the validation logic.
Example Scenario
Platform: DevCommunity (fictional developer community)
Verification: User has verified email, 100+ reputation points, and account >30 days old
OAuth Flow: Standard OAuth 2.0 with access token exchange
Complete File Structure
- index.ts
- App-Bindings.tsx
- Providers-config.ts
- index.ts
- devCommunity.ts
- devCommunity.test.ts
Implementation Files
1. Main Exports (index.ts
)
export { DevCommunityPlatform } from "./App-Bindings.js";
export { PlatformDetails, ProviderConfig, providers } from "./Providers-config.js";
export { DevCommunityProvider } from "./Providers/devCommunity.js";
2. Frontend Platform (App-Bindings.tsx
)
import { AppContext, PlatformOptions, ProviderPayload } from "../types.js";
import { Platform } from "../utils/platform.js";
import React from "react";
export class DevCommunityPlatform extends Platform {
path = "devcommunity";
platformId = "DevCommunity";
clientId: string;
redirectUri: string;
constructor(options: PlatformOptions = {}) {
super();
this.clientId = options.clientId as string;
this.redirectUri = options.redirectUri as string;
this.banner = {
heading: "Connect Your DevCommunity Account",
content: (
<div>
<p>
This Stamp verifies your DevCommunity account and developer activity.
</p>
<br />
<strong>Requirements:</strong>
<ul>
<li>Verified email address</li>
<li>At least 100 reputation points</li>
<li>Account at least 30 days old</li>
<li>Account in good standing</li>
</ul>
<br />
<strong>What we verify:</strong>
<ul>
<li>Account ownership through OAuth</li>
<li>Email verification status</li>
<li>Community reputation score</li>
<li>Account age and activity</li>
</ul>
</div>
),
cta: {
label: "Join DevCommunity",
url: "https://devcommunity.example.com/signup",
},
};
}
async getOAuthUrl(state: string): Promise<string> {
const params = new URLSearchParams({
response_type: "code",
client_id: this.clientId,
state: state,
redirect_uri: this.redirectUri,
scope: "read:user read:profile", // Minimal required scopes
});
return `https://api.devcommunity.example.com/oauth2/authorize?${params.toString()}`;
}
async getProviderPayload(appContext: AppContext): Promise<ProviderPayload> {
return await appContext.waitForRedirect(this);
}
}
3. Configuration (Providers-config.ts
)
import { PlatformSpec, PlatformGroupSpec, Provider } from "../types.js";
import { DevCommunityProvider } from "./Providers/devCommunity.js";
export const PlatformDetails: PlatformSpec = {
icon: "./assets/devCommunityIcon.svg",
platform: "DevCommunity",
name: "DevCommunity",
description: "Verify your developer reputation and community activity",
connectMessage: "Connect Account",
website: "https://devcommunity.example.com",
timeToGet: "3 minutes",
price: "Free",
guide: [
{
type: "steps",
title: "How to Verify Your DevCommunity Account",
items: [
{
title: "Step 1: Create Account",
description: "Sign up at DevCommunity and verify your email address",
actions: [
{
label: "Sign Up Now",
href: "https://devcommunity.example.com/signup",
},
],
},
{
title: "Step 2: Build Your Reputation",
description: "Participate in discussions, answer questions, and help other developers to earn reputation points",
},
{
title: "Step 3: Connect to Human Passport",
description: "Once you have 100+ reputation points, return here and click 'Verify' to add your Stamp",
},
],
},
{
type: "list",
title: "Verification Requirements",
items: [
"Valid email address must be verified",
"Minimum 100 community reputation points",
"Account must be at least 30 days old",
"Account must be active and in good standing",
"No recent violations or suspensions",
],
},
{
type: "list",
title: "Why This Matters",
items: [
"Proves genuine developer community participation",
"Demonstrates consistent engagement over time",
"Validates identity through email verification",
"Shows commitment to helping other developers",
],
},
],
};
export const ProviderConfig: PlatformGroupSpec[] = [
{
platformGroup: "Developer Community",
providers: [
{
title: "Active Community Member",
description: "Demonstrates active participation in the developer community with verified reputation and engagement",
name: "DevCommunityReputation",
},
],
},
];
export const providers: Provider[] = [new DevCommunityProvider()];
4. Provider Exports (Providers/index.ts
)
import { DevCommunityProvider } from "./devCommunity.js";
export const providers = [new DevCommunityProvider()];
5. Verification Logic (Providers/devCommunity.ts
)
import type { RequestPayload, VerifiedPayload } from "@gitcoin/passport-types";
import { ProviderExternalVerificationError, type Provider, type ProviderOptions } from "../../types.js";
import axios from "axios";
import { handleProviderAxiosError } from "../../utils/handleProviderAxiosError.js";
interface DevCommunityTokenResponse {
access_token: string;
token_type: string;
expires_in: number;
scope: string;
}
interface DevCommunityUser {
id: string;
username: string;
email: string;
email_verified: boolean;
reputation: number;
account_created: string;
last_active: string;
status: 'active' | 'suspended' | 'banned';
profile: {
bio?: string;
location?: string;
website?: string;
};
stats: {
posts_count: number;
answers_count: number;
helpful_votes: number;
};
}
export class DevCommunityProvider implements Provider {
type = "DevCommunityReputation";
constructor(options: ProviderOptions = {}) {
// Initialize provider if needed
}
async verify(payload: RequestPayload): Promise<VerifiedPayload> {
const errors: string[] = [];
let valid = false;
let record = undefined;
try {
// 1. Exchange OAuth code for access token
const accessToken = await this.getAccessToken(payload.proofs.code);
// 2. Fetch user data from DevCommunity API
const userData = await this.getUserData(accessToken);
// 3. Validate user meets all requirements
const validation = this.validateUser(userData);
if (!validation.valid) {
return {
valid: false,
errors: validation.errors,
};
}
// 4. Build verification record (no PII)
record = {
id: userData.id,
username: userData.username,
reputation: userData.reputation,
accountAge: this.calculateAccountAge(userData.account_created),
verificationLevel: this.getVerificationLevel(userData.reputation),
verifiedAt: Date.now(),
};
return {
valid: true,
record: record,
errors: undefined,
};
} catch (error) {
throw new ProviderExternalVerificationError(
`DevCommunity verification error: ${error.message}`
);
}
}
private async getAccessToken(code: string): Promise<string> {
const clientId = process.env.DEVCOMMUNITY_CLIENT_ID;
const clientSecret = process.env.DEVCOMMUNITY_CLIENT_SECRET;
const redirectUri = process.env.DEVCOMMUNITY_CALLBACK;
if (!clientId || !clientSecret || !redirectUri) {
throw new Error("Missing DevCommunity OAuth configuration");
}
try {
const response = await axios.post(
"https://api.devcommunity.example.com/oauth2/token",
{
grant_type: "authorization_code",
code: code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
},
{
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'HumanPassport/1.0',
},
timeout: 10000, // 10 second timeout
}
);
const tokenData = response.data as DevCommunityTokenResponse;
if (!tokenData.access_token) {
throw new Error("No access token received");
}
return tokenData.access_token;
} catch (error) {
handleProviderAxiosError(error, "error requesting DevCommunity access token", [code]);
throw error;
}
}
private async getUserData(accessToken: string): Promise<DevCommunityUser> {
try {
const response = await axios.get(
"https://api.devcommunity.example.com/user/profile",
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json',
'User-Agent': 'HumanPassport/1.0',
},
timeout: 10000,
}
);
const userData = response.data as DevCommunityUser;
// Validate response structure
if (!this.isValidUserResponse(userData)) {
throw new Error("Invalid user data structure received");
}
return userData;
} catch (error) {
handleProviderAxiosError(error, "error fetching DevCommunity user data", []);
throw error;
}
}
private validateUser(userData: DevCommunityUser): { valid: boolean; errors?: string[] } {
const errors: string[] = [];
// 1. Email verification requirement
if (!userData.email_verified) {
errors.push("Email address must be verified on DevCommunity");
}
// 2. Reputation requirement
const minReputation = 100;
if (userData.reputation < minReputation) {
errors.push(`Must have at least ${minReputation} reputation points (current: ${userData.reputation})`);
}
// 3. Account age requirement (30 days)
const accountAge = this.calculateAccountAge(userData.account_created);
const minAge = 30; // days
if (accountAge < minAge) {
errors.push(`Account must be at least ${minAge} days old (current: ${accountAge} days)`);
}
// 4. Account status requirement
if (userData.status !== 'active') {
errors.push("Account must be active and in good standing");
}
// 5. Minimum activity requirement
const totalActivity = userData.stats.posts_count + userData.stats.answers_count;
if (totalActivity < 10) {
errors.push("Must have at least 10 posts or answers showing community participation");
}
// 6. Recent activity requirement (must be active within last 90 days)
const lastActiveDate = new Date(userData.last_active);
const daysSinceActive = Math.floor((Date.now() - lastActiveDate.getTime()) / (24 * 60 * 60 * 1000));
if (daysSinceActive > 90) {
errors.push("Account must have been active within the last 90 days");
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
};
}
private calculateAccountAge(accountCreated: string): number {
const createdDate = new Date(accountCreated);
const now = new Date();
const diffTime = now.getTime() - createdDate.getTime();
return Math.floor(diffTime / (24 * 60 * 60 * 1000)); // Convert to days
}
private getVerificationLevel(reputation: number): string {
if (reputation >= 1000) return 'expert';
if (reputation >= 500) return 'advanced';
if (reputation >= 100) return 'intermediate';
return 'beginner';
}
private isValidUserResponse(data: any): data is DevCommunityUser {
return (
data &&
typeof data.id === 'string' &&
typeof data.username === 'string' &&
typeof data.email_verified === 'boolean' &&
typeof data.reputation === 'number' &&
typeof data.account_created === 'string' &&
typeof data.status === 'string' &&
data.stats &&
typeof data.stats.posts_count === 'number'
);
}
}
6. Complete Test Suite (__tests__/devCommunity.test.ts
)
import { DevCommunityProvider } from "../Providers/devCommunity.js";
import { RequestPayload } from "@gitcoin/passport-types";
import axios from "axios";
jest.mock("axios");
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe("DevCommunityProvider", () => {
let provider: DevCommunityProvider;
beforeEach(() => {
provider = new DevCommunityProvider();
jest.clearAllMocks();
// Set environment variables for tests
process.env.DEVCOMMUNITY_CLIENT_ID = "test_client_id";
process.env.DEVCOMMUNITY_CLIENT_SECRET = "test_client_secret";
process.env.DEVCOMMUNITY_CALLBACK = "http://localhost:3000/callback";
});
afterEach(() => {
// Clean up environment variables
delete process.env.DEVCOMMUNITY_CLIENT_ID;
delete process.env.DEVCOMMUNITY_CLIENT_SECRET;
delete process.env.DEVCOMMUNITY_CALLBACK;
});
const validPayload: RequestPayload = {
address: "0x1234567890123456789012345678901234567890",
proofs: { code: "valid_oauth_code" },
type: "DevCommunityReputation",
version: "0.0.0",
};
const mockValidUser = {
id: "user123",
username: "testdeveloper",
email: "test@example.com",
email_verified: true,
reputation: 250,
account_created: "2023-01-01T00:00:00Z",
last_active: new Date().toISOString(),
status: "active" as const,
profile: {
bio: "Full-stack developer",
location: "San Francisco",
},
stats: {
posts_count: 15,
answers_count: 8,
helpful_votes: 42,
},
};
Integration Setup
Environment Variables
Add these to your .env
files:
# Development
NEXT_PUBLIC_DEVCOMMUNITY_CLIENT_ID=dev_client_id_here
DEVCOMMUNITY_CLIENT_SECRET=dev_client_secret_here
NEXT_PUBLIC_DEVCOMMUNITY_CALLBACK=http://localhost:3000/auth/devcommunity/callback
# Production
NEXT_PUBLIC_DEVCOMMUNITY_CLIENT_ID=prod_client_id_here
DEVCOMMUNITY_CLIENT_SECRET=prod_client_secret_here
NEXT_PUBLIC_DEVCOMMUNITY_CALLBACK=https://passport.xyz/auth/devcommunity/callback
System Integration
Add these updates to integrate with the core system:
platforms/src/platforms.ts
import * as DevCommunity from "./DevCommunity/index.js";
const platforms: Record<string, PlatformConfig> = {
// ... existing platforms
DevCommunity,
};
types/src/index.d.ts
export type PROVIDER_ID =
// ... existing providers
| "DevCommunityReputation"
app/config/platformMap.ts
const { DevCommunity } = platforms;
defaultPlatformMap.set("DevCommunity", {
platform: new DevCommunity.DevCommunityPlatform({
clientId: process.env.NEXT_PUBLIC_DEVCOMMUNITY_CLIENT_ID,
redirectUri: process.env.NEXT_PUBLIC_DEVCOMMUNITY_CALLBACK,
}),
platFormGroupSpec: DevCommunity.ProviderConfig,
});
OAuth Application Setup
Configure your OAuth application with:
-
Redirect URIs:
- Development:
http://localhost:3000/auth/devcommunity/callback
- Staging:
https://staging.passport.xyz/auth/devcommunity/callback
- Production:
https://passport.xyz/auth/devcommunity/callback
- Development:
-
Required Scopes:
read:user
- Access to basic user informationread:profile
- Access to profile and reputation data
-
Application Settings:
- Application Type: Web Application
- Grant Types: Authorization Code
- Token Endpoint Auth Method: Client Secret Post
Testing Your Integration
Run the test suite to verify your implementation:
# Run the specific test file
yarn test platforms/src/DevCommunity/__tests__/devCommunity.test.ts
# Run with coverage
yarn test --coverage platforms/src/DevCommunity/
# Run in watch mode for development
yarn test --watch platforms/src/DevCommunity/
Expected test output:
PASS platforms/src/DevCommunity/__tests__/devCommunity.test.ts
DevCommunityProvider
successful verification
✓ should return valid payload for qualified user
✓ should handle expert level user
validation failures
✓ should reject user with unverified email
✓ should reject user with insufficient reputation
✓ should reject user with new account
✓ should reject suspended user
✓ should reject user with insufficient activity
error handling
✓ should throw error for OAuth token failure
✓ should throw error for user data fetch failure
✓ should throw error for missing environment variables
✓ should handle rate limiting
✓ should handle malformed user data
edge cases
✓ should handle user at exact reputation threshold
✓ should handle user at exact account age threshold
✓ should handle user with exactly minimum activity
Test Suites: 1 passed, 1 total
Tests: 14 passed, 14 total
Customization Guide
To adapt this example for your platform:
1. Replace Platform Names
- Change
DevCommunity
to your platform name throughout - Update API endpoints to your actual URLs
- Modify environment variable names
2. Customize Validation Logic
Modify the validateUser()
method to match your requirements:
private validateUser(userData: YourPlatformUser): { valid: boolean; errors?: string[] } {
const errors: string[] = [];
// Add your specific validation requirements
if (userData.yourRequirement < yourThreshold) {
errors.push("Your custom error message");
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
};
}
3. Update OAuth Configuration
- Modify OAuth scopes for your platform's requirements
- Update API endpoints for token exchange and user data
- Adjust request headers as needed for your API
4. Customize UI Content
Update the banner
content and guide
sections in Providers-config.ts
to match your platform's verification process.
This complete example provides a solid foundation for any OAuth-based Stamp integration with Human Passport.