/** * OAuth Token Storage Example * * This example demonstrates: * - Storing OAuth access tokens for future API access * - Using stored tokens to call provider APIs (GitHub, Google) * - Difference between OAuth tokens and JWT tokens * - Token encryption and decryption * - Accessing user's OAuth connections */ import { FlinkApp, Handler, GetHandler, RouteProps } from "@flink-app/flink"; import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin"; import { oauthPlugin } from "@flink-app/oauth-plugin"; import { FlinkContext, FlinkRepo } from "@flink-app/flink"; import https from "https"; interface User { _id?: string; email: string; name?: string; avatarUrl?: string; oauthProviders: Array<{ provider: "github" | "google"; providerId: string; }>; roles: string[]; createdAt: Date; updatedAt: Date; } class UserRepo extends FlinkRepo { async findByEmail(email: string) { return this.getOne({ email }); } } interface AppContext extends FlinkContext { repos: { userRepo: UserRepo; }; plugins: { jwtAuth: any; oauth: any; }; } async function start() { const app = new FlinkApp({ name: "OAuth Token Storage Example", port: 3335, db: { uri: process.env.MONGODB_URI || "mongodb://localhost:27017/oauth-storage-example", }, auth: jwtAuthPlugin({ secret: process.env.JWT_SECRET || "your-super-secret-jwt-key", getUser: async (tokenData) => { const user = await app.ctx.repos.userRepo.getById(tokenData.userId); if (!user) throw new Error("User not found"); return { id: user._id, email: user.email, roles: user.roles, }; }, rolePermissions: { user: ["read:own", "write:own"], }, expiresIn: "7d", }), plugins: [ oauthPlugin({ providers: { github: { clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, callbackUrl: process.env.GITHUB_CALLBACK_URL || "http://localhost:3335/oauth/github/callback", // Request additional scopes for API access scope: ["user:email", "read:user", "repo"], }, google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, callbackUrl: process.env.GOOGLE_CALLBACK_URL || "http://localhost:3335/oauth/google/callback", // Request additional scopes for API access scope: [ "openid", "email", "profile", "https://www.googleapis.com/auth/drive.readonly", ], }, }, // IMPORTANT: Enable token storage storeTokens: true, onAuthSuccess: async ({ profile, provider, tokens }, ctx) => { console.log(`\n=== OAuth Success: ${provider} ===`); console.log(`User: ${profile.email}`); console.log(`OAuth Access Token: ${tokens?.accessToken.substring(0, 20)}...`); console.log( `Token will be encrypted and stored in MongoDB for future API calls` ); // Find or create user let user = await ctx.repos.userRepo.findByEmail(profile.email); if (!user) { user = await ctx.repos.userRepo.create({ email: profile.email, name: profile.name, avatarUrl: profile.avatarUrl, oauthProviders: [{ provider, providerId: profile.id }], roles: ["user"], createdAt: new Date(), updatedAt: new Date(), }); } // Generate JWT token (for app authentication) const jwtToken = await ctx.plugins.jwtAuth.createToken( { userId: user._id, email: user.email }, user.roles ); console.log(`JWT Token: ${jwtToken.substring(0, 20)}...`); console.log(`\nToken Clarification:`); console.log(` - OAuth Access Token: Stored in DB, used to call ${provider} API`); console.log(` - JWT Token: Returned to client, used for app authentication`); return { user: { _id: user._id, email: user.email, name: user.name, roles: user.roles, }, token: jwtToken, redirectUrl: "/dashboard", }; }, onAuthError: async ({ error, provider }) => { console.error(`OAuth error for ${provider}:`, error); return { redirectUrl: `/login?error=${error.code}`, }; }, }), ], }); // Example handler: Get user's GitHub repositories using stored OAuth token app.addHandler({ method: "GET", path: "/github/repos", permissions: ["user"], handler: GetGitHubReposHandler, }); // Example handler: Get user's Google Drive files using stored OAuth token app.addHandler({ method: "GET", path: "/google/drive", permissions: ["user"], handler: GetGoogleDriveHandler, }); // Example handler: Get user's OAuth connections app.addHandler({ method: "GET", path: "/oauth/connections", permissions: ["user"], handler: GetOAuthConnectionsHandler, }); await app.start(); console.log("\n==========================================="); console.log("OAuth Token Storage Example Server"); console.log("==========================================="); console.log(`Server: http://localhost:3335`); console.log(`\nFeatures:`); console.log(` - OAuth tokens are encrypted and stored in MongoDB`); console.log(` - Stored tokens used to call GitHub/Google APIs`); console.log(` - JWT tokens used for app authentication`); console.log(`\nEndpoints:`); console.log(` POST /github/repos - Get user's GitHub repos (requires auth)`); console.log(` POST /google/drive - Get user's Google Drive files (requires auth)`); console.log(` POST /oauth/connections - Get user's OAuth connections`); console.log("===========================================\n"); } start().catch((error) => { console.error("Failed to start application:", error); process.exit(1); }); /** * Handler: Get User's GitHub Repositories * * Uses stored OAuth access token to call GitHub API */ interface GitHubRepo { name: string; full_name: string; description?: string; html_url: string; stargazers_count: number; } interface GetGitHubReposResponse { repos: GitHubRepo[]; source: "github_api"; } const GetGitHubReposHandler: GetHandler< AppContext, GetGitHubReposResponse > = async ({ ctx, req }) => { const userId = req.user._id; // Get stored OAuth connection for GitHub const connection = await ctx.plugins.oauth.getConnection(userId, "github"); if (!connection) { throw new Error( "GitHub not connected. Please login with GitHub to access repositories." ); } console.log( `\nCalling GitHub API with stored OAuth token for user ${userId}` ); console.log(`Token scope: ${connection.scope}`); // Call GitHub API with stored access token const repos = await callGitHubAPI( "/user/repos?sort=updated&per_page=10", connection.accessToken ); return { data: { repos: repos.map((repo: any) => ({ name: repo.name, full_name: repo.full_name, description: repo.description, html_url: repo.html_url, stargazers_count: repo.stargazers_count, })), source: "github_api", }, }; }; /** * Handler: Get User's Google Drive Files * * Uses stored OAuth access token to call Google Drive API */ interface GoogleDriveFile { id: string; name: string; mimeType: string; webViewLink?: string; } interface GetGoogleDriveResponse { files: GoogleDriveFile[]; source: "google_drive_api"; } const GetGoogleDriveHandler: GetHandler< AppContext, GetGoogleDriveResponse > = async ({ ctx, req }) => { const userId = req.user._id; // Get stored OAuth connection for Google const connection = await ctx.plugins.oauth.getConnection(userId, "google"); if (!connection) { throw new Error( "Google not connected. Please login with Google to access Drive files." ); } console.log( `\nCalling Google Drive API with stored OAuth token for user ${userId}` ); console.log(`Token scope: ${connection.scope}`); // Call Google Drive API with stored access token const response = await callGoogleAPI( "/drive/v3/files?pageSize=10&fields=files(id,name,mimeType,webViewLink)", connection.accessToken ); return { data: { files: response.files || [], source: "google_drive_api", }, }; }; /** * Handler: Get User's OAuth Connections * * Shows which providers are connected and their token details */ interface OAuthConnectionInfo { provider: string; providerId: string; scope: string; expiresAt?: string; createdAt: string; } interface GetOAuthConnectionsResponse { connections: OAuthConnectionInfo[]; } const GetOAuthConnectionsHandler: GetHandler< AppContext, GetOAuthConnectionsResponse > = async ({ ctx, req }) => { const userId = req.user._id; // Get all OAuth connections for user const connections = await ctx.plugins.oauth.getConnections(userId); return { data: { connections: connections.map((conn) => ({ provider: conn.provider, providerId: conn.providerId, scope: conn.scope, expiresAt: conn.expiresAt?.toISOString(), createdAt: conn.createdAt.toISOString(), })), }, }; }; /** * Helper Functions for API Calls */ function callGitHubAPI(path: string, accessToken: string): Promise { return new Promise((resolve, reject) => { const options = { hostname: "api.github.com", path: path, method: "GET", headers: { Authorization: `Bearer ${accessToken}`, "User-Agent": "Flink-OAuth-Example", Accept: "application/vnd.github.v3+json", }, }; const req = https.request(options, (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { if (res.statusCode === 200) { resolve(JSON.parse(data)); } else { reject( new Error(`GitHub API error: ${res.statusCode} ${res.statusMessage}`) ); } }); }); req.on("error", reject); req.end(); }); } function callGoogleAPI(path: string, accessToken: string): Promise { return new Promise((resolve, reject) => { const options = { hostname: "www.googleapis.com", path: path, method: "GET", headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", }, }; const req = https.request(options, (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { if (res.statusCode === 200) { resolve(JSON.parse(data)); } else { reject( new Error(`Google API error: ${res.statusCode} ${res.statusMessage}`) ); } }); }); req.on("error", reject); req.end(); }); } /** * OAuth Tokens vs JWT Tokens - Clarification * * OAuth Tokens (Access Tokens): * - Purpose: Call provider APIs (GitHub, Google) on behalf of user * - Issued by: OAuth provider (GitHub, Google) * - Stored: Encrypted in MongoDB (if storeTokens: true) * - Used for: Accessing user's GitHub repos, Google Drive, etc. * - Lifetime: Varies by provider (hours to months) * - Example: "gho_16C7e42F292c6912E7710c838347Ae178B4a" * * JWT Tokens: * - Purpose: Authenticate requests to YOUR app * - Issued by: Your app (via JWT Auth Plugin) * - Stored: Client-side (localStorage, sessionStorage) * - Used for: Accessing protected endpoints in your app * - Lifetime: Configured in JWT Auth Plugin (e.g., 7 days) * - Example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." * * Flow Example: * * 1. User logs in via GitHub OAuth * -> OAuth flow completes * -> OAuth access token: "gho_abc123" (stored in DB if storeTokens: true) * -> JWT token: "eyJ..." (returned to client) * * 2. Client makes request to YOUR app: * GET /api/profile * Authorization: Bearer eyJ... * -> Uses JWT token for authentication * * 3. YOUR app makes request to GitHub API: * GET https://api.github.com/user/repos * Authorization: Bearer gho_abc123 * -> Uses OAuth access token from DB * * When to Store OAuth Tokens: * * YES (storeTokens: true): * - You need to call GitHub/Google APIs on user's behalf * - You want to sync user's data from provider * - You need access to user's repos, drive files, etc. * * NO (storeTokens: false): * - You only use OAuth for authentication * - You don't need provider API access * - You want to minimize stored credentials * * Security Notes: * * - OAuth tokens are encrypted with AES-256-GCM before storage * - Only use HTTPS in production * - Implement token refresh for long-term access * - Monitor token usage and expiration * - Log all API calls for audit trail */