-
Notifications
You must be signed in to change notification settings - Fork 230
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
thorwebdev
committed
Feb 9, 2022
1 parent
850066d
commit 3fb057a
Showing
10 changed files
with
371 additions
and
0 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,3 @@ | ||
import { handleAuth } from '@supabase/supabase-auth-helpers/nextjs'; | ||
|
||
export default handleAuth(); |
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,31 @@ | ||
import { CookieOptions } from '../types'; | ||
import type { NextApiRequest, NextApiResponse } from 'next'; | ||
import handelCallback from './callback'; | ||
import handleUser from './user'; | ||
|
||
export default function handleAuth( | ||
cookieOptions: CookieOptions = { | ||
name: 'sb', | ||
lifetime: 60 * 60 * 8, | ||
domain: '', | ||
path: '/', | ||
sameSite: 'lax' | ||
} | ||
) { | ||
return async (req: NextApiRequest, res: NextApiResponse): Promise<void> => { | ||
let { | ||
query: { supabase: route } | ||
} = req; | ||
|
||
route = Array.isArray(route) ? route[0] : route; | ||
|
||
switch (route) { | ||
case 'callback': | ||
return handelCallback(req, res, cookieOptions); | ||
case 'user': | ||
return handleUser(req, res, cookieOptions); | ||
default: | ||
res.status(404).end(); | ||
} | ||
}; | ||
} |
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,47 @@ | ||
import { CookieOptions } from '../types'; | ||
import type { NextApiRequest, NextApiResponse } from 'next'; | ||
import { setCookies } from '../../shared/utils/cookies'; | ||
|
||
export default function handelCallback( | ||
req: NextApiRequest, | ||
res: NextApiResponse, | ||
cookieOptions: CookieOptions | ||
) { | ||
if (req.method !== 'POST') { | ||
res.setHeader('Allow', 'POST'); | ||
res.status(405).end('Method Not Allowed'); | ||
} | ||
const { event, session } = req.body; | ||
|
||
if (!event) throw new Error('Auth event missing!'); | ||
if (event === 'SIGNED_IN') { | ||
if (!session) throw new Error('Auth session missing!'); | ||
setCookies( | ||
req, | ||
res, | ||
[ | ||
{ key: 'access-token', value: session.access_token }, | ||
{ key: 'refresh-token', value: session.refresh_token } | ||
].map((token) => ({ | ||
name: `${cookieOptions.name}-${token.key}`, | ||
value: token.value, | ||
domain: cookieOptions.domain, | ||
maxAge: cookieOptions.lifetime ?? 0, | ||
path: cookieOptions.path, | ||
sameSite: cookieOptions.sameSite | ||
})) | ||
); | ||
} | ||
if (event === 'SIGNED_OUT') { | ||
setCookies( | ||
req, | ||
res, | ||
['access-token', 'refresh-token'].map((key) => ({ | ||
name: `${cookieOptions.name}-${key}`, | ||
value: '', | ||
maxAge: -1 | ||
})) | ||
); | ||
} | ||
res.status(200).json({}); | ||
} |
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 @@ | ||
export { default as handleAuth } from './auth'; |
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,72 @@ | ||
import { ApiError, CookieOptions } from '../types'; | ||
import type { NextApiRequest, NextApiResponse } from 'next'; | ||
import { setCookies } from '../../shared/utils/cookies'; | ||
import { createClient } from '@supabase/supabase-js'; | ||
|
||
export default async function handleUser( | ||
req: NextApiRequest, | ||
res: NextApiResponse, | ||
cookieOptions: CookieOptions | ||
) { | ||
try { | ||
if ( | ||
!process.env.NEXT_PUBLIC_SUPABASE_URL || | ||
!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY | ||
) { | ||
throw new Error( | ||
'NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY env variables are required!' | ||
); | ||
} | ||
if (!req.cookies) { | ||
throw new Error('Not able to parse cookies!'); | ||
} | ||
const supabase = createClient( | ||
process.env.NEXT_PUBLIC_SUPABASE_URL, | ||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY | ||
); | ||
const access_token = req.cookies[`${cookieOptions.name}-access-token`]; | ||
const refresh_token = req.cookies[`${cookieOptions.name}-refresh-token`]; | ||
|
||
if (!access_token) { | ||
throw new Error('No cookie found!'); | ||
} | ||
|
||
const { user, error: getUserError } = await supabase.auth.api.getUser( | ||
access_token | ||
); | ||
if (getUserError) { | ||
if (!refresh_token) throw new Error('No refresh_token cookie found!'); | ||
if (!res) | ||
throw new Error( | ||
'You need to pass the res object to automatically refresh the session!' | ||
); | ||
const { data, error } = await supabase.auth.api.refreshAccessToken( | ||
refresh_token | ||
); | ||
if (error) { | ||
throw error; | ||
} else if (data) { | ||
setCookies( | ||
req, | ||
res, | ||
[ | ||
{ key: 'access-token', value: data.access_token }, | ||
{ key: 'refresh-token', value: data.refresh_token! } | ||
].map((token) => ({ | ||
name: `${cookieOptions.name}-${token.key}`, | ||
value: token.value, | ||
domain: cookieOptions.domain, | ||
maxAge: cookieOptions.lifetime ?? 0, | ||
path: cookieOptions.path, | ||
sameSite: cookieOptions.sameSite | ||
})) | ||
); | ||
res.status(200).json(user); | ||
} | ||
} | ||
res.status(200).json(user); | ||
} catch (e) { | ||
const error = e as ApiError; | ||
res.status(400).json({ error: error.message }); | ||
} | ||
} |
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 @@ | ||
export * from './handlers'; |
Empty file.
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 @@ | ||
export interface CookieOptions { | ||
// (Optional) The Cookie name prefix. Defaults to `sb` meaning the cookies will be `sb-access-token` and `sb-refresh-token`. | ||
name?: string; | ||
// (Optional) The cookie lifetime (expiration) in seconds. Set to 8 hours by default. | ||
lifetime?: number; | ||
// (Optional) The cookie domain this should run on. Leave it blank to restrict it to your domain. | ||
domain?: string; | ||
path?: string; | ||
// (Optional) SameSite configuration for the session cookie. Defaults to 'lax', but can be changed to 'strict' or 'none'. Set it to false if you want to disable the SameSite setting. | ||
sameSite?: string; | ||
} | ||
|
||
export interface ApiError { | ||
message: string; | ||
status: number; | ||
} |
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
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,191 @@ | ||
type Cookie = { | ||
name: string; | ||
value: string; | ||
maxAge: number; | ||
domain?: string; | ||
path?: string; | ||
sameSite?: string; | ||
}; | ||
|
||
/** | ||
* Serialize data into a cookie header. | ||
*/ | ||
function serialize( | ||
name: string, | ||
val: string, | ||
options: { | ||
maxAge: number; | ||
domain: string; | ||
path: string; | ||
expires: Date; | ||
httpOnly: boolean; | ||
secure: boolean; | ||
sameSite: string; | ||
} | ||
) { | ||
const opt = options || {}; | ||
const enc = encodeURIComponent; | ||
/* eslint-disable-next-line no-control-regex */ | ||
const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; | ||
|
||
if (typeof enc !== 'function') { | ||
throw new TypeError('option encode is invalid'); | ||
} | ||
|
||
if (!fieldContentRegExp.test(name)) { | ||
throw new TypeError('argument name is invalid'); | ||
} | ||
|
||
const value = enc(val); | ||
|
||
if (value && !fieldContentRegExp.test(value)) { | ||
throw new TypeError('argument val is invalid'); | ||
} | ||
|
||
let str = name + '=' + value; | ||
|
||
if (null != opt.maxAge) { | ||
const maxAge = opt.maxAge - 0; | ||
|
||
if (isNaN(maxAge) || !isFinite(maxAge)) { | ||
throw new TypeError('option maxAge is invalid'); | ||
} | ||
|
||
str += '; Max-Age=' + Math.floor(maxAge); | ||
} | ||
|
||
if (opt.domain) { | ||
if (!fieldContentRegExp.test(opt.domain)) { | ||
throw new TypeError('option domain is invalid'); | ||
} | ||
|
||
str += '; Domain=' + opt.domain; | ||
} | ||
|
||
if (opt.path) { | ||
if (!fieldContentRegExp.test(opt.path)) { | ||
throw new TypeError('option path is invalid'); | ||
} | ||
|
||
str += '; Path=' + opt.path; | ||
} | ||
|
||
if (opt.expires) { | ||
if (typeof opt.expires.toUTCString !== 'function') { | ||
throw new TypeError('option expires is invalid'); | ||
} | ||
|
||
str += '; Expires=' + opt.expires.toUTCString(); | ||
} | ||
|
||
if (opt.httpOnly) { | ||
str += '; HttpOnly'; | ||
} | ||
|
||
if (opt.secure) { | ||
str += '; Secure'; | ||
} | ||
|
||
if (opt.sameSite) { | ||
const sameSite = | ||
typeof opt.sameSite === 'string' | ||
? opt.sameSite.toLowerCase() | ||
: opt.sameSite; | ||
|
||
switch (sameSite) { | ||
case 'lax': | ||
str += '; SameSite=Lax'; | ||
break; | ||
case 'strict': | ||
str += '; SameSite=Strict'; | ||
break; | ||
case 'none': | ||
str += '; SameSite=None'; | ||
break; | ||
default: | ||
throw new TypeError('option sameSite is invalid'); | ||
} | ||
} | ||
|
||
return str; | ||
} | ||
|
||
/** | ||
* Based on the environment and the request we know if a secure cookie can be set. | ||
*/ | ||
function isSecureEnvironment(req: any) { | ||
if (!req || !req.headers || !req.headers.host) { | ||
throw new Error('The "host" request header is not available'); | ||
} | ||
|
||
const host = | ||
(req.headers.host.indexOf(':') > -1 && req.headers.host.split(':')[0]) || | ||
req.headers.host; | ||
if ( | ||
['localhost', '127.0.0.1'].indexOf(host) > -1 || | ||
host.endsWith('.local') | ||
) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* Serialize a cookie to a string. | ||
*/ | ||
function serializeCookie(cookie: Cookie, secure: boolean) { | ||
return serialize(cookie.name, cookie.value, { | ||
maxAge: cookie.maxAge, | ||
expires: new Date(Date.now() + cookie.maxAge * 1000), | ||
httpOnly: true, | ||
secure, | ||
path: cookie.path ?? '/', | ||
domain: cookie.domain ?? '', | ||
sameSite: cookie.sameSite ?? 'lax' | ||
}); | ||
} | ||
|
||
/** | ||
* Get Cookie Header strings. | ||
*/ | ||
export function getCookieString( | ||
req: any, | ||
res: any, | ||
cookies: Array<Cookie> | ||
): string[] { | ||
const strCookies = cookies.map((c) => | ||
serializeCookie(c, isSecureEnvironment(req)) | ||
); | ||
const previousCookies = res.getHeader('Set-Cookie'); | ||
if (previousCookies) { | ||
if (previousCookies instanceof Array) { | ||
Array.prototype.push.apply(strCookies, previousCookies); | ||
} else if (typeof previousCookies === 'string') { | ||
strCookies.push(previousCookies); | ||
} | ||
} | ||
return strCookies; | ||
} | ||
|
||
/** | ||
* Set one or more cookies. | ||
*/ | ||
export function setCookies(req: any, res: any, cookies: Array<Cookie>) { | ||
res.setHeader('Set-Cookie', getCookieString(req, res, cookies)); | ||
} | ||
|
||
/** | ||
* Set one or more cookies. | ||
*/ | ||
export function setCookie(req: any, res: any, cookie: Cookie) { | ||
setCookies(req, res, [cookie]); | ||
} | ||
|
||
export function deleteCookie(req: any, res: any, name: string) { | ||
setCookie(req, res, { | ||
name, | ||
value: '', | ||
maxAge: -1 | ||
}); | ||
} |