Skip to content

Commit

Permalink
Merge pull request #77 from diggerhq/feat/m2m-applications-support
Browse files Browse the repository at this point in the history
add support for m2m applications for users
  • Loading branch information
motatoes authored Nov 13, 2024
2 parents a2180b1 + cf93971 commit 726c7ca
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"hastscript": "^9.0.0",
"html2canvas": "^1.4.1",
"inbucket-js-client": "^1.0.1",
"jose": "^5.9.6",
"js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.2",
"jspdf": "^2.5.1",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,20 @@ model user_profiles {
user_roles user_roles[]
}


model user_m2m_applications {
id Int @id @default(autoincrement())
email String
clientId String @unique
audience String?
issuer String?
name String?
description String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
}


/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
/// This model contains row level security and requires additional setup for migrations. Visit https://pris.ly/d/row-level-security for more info.
model user_roles {
Expand Down
12 changes: 12 additions & 0 deletions src/app/api/v1/projects/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { withApiAuth } from '@/middleware/api';
import { NextResponse } from 'next/server';

export const GET = withApiAuth(async function (
req: Request,
userEmail: string,
) {
// Your API logic here

// user, default org, ...
return NextResponse.json({ data: 'your project' });
});
21 changes: 21 additions & 0 deletions src/data/user/m2m.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use server';
import { PrismaClient } from '@prisma/client';

export async function getM2MApplication(clientId: string) {
const prisma = new PrismaClient();

try {
const m2mApp = await prisma.user_m2m_applications.findFirst({
where: { clientId: clientId || '' },
});

if (!m2mApp) {
console.log('No matching M2M application found for client ID:', clientId);
throw new Error('m2m2 application not found');
}

return m2mApp;
} finally {
await prisma.$disconnect();
}
}
43 changes: 43 additions & 0 deletions src/middleware/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// middleware/withApiAuth.ts

import { auth } from '@/auth';
import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { validateM2MToken } from './m2m';

export function withApiAuth(
handler: (req: NextRequest, userEmail: string) => Promise<NextResponse>,
) {
return async function (req: NextRequest) {
// Check for M2M Bearer token
try {
const headersList = headers();
const authHeader = headersList.get('authorization');

if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.split(' ')[1];
const payload = await validateM2MToken(token);
if (payload) {
// Valid M2M token
return handler(req, payload.email);
}
}

// this part is to check if there is a cookie session available
// example if request is made from browser api
const session = await auth();
if (!session) {
return new NextResponse('Unauthorized', { status: 401 });
}

if (!session?.user?.email) {
throw new Error('could not retrieve email from session');
}

return handler(req, session.user?.email);
} catch (error) {
console.error('Auth error:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
};
}
38 changes: 38 additions & 0 deletions src/middleware/m2m.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getM2MApplication } from '@/data/user/m2m';
import { createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose';
export async function validateM2MToken(token: string) {
try {
// Decode without verification to get clientId
const decoded = decodeJwt(token);
const clientId = String(decoded.azp) || String(decoded.client_id);

// Only verify if this clientId is registered in our system
const m2mApp = await getM2MApplication(clientId);
console.log('verifying app for:', m2mApp.email);

// Verify the token
const auth0Domain = m2mApp.issuer;
const audience = m2mApp.audience;

const jwks = createRemoteJWKSet(
new URL(`${auth0Domain}/.well-known/jwks.json`),
);

// TODO: figure out the correct typing here
// @ts-ignore
const { payload } = await jwtVerify(token, jwks, {
audience: [audience],
issuer: `${auth0Domain}/`,
algorithms: ['RS256'],
});

return {
payload,
email: m2mApp.email,
m2mApp,
};
} catch (error) {
console.error('Token validation error:', error);
return null;
}
}
16 changes: 16 additions & 0 deletions supabase/migrations/20241112163808_create_m2m_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE TABLE "user_m2m_applications" (
"id" SERIAL PRIMARY KEY,
"email" TEXT NOT NULL,
"clientId" TEXT UNIQUE NOT NULL,
"audience" TEXT NOT NULL,
"issuer" TEXT NOT NULL,
"name" TEXT,
"description" TEXT,
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Create an index on userId for better query performance
CREATE INDEX "m2m_application_user_id_idx" ON "user_m2m_applications"("email");


0 comments on commit 726c7ca

Please sign in to comment.