Organize directories
This commit is contained in:
parent
8a91c9b89b
commit
4898382f78
433 changed files with 0 additions and 0 deletions
|
|
@ -1,210 +0,0 @@
|
|||
import { promisify } from "util";
|
||||
import jwt from "jsonwebtoken";
|
||||
import jwksClient from "jwks-rsa";
|
||||
import * as Boom from "@hapi/boom";
|
||||
import * as Wreck from "@hapi/wreck";
|
||||
import Providers from "next-auth/providers";
|
||||
import type { Adapter } from "next-auth/adapters";
|
||||
import type { IncomingMessage } from "http";
|
||||
|
||||
const CF_JWT_HEADER_NAME = "cf-access-jwt-assertion";
|
||||
const CF_JWT_ALGOS = ["RS256"];
|
||||
|
||||
export type VerifyFn = (token: string) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Returns a function that will accept a jwt and verify it against the cloudflare access details
|
||||
*
|
||||
* @param audience the cloudflare access audience id
|
||||
* @param domain the cloudflare access domain
|
||||
*/
|
||||
export const cfVerifier = (audience: string, domain: string): VerifyFn => {
|
||||
if (!audience || !domain)
|
||||
throw Boom.badImplementation(
|
||||
"Cloudflare configuration is missing. See project documentation."
|
||||
);
|
||||
const issuer = `https://${domain}`;
|
||||
const client = jwksClient({
|
||||
jwksUri: `${issuer}/cdn-cgi/access/certs`,
|
||||
});
|
||||
|
||||
return async (token) => {
|
||||
const getKey = (header, callback) => {
|
||||
client.getSigningKey(header.kid, function (err, key) {
|
||||
if (err)
|
||||
throw Boom.serverUnavailable(
|
||||
"failed to fetch cloudflare access jwks"
|
||||
);
|
||||
callback(undefined, key.getPublicKey());
|
||||
});
|
||||
};
|
||||
|
||||
const opts = {
|
||||
algorithms: CF_JWT_ALGOS,
|
||||
audience,
|
||||
issuer,
|
||||
};
|
||||
// @ts-expect-error: Too many args
|
||||
return promisify(jwt.verify)(token, getKey, opts);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies the Cloudflare Access JWT and returns the decoded token's contents.
|
||||
* Throws if the token is missing or invalid.
|
||||
*
|
||||
* @param verifier the verification function
|
||||
* @param req the incoming http request to verify
|
||||
* @return the original token and the decoded contents.
|
||||
*/
|
||||
export const verifyRequest = async (
|
||||
verifier: VerifyFn,
|
||||
req: IncomingMessage
|
||||
): Promise<{ token: string; decoded: any }> => {
|
||||
const token = req.headers[CF_JWT_HEADER_NAME] as string;
|
||||
if (token) {
|
||||
try {
|
||||
const decoded = await verifier(token);
|
||||
return { token, decoded };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw Boom.unauthorized("invalid cloudflare access token");
|
||||
}
|
||||
}
|
||||
|
||||
throw Boom.unauthorized("cloudflare access token missing");
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches user identity information from cloudflare.
|
||||
*
|
||||
* @param domain the cloudflare access domain
|
||||
* @param token the encoded jwt token for the user
|
||||
* @see https://developers.cloudflare.com/access/setting-up-access/json-web-token#groups-within-a-jwt
|
||||
*/
|
||||
export const getIdentity = async (
|
||||
domain: string,
|
||||
token: string
|
||||
): Promise<any> => {
|
||||
const { payload } = await Wreck.get(
|
||||
`https://${domain}/cdn-cgi/access/get-identity`,
|
||||
{
|
||||
headers: {
|
||||
Cookie: `CF_Authorization=${token}`,
|
||||
},
|
||||
json: true,
|
||||
}
|
||||
);
|
||||
return payload;
|
||||
};
|
||||
|
||||
const cloudflareAccountProvider = "cloudflare-access";
|
||||
|
||||
const cloudflareAuthorizeCallback = (
|
||||
req: IncomingMessage,
|
||||
domain: string,
|
||||
verifier: VerifyFn,
|
||||
adapter: Adapter
|
||||
): (() => Promise<any>) => async () => {
|
||||
/*
|
||||
|
||||
lots of little variables in here.
|
||||
|
||||
token: the encoded jwt from cloudflare access
|
||||
decoded: the decoded jwt containing the content cloudflare gives us
|
||||
identity: we call the cloudflare access identity endpoint to retrieve more user identity information
|
||||
this data is identity provider specific, so the format is unknown
|
||||
it would be possible to support specific identity providers and have roles/groups
|
||||
profile: this is the accumulated user information we have that we will fetch/build the user record with
|
||||
*/
|
||||
|
||||
const { token, decoded } = await verifyRequest(verifier, req);
|
||||
|
||||
const profile = {
|
||||
email: undefined,
|
||||
name: undefined,
|
||||
avatar: undefined,
|
||||
};
|
||||
if (decoded.email) profile.email = decoded.email;
|
||||
if (decoded.name) profile.name = decoded.name;
|
||||
const identity = await getIdentity(domain, token);
|
||||
|
||||
if (identity.email) profile.email = identity.email;
|
||||
if (identity.name) profile.name = identity.name;
|
||||
|
||||
if (!profile.email)
|
||||
throw new Error("cloudflare access authorization: email not found");
|
||||
|
||||
const providerId = `cfaccess|${identity.idp.type}|${identity.idp.id}`;
|
||||
const providerAccountId = identity.user_uuid;
|
||||
|
||||
if (!providerAccountId)
|
||||
throw new Error(
|
||||
"cloudflare access authorization: missing provider account id"
|
||||
);
|
||||
|
||||
const {
|
||||
getUserByProviderAccountId,
|
||||
getUserByEmail,
|
||||
createUser,
|
||||
linkAccount,
|
||||
} =
|
||||
// @ts-expect-error: non-existent property
|
||||
await adapter.getAdapter({} as any);
|
||||
|
||||
const userByProviderAccountId = await getUserByProviderAccountId(
|
||||
providerId,
|
||||
providerAccountId
|
||||
);
|
||||
if (userByProviderAccountId) {
|
||||
return userByProviderAccountId;
|
||||
}
|
||||
|
||||
const userByEmail = await getUserByEmail(profile.email);
|
||||
if (userByEmail) {
|
||||
// we will not explicitly link accounts
|
||||
throw new Error(
|
||||
"cloudflare access authorization: user exists for email address, but is not linked."
|
||||
);
|
||||
}
|
||||
|
||||
const user = await createUser(profile);
|
||||
|
||||
// between the previous line and the next line exists a transactional bug
|
||||
// https://github.com/nextauthjs/next-auth/issues/876
|
||||
// hopefully we don't experience it
|
||||
|
||||
await linkAccount(
|
||||
user.id,
|
||||
providerId,
|
||||
cloudflareAccountProvider,
|
||||
providerAccountId,
|
||||
// the following are unused but are specified for completness
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param audience the cloudflare access audience id
|
||||
* @param domain the cloudflare access domain (including the .cloudflareaccess.com bit)
|
||||
* @param adapter the next-auth adapter used to talk to the backend
|
||||
* @param req the incoming request object used to parse the jwt from
|
||||
*/
|
||||
export const CloudflareAccessProvider = (
|
||||
audience: string,
|
||||
domain: string,
|
||||
adapter: Adapter,
|
||||
req: IncomingMessage
|
||||
) => {
|
||||
const verifier = cfVerifier(audience, domain);
|
||||
return Providers.Credentials({
|
||||
id: cloudflareAccountProvider,
|
||||
name: "Cloudflare Access",
|
||||
credentials: {},
|
||||
authorize: cloudflareAuthorizeCallback(req, domain, verifier, adapter),
|
||||
});
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue