-
-
Save ClaytonFarr/d4555f396179b0375652285abe43d37e to your computer and use it in GitHub Desktop.
SvelteKit hooks.js: parsing & passing JWT from cookie
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
// ====================================================== | |
// Netlify Identity Custom API | |
// ====================================================== | |
// Methods to interact directly with a Netlify Identity (GoTrue) instance. | |
// These replicate the functionality of the GoTrue-JS library methods | |
// but are able to run server side / within serverless funcitons (e.g. do not rely on `window`) | |
// ------------------------------------------------------ | |
// Netlify Identity methods | |
// ------------------------------------------------------ | |
export async function signupUser(email, password) { | |
if (!email) return { ok: false, status: 400, body: JSON.stringify({ error: 'An email is required.' }) }; | |
if (!password) return { ok: false, status: 400, body: JSON.stringify({ error: 'A password is required.' }) }; | |
return await ask({ method: 'POST', endpoint: 'signup', data: { email, password } }) | |
} | |
export async function confirmUser(confirmationToken) { | |
if (!confirmationToken) return { ok: false, status: 400, body: JSON.stringify({ error: 'A token is required.' }) }; | |
return await ask({ method: 'POST', endpoint: 'verify', data: { token: confirmationToken, type: 'signup' } }) | |
} | |
export async function loginUser(email, password) { | |
if (!email) return { ok: false, status: 400, body: JSON.stringify({ error: 'An email is required.' }) }; | |
if (!password) return { ok: false, status: 400, body: JSON.stringify({ error: 'A password is required.' }) }; | |
const data = `grant_type=password&username=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`; | |
return await ask({ method: 'POST', endpoint: 'token', data, contentType: 'urlencoded', stringify: false }) | |
} | |
export async function requestPasswordRecovery(email) { | |
if (!email) return { ok: false, status: 400, body: JSON.stringify({ error: 'An email is required.' }) }; | |
return await ask({ method: 'POST', endpoint: 'recover', data: { email } }) | |
} | |
export async function verifyPasswordRecovery(recoveryToken) { | |
if (!recoveryToken) return { ok: false, status: 400, body: JSON.stringify({ error: 'A token is required.' }) }; | |
return await ask({ method: 'POST', endpoint: 'verify', data: { token: recoveryToken, type: 'recovery' } }) | |
} | |
export async function requestNewToken(refreshToken) { | |
// TODO: test NI requestNewToken method | |
if (!refreshToken) return { ok: false, status: 400, body: JSON.stringify({ error: 'A token is required.' }) }; | |
const data = `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`; | |
return await ask({ method: 'POST', endpoint: 'token', data, contentType: 'urlencoded', stringify: false }) | |
} | |
export async function getUser(accessToken) { | |
// TODO: test NI getUser method | |
// Note - this method requires an admin token, available from a serverless function's 'context.clientContext' object. | |
// For this, it hits a custom serverless function as a pass thru, instead of the Netlify Indentity endpoint directly. | |
// In this project, the source for this serverless function is at `src/additional_functions/getUser.js`. | |
if (!accessToken) return { ok: false, status: 400, body: JSON.stringify({ error: 'A token is required.' }) }; | |
return await ask({ method: 'GET', endpoint: '.netlify/functions/getUser', data: null, token: accessToken }) | |
} | |
export async function updateUser(accessToken, data) { | |
// TODO: test NI updateUser method | |
if (!accessToken) return { ok: false, status: 400, body: JSON.stringify({ error: 'A token is required.' }) }; | |
return await ask({ method: 'PUT', endpoint: 'user', data, token: accessToken }) | |
} | |
export async function confirmEmailChange(accessToken, emailChangeToken) { | |
if (!accessToken || !emailChangeToken) return { ok: false, status: 400, body: JSON.stringify({ error: 'A token is required.' }) }; | |
return await ask({ method: 'PUT', endpoint: 'user', data: { email_change_token: emailChangeToken }, token: accessToken }) | |
} | |
export async function logoutUser(accessToken) { | |
if (!accessToken) return { ok: false, status: 400, body: JSON.stringify({ error: 'A token is required.' }) }; | |
return await ask({ method: 'POST', endpoint: 'logout', data: null, token: accessToken }) | |
} | |
export async function deleteUser(accessToken) { | |
// TODO: test NI deleteUser method | |
// Note - this method requires an admin token, available from a serverless function's 'context.clientContext' object. | |
// For this, it hits a custom serverless function as a pass thru, instead of the Netlify Indentity endpoint directly. | |
// In this project, the source for this serverless function is at `src/additional_functions/deleteUser.js`. | |
if (!accessToken) return { ok: false, status: 400, body: JSON.stringify({ error: 'A token is required.' }) }; | |
return await ask({ method: 'DELETE', endpoint: '.netlify/functions/deleteUser', token: accessToken }) | |
} | |
export async function getUsers() { | |
// TODO: test NI getUsers method | |
return await ask({ method: 'GET', endpoint: '.netlify/functions/getUsers' }) | |
} | |
// ------------------------------------------------------ | |
// Fetch Utility Function | |
// ------------------------------------------------------ | |
const ask = async ({ method, endpoint, data, token, contentType = 'json', stringify = true, includeCredentials = false }) => { | |
// BUILD REQUEST | |
const opts = { method, headers: {} }; | |
if (data && stringify) | |
opts.body = JSON.stringify(data); | |
if (data && !stringify) | |
opts.body = data; | |
if (data && contentType === 'json') | |
opts.headers['Content-Type'] = 'application/json'; | |
if (data && contentType === 'urlencoded') | |
opts.headers['Content-Type'] = 'application/x-www-form-urlencoded'; | |
if (token) | |
opts.headers['Authorization'] = `Bearer ${token}`; | |
if (includeCredentials) | |
opts.credentials = 'include'; | |
// MAKE REQUEST | |
try { | |
// SvelteKit handles fetch universally on server & in browser | |
const response = await fetch(`${process.env['URL']}/.netlify/identity/${endpoint}`, opts); | |
const text = await response.text(); | |
// if response fails throw error | |
if (!response.ok) { | |
// if a custom error message is present in response, use it | |
let errorMessage = response.statusText; | |
if(response.statusText === 'Method Not Allowed') errorMessage = 'Unable to process request.'; | |
if(text && JSON.parse(text).error_description) errorMessage = JSON.parse(text).error_description; | |
if(text && JSON.parse(text).msg) errorMessage = JSON.parse(text).msg; | |
const errorResponse = { status: response.status, message: errorMessage }; | |
throw errorResponse; | |
} | |
// else continue processing request | |
try { | |
// try to parse response into JSON object | |
return { | |
ok: true, | |
status: response.status, | |
body: JSON.parse(text), | |
}; | |
} catch (error) { | |
// if unable to parse JSON return response as text instead | |
return { | |
ok: true, | |
status: response.status, | |
body: text, | |
}; | |
} | |
} catch (error) { | |
// if request unsuccessful, return error information | |
const errorMessage = error.message || error; | |
return { | |
ok: false, | |
status: error.status, | |
body: { error: errorMessage }, | |
}; | |
} | |
}; | |
// ------------------------------------------------------ | |
// Cookie Utility Functions | |
// ------------------------------------------------------ | |
const JWT_COOKIE_NAME = 'id_jwt'; | |
export const parseIdentityCookies = (request) => { | |
const cookies = request.headers.cookie; | |
let jwt = null; | |
// grab JWT value, if cookie is present | |
if (cookies) jwt = cookies.split('; ').find((row) => row.startsWith(JWT_COOKIE_NAME)) | |
if (jwt) jwt = jwt.slice(JWT_COOKIE_NAME.length + 1); | |
return { jwt } | |
} | |
export function setIdentityCookies(status, body, token) { | |
// wrap passed status & body with JWT cookie | |
return { | |
ok: true, | |
status: status, | |
headers: { 'Set-Cookie': `${JWT_COOKIE_NAME}=${token}; Path=/; SameSite=Lax; HttpOnly; Secure; Max-Age=3600;` }, | |
body, | |
}; | |
} | |
export const clearIdentityCookies = (status, body) => { | |
// wrap passed status & body with null value cookie | |
return { | |
ok: true, | |
status: status, | |
headers: { 'Set-Cookie': `${JWT_COOKIE_NAME}=; Path=/; SameSite=Lax; HttpOnly; Secure; Max-Age=0;` }, | |
body, | |
}; | |
} |
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
// ====================================================== | |
// Helpers | |
// ====================================================== | |
export function isValidJSONString(str) { | |
try { | |
JSON.parse(str); | |
} catch (e) { | |
return false; | |
} | |
return true; | |
} | |
export const parseJwt = (jwt) => { | |
const base64Url = jwt.split('.')[1]; | |
if(base64Url) { | |
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); | |
const buff = Buffer.from(base64, 'base64'); | |
const payloadinit = buff.toString('ascii'); | |
if (isValidJSONString(payloadinit)) { | |
return JSON.parse(payloadinit); | |
} | |
} | |
}; |
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
import { parseIdentityCookies } from '$lib/apis/auth-api' | |
import { parseJwt } from '$lib/utils/helpers' | |
// ============================================================================== | |
// HANDLE | |
// ============================================================================== | |
export async function handle({ request, render }) { | |
// parse jwt from cookie in request, if present, and populate locals.user | |
const { jwt } = parseIdentityCookies(request); | |
if (jwt) { | |
request.locals.token = jwt; | |
request.locals.user = parseJwt(jwt); | |
} | |
if (request.locals.user) { | |
request.locals.user.authenticated = true; | |
} else { | |
request.locals.user = {}; | |
request.locals.user.authenticated = false; | |
} | |
// process requested route/endpoint | |
const response = await render(request); | |
return { | |
...response, | |
headers: { | |
...response.headers, | |
// 'x-custom-header': 'potato', | |
} | |
}; | |
} | |
// ============================================================================== | |
// GETSESSION | |
// ============================================================================== | |
export function getSession(request) { | |
return { | |
user: { | |
authenticated: request.locals.user.authenticated || false, | |
authExpires: request.locals.user.exp || null, | |
id: request.locals.user.sub || null, | |
email: request.locals.user.email || null, | |
} | |
}; | |
} |
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
import * as auth from '$lib/apis/auth-api'; | |
export async function post(request) { | |
// NOTE - there is a 15 minute default timeout between sending 'email change' emails | |
// 'SMTP_MAX_FREQUENCY' setting @ https://github.com/netlify/gotrue/blob/master/README.md#e-mail | |
// attempt to initiate 'update email' process | |
// if successful, will send email change confirmation email | |
const { token } = request.locals; | |
const { email } = request.body; | |
const data = await auth.updateUser(token, { email }); | |
// if unsuccessful | |
if (!data.ok) { | |
let errorMessage = data.body.error; | |
if (data.body.error === 'Bad Request') errorMessage = 'Unable to update email.'; | |
if (data.body.error === 'Bad Gateway') errorMessage = 'Issue reaching server - please submit again.'; | |
return { | |
ok: false, | |
status: data.status, | |
body: { error: errorMessage }, | |
} | |
}; | |
// if successful | |
return { | |
ok: true, | |
status: data.status, | |
body: { | |
pendingEmail: data.body.new_email, | |
emailChangeSentAt: data.body.email_change_sent_at, | |
} | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment