-
-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
333 additions
and
164 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} |
Oops, something went wrong.