Skip to content

Commit

Permalink
Custom Figma OAuth2 implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanjshaw committed Oct 13, 2024
1 parent f61c2f0 commit f183bd9
Show file tree
Hide file tree
Showing 9 changed files with 333 additions and 164 deletions.
79 changes: 79 additions & 0 deletions app/api/auth/figma/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { type NextRequest, NextResponse } from 'next/server';

/**
* Figma OAuth callback
* See: https://www.figma.com/developers/api#oauth2
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const { FIGMA_CLIENT_ID, FIGMA_CLIENT_SECRET, NEXT_PUBLIC_HOST } = process.env;

// Retrieve the stored state from the cookie and verify the CSRF token
const stateParam = searchParams.get('state');
const storedState = request.cookies.get('oauth_state')?.value;
if (storedState !== stateParam) {
return NextResponse.json({ error: 'State mismatch. Possible CSRF attack.' }, { status: 400 });
}

// Get the original page URL from the state
let state;
try {
state = JSON.parse(decodeURIComponent(stateParam!));
} catch {
return NextResponse.json({ error: 'Invalid state parameter' }, { status: 400 });
}

// Exchange the authorization code for an access token
const code = searchParams.get('code');
const tokenUrl = 'https://api.figma.com/v1/oauth/token';
const redirectUri = `${NEXT_PUBLIC_HOST}/api/auth/figma/callback`;

const body = new URLSearchParams({
client_id: FIGMA_CLIENT_ID as string,
client_secret: FIGMA_CLIENT_SECRET as string,
redirect_uri: redirectUri,
code: code as string,
grant_type: 'authorization_code',
});

const tokenResponse = await fetch(tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});

const tokenData = await tokenResponse.json();

if (!tokenResponse.ok) {
return NextResponse.json({ error: 'Failed to exchange authorization code for token' }, { status: 400 });
}

const response = NextResponse.redirect(state.originalUrl);
response.cookies.set('figma_access_token', tokenData.access_token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
});
response.cookies.set('figma_refresh_token', tokenData.refresh_token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
});
response.cookies.set('figma_user_id', tokenData.user_id.toString(), {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
});
const expiresAt = Date.now() + tokenData.expires_in * 1000;
response.cookies.set('figma_token_expires_at', expiresAt.toString(), {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
});

return response;
}
27 changes: 27 additions & 0 deletions app/api/auth/figma/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { type NextRequest, NextResponse } from 'next/server';

/**
* Figma OAuth signin redirect
* See: https://www.figma.com/developers/api#oauth2
*/
export async function GET(request: NextRequest) {
const { FIGMA_CLIENT_ID, NEXT_PUBLIC_HOST } = process.env;

// Get the original page URL from query params or referrer header
const originalUrl = request.nextUrl.searchParams.get('original_url');

const redirectUri = `${NEXT_PUBLIC_HOST}/api/auth/figma/callback`;
const state = JSON.stringify({
csrfToken: Math.random().toString(36).substring(2),
originalUrl: originalUrl,
});

const response = NextResponse.redirect(
`https://www.figma.com/oauth?client_id=${FIGMA_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=files:read&response_type=code&state=${encodeURIComponent(state)}`
);

// Set CSRF token in a cookie (optional for added security)
response.cookies.set('oauth_state', state, { httpOnly: true, secure: true, sameSite: 'none', path: '/' });

return response;
}
16 changes: 16 additions & 0 deletions app/api/auth/figma/status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { type NextRequest, NextResponse } from 'next/server';

/**
* Returns the current Figma access token for the user.
*
* We use httpOnly cookies to prevent XSS, token leaks and request forgery.
*/
export async function GET(request: NextRequest) {
const figmaAccessToken = request.cookies.get('figma_access_token')?.value;

if (figmaAccessToken) {
return NextResponse.json({ accessToken: figmaAccessToken });
}

return NextResponse.json({ accessToken: null });
}
Loading

0 comments on commit f183bd9

Please sign in to comment.