Skip to content

Instantly share code, notes, and snippets.

@ClaytonFarr
Last active June 13, 2021 19:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ClaytonFarr/d4555f396179b0375652285abe43d37e to your computer and use it in GitHub Desktop.
Save ClaytonFarr/d4555f396179b0375652285abe43d37e to your computer and use it in GitHub Desktop.
SvelteKit hooks.js: parsing & passing JWT from cookie
// ======================================================
// 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,
};
}
// ======================================================
// 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);
}
}
};
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,
}
};
}
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