DEV Community

Akshat Batra
Akshat Batra

Posted on

InnoGate: Anti-Piracy Research Discovery Platform with AI-Powered RAG and Auth0 FGA

Auth0 for AI Agents Challenge Submission

This is a submission for the Auth0 for AI Agents Challenge

What We Built

InnoGate is an ethical research paper sharing and discovery platform that combines Auth0's Fine-Grained Authorization (FGA) with AI-powered Retrieval Augmented Generation (RAG) to create a secure, intelligent research assistant. The platform addresses two critical challenges in academic research:

The Problem

  1. Piracy and Ethical Sourcing: Researchers often resort to platforms like Sci-Hub due to paywalls, creating ethical and legal concerns
  2. Information Overload: With millions of research papers published annually, finding and synthesizing relevant information is overwhelming

The Solution

InnoGate provides:

  • 🔬 Ethical Research Access: Integration with OpenAlex API for legitimate, open-access research papers
  • 🤖 AI-Powered Research Assistant: Chat with your research papers using RAG technology
  • 🔐 Fine-Grained Access Control: Share papers securely with colleagues using Auth0 FGA
  • 🎯 Smart Suggestions: AI automatically suggests relevant papers based on your queries
  • 🔍 Researcher Discovery: Find and follow researchers by ORCID ID

Demo Video

Tech Stack

  • Backend: Fastify, PostgreSQL, Drizzle ORM
  • Frontend: React 19, React Router 7, Tailwind CSS
  • AI/ML: LangChain, Azure OpenAI (embeddings), OpenRouter (chat)
  • Auth: Auth0 (JWT), Auth0 FGA (fine-grained authorization)
  • APIs: OpenAlex (research papers)

Repository

🔗 Repository: GitHub - InnoGate

Key Features

1. Researcher Discovery & PDF Management

Users can search researchers by ORCID ID and automatically
link their publications with metadata

  • Search by ORCID: e.g. 0000-0002-1825-0097
  • Auto-fetch papers from OpenAlex API
  • Upload full PDFs with researcher metadata

2. AI-Powered Suggestions

  • Type a research question
  • AI automatically suggests relevant papers (with relevance scores)
  • One-click to load papers into RAG context

3. Secure PDF Sharing

  • Share papers with colleagues via email
  • Recipients automatically linked to researcher
  • Fine-grained permissions via Auth0 FGA

How I Used Auth0 for AI Agents

Auth0 is the backbone of InnoGate's security architecture, implementing both authentication and fine-grained authorization.

1. Authentication with Auth0 Provider

Frontend Setup:

export default function AuthProvider({ children }: Props) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    // Render children directly during SSR (no auth on server render)
    return <>{children}</>;
  }

  const domain = import.meta.env.VITE_AUTH0_DOMAIN as string | undefined;
  const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID as string | undefined;
  const audience = import.meta.env.VITE_AUTH0_AUDIENCE as string | undefined;

  if (!domain || !clientId) {
    console.warn("VITE_AUTH0_DOMAIN or VITE_AUTH0_CLIENT_ID is not set.");
    return <>{children}</>;
  }

  return (
    <Auth0Provider
      domain={domain}
      clientId={clientId}
      authorizationParams={{ 
        redirect_uri: window.location.origin,
        audience: audience, // Request access token for the API
      }}
      cacheLocation="localstorage"
      useRefreshTokens={true}
    >
      <UserInitializer>{children}</UserInitializer>
    </Auth0Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Backend JWT Verification:

// server/src/index.ts
import Auth0 from "@auth0/auth0-fastify-api";

await fastify.register(Auth0, {
  domain: process.env.AUTH0_DOMAIN,
  audience: process.env.AUTH0_AUDIENCE,
});

// All routes protected with JWT
fastify.post("/api/pdfs/upload", {
  preHandler: fastify.requireAuth(),
}, async (request, reply) => {
  const userEmail = (request.user as any)?.['https://innogate.app/email'];
  // ... handle upload
});
Enter fullscreen mode Exit fullscreen mode

Custom Claims in Auth0:

exports.onExecutePostLogin = async (event, api) => {
  // Add email as a custom claim to the access token
  if (event.user.email) {
    api.accessToken.setCustomClaim('https://innogate.app/email', event.user.email);
  }
};
Enter fullscreen mode Exit fullscreen mode

2. Fine-Grained Authorization with Auth0 FGA

This is where InnoGate's security shines. Auth0 FGA provides document-level access control that's both powerful and flexible.

The FGA Authorization Model

The heart of FGA is the authorization model. Here's InnoGate's model:

model
  schema 1.1

type user

type doc
  relations
    define owner: [user]
    define viewer: [user]
    define can_view: owner or viewer
Enter fullscreen mode Exit fullscreen mode

What this means:

  • type user: Represents authenticated users
  • type doc: Represents research papers/PDFs
  • owner: Direct relation - users who uploaded the document
  • viewer: Direct relation - users granted read access
  • can_view: Computed relation - authorization check that returns true if user is owner OR viewer

This model elegantly handles the core requirements:

  • Document owners have full control
  • Shared users get viewer access
  • Authorization checks use the can_view computed relation

FGA Client Configuration

// server/src/lib/fga.ts
import { OpenFgaClient, CredentialsMethod } from "@openfga/sdk";

export function getFGAClient() {
  return new OpenFgaClient({
    apiUrl: process.env.FGA_API_URL,           // https://api.us1.fga.dev
    storeId: process.env.FGA_STORE_ID,         // Your FGA store ID
    credentials: {
      method: CredentialsMethod.ClientCredentials,
      config: {
        clientId: process.env.FGA_CLIENT_ID,
        clientSecret: process.env.FGA_CLIENT_SECRET,
        apiTokenIssuer: process.env.FGA_API_URL,
        apiAudience: process.env.FGA_API_AUDIENCE,
      },
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

Core FGA Operations

Granting Access (Creating Tuples):

// server/src/lib/fga.ts
export async function grantDocumentAccess(
  userEmail: string,
  documentId: string,
  relation: "owner" | "viewer"
) {
  const fgaClient = getFGAClient();

  console.log(`[FGA] Granting ${relation} access to doc:${documentId}`);

  const result = await fgaClient.write({
    writes: [{
      user: `user:${userEmail}`,
      relation,
      object: `doc:${documentId}`,
    }],
  });

  console.log(`[FGA] Successfully granted access:`, result);
  return result;
}
Enter fullscreen mode Exit fullscreen mode

When a PDF is uploaded, two tuples are created:

// User is both owner and viewer
await grantDocumentAccess(userEmail, pdfId, "owner");
await grantDocumentAccess(userEmail, pdfId, "viewer");
Enter fullscreen mode Exit fullscreen mode

Checking Access (Authorization):

export async function canUserViewDocument(
  userEmail: string,
  documentId: string
): Promise<boolean> {
  const fgaClient = getFGAClient();

  try {
    const { allowed } = await fgaClient.check({
      user: `user:${userEmail}`,
      relation: "viewer",
      object: `doc:${documentId}`,
    });

    return allowed || false;
  } catch (error) {
    console.error("FGA check error:", error);
    return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

Batch Checking (Performance Optimization):

export async function batchCheckDocumentAccess(
  userEmail: string,
  documentIds: string[]
): Promise<Record<string, boolean>> {
  const fgaClient = getFGAClient();
  const results: Record<string, boolean> = {};

  console.log(`[FGA] Batch checking ${documentIds.length} documents`);

  const checks = documentIds.map(docId => ({
    user: `user:${userEmail}`,
    relation: "viewer",
    object: `doc:${docId}`,
  }));

  // Parallel FGA checks for performance
  const responses = await Promise.all(
    checks.map(check => fgaClient.check(check))
  );

  documentIds.forEach((docId, index) => {
    results[docId] = responses[index].allowed || false;
  });

  return results;
}
Enter fullscreen mode Exit fullscreen mode

Real-World FGA Integration

Use Case 1: PDF Upload

// server/src/routes/pdfs.ts
fastify.post("/api/pdfs/upload", {
  preHandler: fastify.requireAuth(),
}, async (request, reply) => {
  const { workId, workTitle, orcidId, researcherName } = request.query;
  const userEmail = (request.user as any)?.['https://innogate.app/email'];

  // 1. Save PDF to database
  const [uploaded] = await db.insert(uploadedPdfs)
    .values({
      ownerId: user.id,
      workId,
      fileName: uniqueFileName,
      originalName: data.filename,
      workTitle,
      orcidId,
      researcherName,
    })
    .returning();

  // 2. Create FGA tuples (this is the magic!)
  if (process.env.FGA_STORE_ID) {
    try {
      // Grant both owner and viewer relations
      await grantDocumentAccess(userEmail, uploaded.id, "owner");
      await grantDocumentAccess(userEmail, uploaded.id, "viewer");

      fastify.log.info(`✅ FGA access granted for ${uploaded.id}`);
    } catch (fgaError) {
      fastify.log.error(fgaError, "Failed to grant FGA access");
      // Graceful degradation: Upload succeeds even if FGA fails
    }
  }

  return reply.send({ id: uploaded.id, workId: uploaded.workId });
});
Enter fullscreen mode Exit fullscreen mode

Use Case 2: Accepting Share Requests

// server/src/routes/pdfs.ts
fastify.post("/api/pdfs/share-requests/:id/accept", {
  preHandler: fastify.requireAuth(),
}, async (request, reply) => {
  const { id } = request.params;
  const userEmail = (request.user as any)?.['https://innogate.app/email'];

  const shareRequest = await db.query.pdfShareRequests.findFirst({
    where: and(
      eq(pdfShareRequests.id, id),
      eq(pdfShareRequests.toUserId, user.id)
    ),
    with: { pdf: true },
  });

  // 1. Grant database access
  await db.insert(pdfAccess)
    .values({
      pdfId: shareRequest.pdfId,
      userId: user.id,
    });

  // 2. Create FGA tuple for shared access
  if (process.env.FGA_STORE_ID) {
    try {
      await grantDocumentAccess(userEmail, shareRequest.pdfId, "viewer");
      fastify.log.info(`✅ FGA viewer access granted to ${userEmail}`);
    } catch (fgaError) {
      fastify.log.error(fgaError, "Failed to grant FGA access");
    }
  }

  // 3. Delete the share request (no longer needed)
  await db.delete(pdfShareRequests).where(eq(pdfShareRequests.id, id));

  return reply.send({ message: "Share request accepted" });
});
Enter fullscreen mode Exit fullscreen mode

Use Case 3: AI PDF Suggestions with FGA

This is where FGA truly shines - protecting AI-powered features:

// server/src/routes/pdf-suggestions.ts
fastify.post("/api/pdf-suggestions/suggest", {
  preHandler: fastify.requireAuth(),
}, async (request, reply) => {
  const { query } = request.body;
  const userEmail = (request.user as any)?.['https://innogate.app/email'];

  // Step 1: Get PDFs from database (first layer of security)
  const accessiblePdfs = await db
    .select({
      id: uploadedPdfs.id,
      workTitle: uploadedPdfs.workTitle,
      originalName: uploadedPdfs.originalName,
      researcherName: uploadedPdfs.researcherName,
      ownerId: uploadedPdfs.ownerId,
    })
    .from(uploadedPdfs)
    .leftJoin(pdfAccess, eq(pdfAccess.pdfId, uploadedPdfs.id))
    .where(
      or(
        eq(uploadedPdfs.ownerId, user.id),      // User owns it
        eq(pdfAccess.userId, user.id)           // User has access
      )
    );

  // Step 2: Verify with FGA (second layer of security - defense in depth!)
  let authorizedPdfIds = accessiblePdfs.map(pdf => pdf.id);

  if (process.env.FGA_STORE_ID) {
    const pdfIds = accessiblePdfs.map(pdf => pdf.id);

    // Batch check all PDFs in parallel
    const accessResults = await batchCheckDocumentAccess(userEmail, pdfIds);

    // Filter to only FGA-authorized PDFs
    authorizedPdfIds = pdfIds.filter(id => accessResults[id]);

    console.log(`[FGA] User authorized for ${authorizedPdfIds.length}/${pdfIds.length} PDFs`);
  }

  // Filter PDFs to only authorized ones
  const authorizedPdfs = accessiblePdfs.filter(pdf => 
    authorizedPdfIds.includes(pdf.id)
  );

  if (authorizedPdfs.length === 0) {
    return reply.send({
      suggestions: [],
      message: "No authorized PDFs available",
    });
  }

  // Step 3: Create embeddings for semantic search
  const embeddings = new AzureOpenAIEmbeddings({
    azureOpenAIApiKey: process.env.AZURE_OPENAI_API_KEY,
    azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME,
    azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME,
    azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION,
  });

  const queryEmbedding = await embeddings.embedQuery(query);

  // Step 4: Create embeddings for PDF titles and researchers
  const pdfTexts = authorizedPdfs.map(pdf => {
    const title = pdf.workTitle || pdf.originalName;
    const researcher = pdf.researcherName || "";
    return `${title} ${researcher}`.trim();
  });

  const pdfEmbeddings = await embeddings.embedDocuments(pdfTexts);

  // Step 5: Calculate cosine similarity
  const similarities = pdfEmbeddings.map((pdfEmbed, index) => ({
    pdf: authorizedPdfs[index],
    similarity: cosineSimilarity(queryEmbedding, pdfEmbed),
  }));

  // Step 6: Return top 5 suggestions
  const topSuggestions = similarities
    .sort((a, b) => b.similarity - a.similarity)
    .slice(0, 5)
    .filter(s => s.similarity > 0.3)
    .map(s => ({
      id: s.pdf.id,
      workTitle: s.pdf.workTitle,
      originalName: s.pdf.originalName,
      researcherName: s.pdf.researcherName,
      isOwner: s.pdf.ownerId === user.id,
      relevanceScore: Math.round(s.similarity * 100),
    }));

  return reply.send({
    suggestions: topSuggestions,
    totalAccessiblePdfs: authorizedPdfs.length,
  });
});

// Cosine similarity helper
function cosineSimilarity(vecA: number[], vecB: number[]): number {
  let dotProduct = 0;
  let normA = 0;
  let normB = 0;

  for (let i = 0; i < vecA.length; i++) {
    dotProduct += vecA[i] * vecB[i];
    normA += vecA[i] * vecA[i];
    normB += vecB[i] * vecB[i];
  }

  return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
Enter fullscreen mode Exit fullscreen mode

Key Insight: Notice the defense-in-depth approach:

  1. Database query filters by ownership/access
  2. FGA batch check validates each document
  3. Only authorized PDFs get embedded and suggested
  4. AI never sees documents the user shouldn't access

FGA Best Practices from InnoGate

1. Always Log FGA Operations

console.log(`[FGA] Granting ${relation} access to doc:${documentId}`);
console.log(`[FGA] Batch checking ${documentIds.length} documents`);
Enter fullscreen mode Exit fullscreen mode

This is essential for debugging authorization issues.

2. Implement Graceful Degradation

if (process.env.FGA_STORE_ID) {
  try {
    await grantDocumentAccess(...);
  } catch (fgaError) {
    fastify.log.error(fgaError, "FGA failed");
    // Don't fail the entire operation
  }
}
Enter fullscreen mode Exit fullscreen mode

If FGA is unavailable, fall back to database-only authorization.

3. Use Batch Operations for Performance

// ❌ Bad: Sequential checks (slow)
for (const docId of documentIds) {
  await canUserViewDocument(email, docId);
}

// ✅ Good: Parallel batch check (fast)
const results = await batchCheckDocumentAccess(email, documentIds);
Enter fullscreen mode Exit fullscreen mode

4. Create Both Owner and Viewer Tuples

// When uploading, grant both relations
await grantDocumentAccess(email, docId, "owner");
await grantDocumentAccess(email, docId, "viewer");
Enter fullscreen mode Exit fullscreen mode

This allows the can_view computed relation to work correctly.

5. Delete Tuples When Revoking Access

export async function revokeDocumentAccess(
  userEmail: string,
  documentId: string,
  relation: "owner" | "viewer"
) {
  const fgaClient = getFGAClient();

  await fgaClient.write({
    deletes: [{
      user: `user:${userEmail}`,
      relation,
      object: `doc:${documentId}`,
    }],
  });
}
Enter fullscreen mode Exit fullscreen mode

3. RAG Implementation with Vector Stores

// server/src/routes/pdf-rag.ts
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { AzureOpenAIEmbeddings } from "@langchain/openai";

const vectorStores = new Map<string, MemoryVectorStore>();

fastify.post("/api/pdf-rag/load", {
  preHandler: fastify.requireAuth(),
}, async (request, reply) => {
  const { pdfId } = request.body;
  const userEmail = (request.user as any)?.['https://innogate.app/email'];

  // FGA authorization check before loading
  const hasAccess = await canUserViewDocument(userEmail, pdfId);
  if (!hasAccess) {
    return reply.code(403).send({ error: "Access denied" });
  }

  // Load and embed PDF
  const documents = await loadPDF(pdfPath);
  const textSplitter = new RecursiveCharacterTextSplitter({
    chunkSize: 1000,
    chunkOverlap: 200,
  });
  const splits = await textSplitter.splitDocuments(documents);

  const embeddings = new AzureOpenAIEmbeddings({...config});
  const vectorStore = await MemoryVectorStore.fromDocuments(splits, embeddings);

  vectorStores.set(pdfId, vectorStore);
  return reply.send({ success: true });
});
Enter fullscreen mode Exit fullscreen mode

Lessons Learned and Takeaways

1. Fine-Grained Authorization is Complex but Essential

Auth0 FGA proved invaluable because:

  • Separation of Concerns: Authorization logic lives in FGA, not scattered across code
  • Relationship-Based Access: The model naturally expresses owner/viewer relationships
  • Scalability: Adding new relations (e.g., editor, commenter) is trivial
  • Audit Trail: Every access decision is logged

Challenge: Understanding FGA's tuple-based model took time.

Lesson: Start with the simplest model and iterate. The define can_view: owner or viewer syntax is powerful.

2. Double Defense Wins

InnoGate implements multiple security layers:

  1. JWT Authentication (Auth0)
  2. Database constraints
  3. FGA authorization checks
  4. Application validation

Lesson: FGA can be used to either replace database based authorization or bolster it by adding another layer of checks. In case database is somehow compromised Auth0 FGA will ensure that no data leaks.

3. Batch Operations are Critical

Checking 50 PDFs sequentially would take seconds. Batch checking takes milliseconds.

// Parallel FGA checks
const responses = await Promise.all(
  checks.map(check => fgaClient.check(check))
);
Enter fullscreen mode Exit fullscreen mode

Lesson: Always use batch operations for performance.

Team Members Handles:

@soniab - Management Lead
@akshatbatra - Developer

Top comments (0)