cf-auth0
Version:
🔐 Auth0 Client on Cloudflare Pages
239 lines (238 loc) • 9.17 kB
JavaScript
import * as jwt from './server/auth/jsonwebtoken';
import { attempt } from '@jill64/attempt';
import crypto from 'node:crypto';
import { JwksClient } from './server/auth/jwks-rsa';
const COOKIE_DURATION_SECONDS = 60 * 60 * 24 * 7; // 1 week
export class CfAuth0 {
auth0ClientId;
auth0ClientSecret;
auth0Domain;
auth0CustomDomain;
baseUrl;
auth0CookieName;
sessionSecret;
callbackPath;
loginPath;
logoutPath;
isSvelteKit;
jwtPayload = {};
cachedKey = undefined;
constructor({ auth0CookieName = 'auth0', sessionSecret, auth0ClientId, auth0ClientSecret, auth0Domain, auth0CustomDomain = auth0Domain, baseUrl, callbackPath, loginPath, logoutPath, isSvelteKit }) {
this.auth0ClientId = auth0ClientId;
this.auth0ClientSecret = auth0ClientSecret;
this.auth0Domain = auth0Domain;
this.auth0CustomDomain = auth0CustomDomain;
this.baseUrl = baseUrl;
this.auth0CookieName = auth0CookieName;
this.sessionSecret = sessionSecret;
this.callbackPath = callbackPath;
this.loginPath = loginPath;
this.isSvelteKit = isSvelteKit;
this.logoutPath = logoutPath;
}
/** Please redirect to return URL after calling this function */
async auth({ cookies, url }) {
const cookie = cookies.get(this.auth0CookieName);
if (cookie) {
const payload = await attempt(() => jwt.verify(cookie, this.sessionSecret), () => {
cookies.delete(this.auth0CookieName, { path: '/' });
return this.baseUrl;
});
if (typeof payload === 'string') {
return payload;
}
await this.setAuthCookie({
cookies,
payload
});
this.jwtPayload = payload;
return payload;
}
const { pathname, search, hash } = url;
return `${this.loginPath}?returnUrl=${pathname}${search}${hash}`;
}
/** Please redirect to return URL after calling this function */
login({ cookies, url }) {
const csrfState = crypto.randomBytes(16).toString('hex');
cookies.set('csrfState', csrfState, {
httpOnly: true,
sameSite: 'lax',
maxAge: 1000,
path: '/'
});
const returnUrl = encodeURIComponent(url.searchParams.get('returnUrl') || '/');
const query = {
scope: 'openid profile email',
response_type: 'code',
client_id: this.auth0ClientId,
redirect_uri: `${this.baseUrl}${this.callbackPath}?returnUrl=${returnUrl}`,
state: csrfState
};
return `https://${this.auth0CustomDomain}/authorize?${new URLSearchParams(query).toString()}`;
}
async verifyToken(token) {
return jwt.verify(token, async (header) => {
const client = new JwksClient({
jwksUri: `https://${this.auth0Domain}/.well-known/jwks.json`
});
const key = await client.getSigningKey(header.kid);
if (this.cachedKey) {
return this.cachedKey;
}
else {
const signingKey = key?.getPublicKey();
this.cachedKey = signingKey;
return signingKey;
}
});
}
async getToken({ code, redirect_uri }) {
const resp = await fetch(`https://${this.auth0Domain}/oauth/token`, {
method: 'POST',
body: JSON.stringify({
code,
client_id: this.auth0ClientId,
client_secret: this.auth0ClientSecret,
redirect_uri,
grant_type: 'authorization_code'
}),
headers: {
'Content-Type': 'application/json'
}
});
return await resp.json();
}
async getAuthUser(cookies) {
const jwtToken = cookies.get(this.auth0CookieName);
if (!jwtToken) {
return null;
}
return jwt.decode(jwtToken);
}
/** Please redirect to return URL after calling this function */
logout({ cookies, returnUrl = this.baseUrl }) {
cookies.delete(this.auth0CookieName, { path: '/' });
return `https://${this.auth0CustomDomain}/logout?client_id=${this.auth0ClientId}&returnTo=${returnUrl}`;
}
async callback({ cookies, url }) {
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
let returnUrl = url.searchParams.get('returnUrl') || '/';
if (this.isSvelteKit && returnUrl.includes('/__data.json')) {
returnUrl = returnUrl.replace('/__data.json', '');
}
const csrfState = cookies.get('csrfState');
if (state !== csrfState || !code) {
if (csrfState === undefined) {
return this.loginPath;
}
throw new Error('Invalid state');
}
const token = await this.getToken({
code,
redirect_uri: `${this.baseUrl}${this.callbackPath}`
});
const authUser = await this.verifyToken(token.id_token);
await this.setAuthCookie({
cookies,
payload: authUser
});
cookies.delete('csrfState', { path: '/' });
return returnUrl;
}
async delete() {
const accessToken = await this.getManagementApiToken();
await this.deleteAuth0User(accessToken);
return this.logoutPath;
}
async changeEmail(newEmail) {
const accessToken = await this.getManagementApiToken();
await this.changeUserEmail(newEmail, accessToken);
return this.logoutPath;
}
async changePassword(newPassword) {
const accessToken = await this.getManagementApiToken();
await this.changeUserPassword({
new_password: newPassword,
access_token: accessToken
});
return this.logoutPath;
}
async getManagementApiToken() {
const url = `https://${this.auth0Domain}/oauth/token`;
const response = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
client_id: this.auth0ClientId,
client_secret: this.auth0ClientSecret,
audience: `https://${this.auth0Domain}/api/v2/`,
grant_type: 'client_credentials'
})
});
if (!response.ok) {
throw new Error(`Failed to Get Token Error: ${response.status} ${response.statusText}`);
}
const data = (await response.json());
return data.access_token;
}
async deleteAuth0User(accessToken) {
console.log('this.jwtPayload.sub', this.jwtPayload.sub);
const url = `https://${this.auth0Domain}/api/v2/users/${encodeURIComponent(this.jwtPayload.sub)}`;
const response = await fetch(url, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${accessToken}`
}
});
if (!response.ok) {
throw new Error(`Failed to Get Token: ${response.status} ${response.statusText}`);
}
}
async changeUserEmail(newEmail, accessToken) {
const url = `https://${this.auth0Domain}/api/v2/users/${encodeURIComponent(this.jwtPayload.sub)}`;
const res = await fetch(url, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: newEmail,
connection: 'Username-Password-Authentication',
verify_email: true
})
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Failed to update user email. Status: ${res.status} - ${errorText}`);
}
}
async changeUserPassword({ new_password, access_token }) {
const updateUrl = `https://${this.auth0Domain}/api/v2/users/${encodeURIComponent(this.jwtPayload.sub)}`;
const updateResponse = await fetch(updateUrl, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${access_token}`
},
body: JSON.stringify({
password: new_password,
connection: 'Username-Password-Authentication' // Database connection の名前
})
});
if (!updateResponse.ok) {
const errorText = await updateResponse.text();
throw new Error(`Failed to update user password: ${updateResponse.status} - ${errorText}`);
}
}
async setAuthCookie({ cookies, payload }) {
const cookieValue = await jwt.sign(payload, this.sessionSecret);
cookies.set(this.auth0CookieName, cookieValue, {
httpOnly: true,
sameSite: 'lax',
maxAge: COOKIE_DURATION_SECONDS,
path: '/'
});
}
}