=> {
try {
assertReqRes(req, res);
- if (!sessionCache.isAuthenticated(req, res)) {
- res.status(401).json({
- error: 'not_authenticated',
- description: 'The user does not have an active session or is not authenticated'
- });
+ if (!(await sessionCache.isAuthenticated(req, res))) {
+ res.status(204).end();
return;
}
- const session = sessionCache.get(req, res) as Session;
+ const session = (await sessionCache.get(req, res)) as Session;
res.setHeader('Cache-Control', 'no-store');
- if (options?.refetch) {
+ if (options.refetch) {
const { accessToken } = await getAccessToken(req, res);
if (!accessToken) {
throw new Error('No access token available to refetch the profile');
@@ -77,7 +141,7 @@ export default function profileHandler(
newSession = await options.afterRefetch(req, res, newSession);
}
- sessionCache.set(req, res, newSession);
+ await sessionCache.set(req, res, newSession);
res.json(newSession.user);
return;
@@ -85,7 +149,20 @@ export default function profileHandler(
res.json(session.user);
} catch (e) {
- throw new HandlerError(e);
+ throw new ProfileHandlerError(e as HandlerErrorCause);
+ }
+ };
+ return (
+ reqOrOptions: NextApiRequest | ProfileOptionsProvider | ProfileOptions,
+ res?: NextApiResponse,
+ options?: ProfileOptions
+ ): any => {
+ if (reqOrOptions instanceof IncomingMessage && res) {
+ return profile(reqOrOptions, res, options);
+ }
+ if (typeof reqOrOptions === 'function') {
+ return (req: NextApiRequest, res: NextApiResponse) => profile(req, res, reqOrOptions(req));
}
+ return (req: NextApiRequest, res: NextApiResponse) => profile(req, res, reqOrOptions as ProfileOptions);
};
}
diff --git a/src/helpers/get-server-side-props-wrapper.ts b/src/helpers/get-server-side-props-wrapper.ts
deleted file mode 100644
index 0420b6fb3..000000000
--- a/src/helpers/get-server-side-props-wrapper.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { GetServerSideProps } from 'next';
-import { ParsedUrlQuery } from 'querystring';
-import SessionCache from '../session/cache';
-
-/**
- * If you're using >=Next 12 and {@link getSession} or {@link getAccessToken} without `withPageAuthRequired`, because
- * you don't want to require authentication on your route, you might get a warning/error: "You should not access 'res'
- * after getServerSideProps resolves". You can work around this by wrapping your `getServerSideProps` in
- * `getServerSidePropsWrapper`, this ensures that the code that accesses `res` will run within
- * the lifecycle of `getServerSideProps`, avoiding the warning/error eg:
- *
- * **NOTE: you do not need to do this if you're already using {@link WithPageAuthRequired}**
- *
- * ```js
- * // pages/protected-page.js
- * import { withPageAuthRequired } from '@auth0/nextjs-auth0';
- *
- * export default function ProtectedPage() {
- * return Protected content
;
- * }
- *
- * export const getServerSideProps = getServerSidePropsWrapper(async (ctx) => {
- * const session = getSession(ctx.req, ctx.res);
- * if (session) {
- * // Use is authenticated
- * } else {
- * // User is not authenticated
- * }
- * });
- * ```
- *
- * @category Server
- */
-export type GetServerSidePropsWrapper = (
- getServerSideProps: GetServerSideProps
-) => GetServerSideProps
;
-
-/**
- * @ignore
- */
-export default function getServerSidePropsWrapperFactory(getSessionCache: () => SessionCache) {
- return function getServerSidePropsWrapper(getServerSideProps: GetServerSideProps): GetServerSideProps {
- return async function wrappedGetServerSideProps(...args) {
- const sessionCache = getSessionCache();
- const [ctx] = args;
- sessionCache.init(ctx.req, ctx.res, false);
- const ret = await getServerSideProps(...args);
- sessionCache.save(ctx.req, ctx.res);
- return ret;
- };
- };
-}
diff --git a/src/helpers/index.ts b/src/helpers/index.ts
index d57ed453d..b2024e2e5 100644
--- a/src/helpers/index.ts
+++ b/src/helpers/index.ts
@@ -6,7 +6,3 @@ export {
WithPageAuthRequiredOptions,
PageRoute
} from './with-page-auth-required';
-export {
- default as getServerSidePropsWrapperFactory,
- GetServerSidePropsWrapper
-} from './get-server-side-props-wrapper';
diff --git a/src/helpers/testing.ts b/src/helpers/testing.ts
new file mode 100644
index 000000000..05ce5bf54
--- /dev/null
+++ b/src/helpers/testing.ts
@@ -0,0 +1,33 @@
+import { Config as BaseConfig, CookieConfig, CookieStore, NodeCookies as Cookies } from '../auth0-session';
+import { Session } from '../session';
+
+/**
+ * Configuration parameters used by {@link generateSessionCookie}.
+ */
+export type GenerateSessionCookieConfig = {
+ /**
+ * The secret used to derive an encryption key for the session cookie.
+ *
+ * **IMPORTANT**: you must use the same value as in the SDK configuration.
+ * See {@link ConfigParameters.secret}.
+ */
+ secret: string;
+
+ /**
+ * Integer value, in seconds, used as the duration of the session cookie.
+ * Defaults to `604800` seconds (7 days).
+ */
+ duration?: number;
+} & Partial;
+
+export const generateSessionCookie = async (
+ session: Partial,
+ config: GenerateSessionCookieConfig
+): Promise => {
+ const weekInSeconds = 7 * 24 * 60 * 60;
+ const { secret, duration: absoluteDuration = weekInSeconds, ...cookie } = config;
+ const cookieStoreConfig = { secret, session: { absoluteDuration, cookie } };
+ const cookieStore = new CookieStore(cookieStoreConfig as BaseConfig, Cookies);
+ const epoch = (Date.now() / 1000) | 0;
+ return cookieStore.encrypt(session, { iat: epoch, uat: epoch, exp: epoch + absoluteDuration });
+};
diff --git a/src/helpers/with-api-auth-required.ts b/src/helpers/with-api-auth-required.ts
index 8799e2bc5..76597cfb7 100644
--- a/src/helpers/with-api-auth-required.ts
+++ b/src/helpers/with-api-auth-required.ts
@@ -3,8 +3,8 @@ import { SessionCache } from '../session';
import { assertReqRes } from '../utils/assert';
/**
- * Wrap an API Route to check that the user has a valid session. If they're not logged in the handler will return a
- * 401 Unauthorized.
+ * Wrap an API route to check that the user has a valid session. If they're not logged in the
+ * handler will return a 401 Unauthorized.
*
* ```js
* // pages/api/protected-route.js
@@ -26,18 +26,19 @@ export type WithApiAuthRequired = (apiRoute: NextApiHandler) => NextApiHandler;
* @ignore
*/
export default function withApiAuthFactory(sessionCache: SessionCache): WithApiAuthRequired {
- return (apiRoute) => async (req: NextApiRequest, res: NextApiResponse): Promise => {
- assertReqRes(req, res);
+ return (apiRoute) =>
+ async (req: NextApiRequest, res: NextApiResponse): Promise => {
+ assertReqRes(req, res);
- const session = sessionCache.get(req, res);
- if (!session || !session.user) {
- res.status(401).json({
- error: 'not_authenticated',
- description: 'The user does not have an active session or is not authenticated'
- });
- return;
- }
+ const session = await sessionCache.get(req, res);
+ if (!session || !session.user) {
+ res.status(401).json({
+ error: 'not_authenticated',
+ description: 'The user does not have an active session or is not authenticated'
+ });
+ return;
+ }
- await apiRoute(req, res);
- };
+ await apiRoute(req, res);
+ };
}
diff --git a/src/helpers/with-middleware-auth-required.ts b/src/helpers/with-middleware-auth-required.ts
new file mode 100644
index 000000000..0e68cbee9
--- /dev/null
+++ b/src/helpers/with-middleware-auth-required.ts
@@ -0,0 +1,91 @@
+import { NextMiddleware, NextRequest, NextResponse } from 'next/server';
+import { SessionCache } from '../session';
+
+/**
+ * Protect your pages with Next.js Middleware. For example:
+ *
+ * To protect all your routes:
+ *
+ * ```js
+ * // middleware.js
+ * import { withMiddlewareAuthRequired } from '@auth0/nextjs-auth0/middleware';
+ *
+ * export default withMiddlewareAuthRequired();
+ * ```
+ *
+ * To protect specific routes:
+ *
+ * ```js
+ * // middleware.js
+ * import { withMiddlewareAuthRequired } from '@auth0/nextjs-auth0/middleware';
+ *
+ * export default withMiddlewareAuthRequired();
+ *
+ * export const config = {
+ * matcher: '/about/:path*',
+ * };
+ * ```
+ * For more info see: https://nextjs.org/docs/advanced-features/middleware#matching-paths
+ *
+ * To run custom middleware for authenticated users:
+ *
+ * ```js
+ * // middleware.js
+ * import { withMiddlewareAuthRequired, getSession } from '@auth0/nextjs-auth0/middleware';
+ *
+ * export default withMiddlewareAuthRequired(async function middleware(req) {
+ * const res = NextResponse.next();
+ * const user = await getSession(req, res);
+ * res.cookies.set('hl', user.language);
+ * return res;
+ * });
+ * ```
+ *
+ * @category Server
+ */
+export type WithMiddlewareAuthRequired = (middleware?: NextMiddleware) => NextMiddleware;
+
+/**
+ * @ignore
+ */
+export default function withMiddlewareAuthRequiredFactory(
+ { login, callback, unauthorized }: { login: string; callback: string; unauthorized: string },
+ getSessionCache: () => SessionCache
+): WithMiddlewareAuthRequired {
+ return function withMiddlewareAuthRequired(middleware?): NextMiddleware {
+ return async function wrappedMiddleware(...args) {
+ const [req] = args;
+ const { pathname, origin } = req.nextUrl;
+ const ignorePaths = [login, callback, unauthorized, '/_next', '/favicon.ico'];
+ if (ignorePaths.some((p) => pathname.startsWith(p))) {
+ return;
+ }
+
+ const sessionCache = getSessionCache();
+
+ const authRes = NextResponse.next();
+ const session = await sessionCache.get(req, authRes);
+ if (!session?.user) {
+ if (pathname.startsWith('/api')) {
+ return NextResponse.rewrite(new URL(unauthorized, origin), { status: 401 });
+ }
+ return NextResponse.redirect(
+ new URL(`${login}?returnTo=${encodeURIComponent(req.nextUrl.toString())}`, origin)
+ );
+ }
+ const res = await (middleware && middleware(...args));
+
+ if (res) {
+ const headers = new Headers(res.headers);
+ const cookies = headers.get('set-cookie')?.split(', ') || [];
+ const authCookies = authRes.headers.get('set-cookie')?.split(', ') || [];
+ if (cookies.length || authCookies.length) {
+ headers.set('set-cookie', [...authCookies, ...cookies].join(', '));
+ }
+ return NextResponse.next({ ...res, status: res.status, headers });
+ } else {
+ return authRes;
+ }
+ };
+ };
+}
diff --git a/src/helpers/with-page-auth-required.ts b/src/helpers/with-page-auth-required.ts
index 2ca7b9291..5cef9bf34 100644
--- a/src/helpers/with-page-auth-required.ts
+++ b/src/helpers/with-page-auth-required.ts
@@ -1,19 +1,11 @@
import { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
import { Claims, SessionCache } from '../session';
import { assertCtx } from '../utils/assert';
-import React, { ComponentType } from 'react';
-import {
- UserProps,
- WithPageAuthRequiredOptions as WithPageAuthRequiredCSROptions,
- WithPageAuthRequiredProps
-} from '../frontend/with-page-auth-required';
-import { withPageAuthRequired as withPageAuthRequiredCSR } from '../frontend';
import { ParsedUrlQuery } from 'querystring';
-import getServerSidePropsWrapperFactory from './get-server-side-props-wrapper';
/**
* If you wrap your `getServerSideProps` with {@link WithPageAuthRequired} your props object will be augmented with
- * the user property, which will be the user's {@link Claims}
+ * the user property, which will be the user's {@link Claims}.
*
* ```js
* // pages/profile.js
@@ -31,7 +23,7 @@ import getServerSidePropsWrapperFactory from './get-server-side-props-wrapper';
export type GetServerSidePropsResultWithSession = GetServerSidePropsResult
;
/**
- * A page route that has been augmented with {@link WithPageAuthRequired}
+ * A page route that has been augmented with {@link WithPageAuthRequired}.
*
* @category Server
*/
@@ -42,8 +34,9 @@ export type PageRoute
= (
/**
* If you have a custom returnTo url you should specify it in `returnTo`.
*
- * You can pass in your own `getServerSideProps` method, the props returned from this will be merged with the
- * user props. You can also access the user session data by calling `getSession` inside of this method, eg:
+ * You can pass in your own `getServerSideProps` method, the props returned from this will be
+ * merged with the user props. You can also access the user session data by calling `getSession`
+ * inside of this method. For example:
*
* ```js
* // pages/protected-page.js
@@ -71,7 +64,8 @@ export type WithPageAuthRequiredOptions
(
- Component: ComponentType
,
- options?: WithPageAuthRequiredCSROptions
- ): React.FC
;
-
(opts?: WithPageAuthRequiredOptions
): PageRoute
;
-};
+export type WithPageAuthRequired =
(
+ opts?: WithPageAuthRequiredOptions
+) => PageRoute
;
/**
* @ignore
@@ -104,40 +94,26 @@ export default function withPageAuthRequiredFactory(
loginUrl: string,
getSessionCache: () => SessionCache
): WithPageAuthRequired {
- return (
- optsOrComponent: WithPageAuthRequiredOptions | ComponentType = {},
- csrOpts?: WithPageAuthRequiredCSROptions
- ): any => {
- if (typeof optsOrComponent === 'function') {
- return withPageAuthRequiredCSR(optsOrComponent, csrOpts);
- }
- const { getServerSideProps, returnTo } = optsOrComponent;
- const getServerSidePropsWrapper = getServerSidePropsWrapperFactory(getSessionCache);
- return getServerSidePropsWrapper(
- async (ctx: GetServerSidePropsContext): Promise => {
- assertCtx(ctx);
- const sessionCache = getSessionCache();
- const session = sessionCache.get(ctx.req, ctx.res);
- if (!session?.user) {
- // 10 - redirect
- // 9.5.4 - unstable_redirect
- // 9.4 - res.setHeaders
- return {
- redirect: {
- destination: `${loginUrl}?returnTo=${encodeURIComponent(returnTo || ctx.resolvedUrl)}`,
- permanent: false
- }
- };
- }
- let ret: any = { props: {} };
- if (getServerSideProps) {
- ret = await getServerSideProps(ctx);
- }
- if (ret.props instanceof Promise) {
- return { ...ret, props: ret.props.then((props: any) => ({ ...props, user: session.user })) };
- }
- return { ...ret, props: { ...ret.props, user: session.user } };
+ return ({ getServerSideProps, returnTo } = {}) =>
+ async (ctx) => {
+ assertCtx(ctx);
+ const sessionCache = getSessionCache();
+ const session = await sessionCache.get(ctx.req, ctx.res);
+ if (!session?.user) {
+ return {
+ redirect: {
+ destination: `${loginUrl}?returnTo=${encodeURIComponent(returnTo || ctx.resolvedUrl)}`,
+ permanent: false
+ }
+ };
+ }
+ let ret: any = { props: {} };
+ if (getServerSideProps) {
+ ret = await getServerSideProps(ctx);
+ }
+ if (ret.props instanceof Promise) {
+ return { ...ret, props: ret.props.then((props: any) => ({ user: session.user, ...props })) };
}
- );
- };
+ return { ...ret, props: { user: session.user, ...ret.props } };
+ };
}
diff --git a/src/index.browser.ts b/src/index.browser.ts
deleted file mode 100644
index 1d0732da9..000000000
--- a/src/index.browser.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { InitAuth0, SignInWithAuth0 } from './instance';
-import { GetAccessToken, GetSession } from './session';
-import { WithApiAuthRequired } from './helpers';
-import { HandleAuth, HandleCallback, HandleLogin, HandleLogout, HandleProfile } from './handlers';
-export {
- UserProvider,
- UserProviderProps,
- UserProfile,
- UserContext,
- RequestError,
- useUser,
- withPageAuthRequired,
- WithPageAuthRequired
-} from './frontend';
-
-const serverSideOnly = (method: string): string => `The ${method} method can only be used from the server side`;
-
-const instance: SignInWithAuth0 = {
- getSession() {
- throw new Error(serverSideOnly('getSession'));
- },
- getAccessToken() {
- throw new Error(serverSideOnly('getAccessToken'));
- },
- withApiAuthRequired() {
- throw new Error(serverSideOnly('withApiAuthRequired'));
- },
- handleLogin() {
- throw new Error(serverSideOnly('handleLogin'));
- },
- handleLogout() {
- throw new Error(serverSideOnly('handleLogout'));
- },
- handleCallback() {
- throw new Error(serverSideOnly('handleCallback'));
- },
- handleProfile() {
- throw new Error(serverSideOnly('handleProfile'));
- },
- handleAuth() {
- throw new Error(serverSideOnly('handleAuth'));
- },
- withPageAuthRequired() {
- throw new Error(serverSideOnly('withPageAuthRequired'));
- },
- getServerSidePropsWrapper() {
- throw new Error(serverSideOnly('getServerSidePropsWrapper'));
- }
-};
-
-export const initAuth0: InitAuth0 = () => instance;
-export const getSession: GetSession = (...args) => instance.getSession(...args);
-export const getAccessToken: GetAccessToken = (...args) => instance.getAccessToken(...args);
-export const withApiAuthRequired: WithApiAuthRequired = (...args) => instance.withApiAuthRequired(...args);
-export const handleLogin: HandleLogin = (...args) => instance.handleLogin(...args);
-export const handleLogout: HandleLogout = (...args) => instance.handleLogout(...args);
-export const handleCallback: HandleCallback = (...args) => instance.handleCallback(...args);
-export const handleProfile: HandleProfile = (...args) => instance.handleProfile(...args);
-export const handleAuth: HandleAuth = (...args) => instance.handleAuth(...args);
diff --git a/src/index.ts b/src/index.ts
index 8c7e559e0..9909d5431 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,5 @@
import {
+ NodeCookies as Cookies,
CookieStore,
TransientStore,
clientFactory,
@@ -24,7 +25,8 @@ import {
ProfileOptions,
CallbackOptions,
AfterCallback,
- AfterRefetch
+ AfterRefetch,
+ OnError
} from './handlers';
import {
sessionFactory,
@@ -35,7 +37,9 @@ import {
Session,
AccessTokenRequest,
GetAccessTokenResult,
- Claims
+ Claims,
+ updateSessionFactory,
+ UpdateSession
} from './session/';
import {
withPageAuthRequiredFactory,
@@ -44,17 +48,87 @@ import {
WithPageAuthRequired,
GetServerSidePropsResultWithSession,
WithPageAuthRequiredOptions,
- PageRoute,
- getServerSidePropsWrapperFactory,
- GetServerSidePropsWrapper
+ PageRoute
} from './helpers';
-import { InitAuth0, SignInWithAuth0 } from './instance';
import version from './version';
import { getConfig, getLoginUrl, ConfigParameters } from './config';
+import { setIsUsingNamedExports, setIsUsingOwnInstance } from './utils/instance-check';
+
+/**
+ * The SDK server instance.
+ *
+ * This is created for you when you use the named exports, or you can create your own using {@link InitAuth0}.
+ *
+ * See {@link ConfigParameters} for more info.
+ *
+ * @category Server
+ */
+export interface Auth0Server {
+ /**
+ * Session getter.
+ */
+ getSession: GetSession;
+
+ /**
+ * Append properties to the user.
+ */
+ updateSession: UpdateSession;
+
+ /**
+ * Access token getter.
+ */
+ getAccessToken: GetAccessToken;
+
+ /**
+ * Login handler which will redirect the user to Auth0.
+ */
+ handleLogin: HandleLogin;
+
+ /**
+ * Callback handler which will complete the transaction and create a local session.
+ */
+ handleCallback: HandleCallback;
+
+ /**
+ * Logout handler which will clear the local session and the Auth0 session.
+ */
+ handleLogout: HandleLogout;
+
+ /**
+ * Profile handler which return profile information about the user.
+ */
+ handleProfile: HandleProfile;
+
+ /**
+ * Helper that adds auth to an API route.
+ */
+ withApiAuthRequired: WithApiAuthRequired;
+
+ /**
+ * Helper that adds auth to a Page route.
+ */
+ withPageAuthRequired: WithPageAuthRequired;
+
+ /**
+ * Create the main handlers for your api routes.
+ */
+ handleAuth: HandleAuth;
+}
+
+/**
+ * Initialise your own instance of the SDK.
+ *
+ * See {@link ConfigParameters}.
+ *
+ * @category Server
+ */
+export type InitAuth0 = (params?: ConfigParameters) => Auth0Server;
-let instance: SignInWithAuth0 & { sessionCache: SessionCache };
+let instance: Auth0Server & { sessionCache: SessionCache };
-function getInstance(): SignInWithAuth0 & { sessionCache: SessionCache } {
+// For using managed instance with named exports.
+function getInstance(): Auth0Server & { sessionCache: SessionCache } {
+ setIsUsingNamedExports();
if (instance) {
return instance;
}
@@ -62,13 +136,20 @@ function getInstance(): SignInWithAuth0 & { sessionCache: SessionCache } {
return instance;
}
-export const _initAuth = (params?: ConfigParameters): SignInWithAuth0 & { sessionCache: SessionCache } => {
+// For creating own instance.
+export const initAuth0: InitAuth0 = (params) => {
+ setIsUsingOwnInstance();
+ const { sessionCache, ...publicApi } = _initAuth(params); // eslint-disable-line @typescript-eslint/no-unused-vars
+ return publicApi;
+};
+
+export const _initAuth = (params?: ConfigParameters): Auth0Server & { sessionCache: SessionCache } => {
const { baseConfig, nextConfig } = getConfig(params);
// Init base layer (with base config)
const getClient = clientFactory(baseConfig, { name: 'nextjs-auth0', version });
const transientStore = new TransientStore(baseConfig);
- const cookieStore = new CookieStore(baseConfig);
+ const cookieStore = new CookieStore(baseConfig, Cookies);
const sessionCache = new SessionCache(baseConfig, cookieStore);
const baseHandleLogin = baseLoginHandler(baseConfig, getClient, transientStore);
const baseHandleLogout = baseLogoutHandler(baseConfig, getClient, sessionCache);
@@ -76,10 +157,10 @@ export const _initAuth = (params?: ConfigParameters): SignInWithAuth0 & { sessio
// Init Next layer (with next config)
const getSession = sessionFactory(sessionCache);
+ const updateSession = updateSessionFactory(sessionCache);
const getAccessToken = accessTokenFactory(nextConfig, getClient, sessionCache);
const withApiAuthRequired = withApiAuthRequiredFactory(sessionCache);
const withPageAuthRequired = withPageAuthRequiredFactory(nextConfig.routes.login, () => sessionCache);
- const getServerSidePropsWrapper = getServerSidePropsWrapperFactory(() => sessionCache);
const handleLogin = loginHandler(baseHandleLogin, nextConfig, baseConfig);
const handleLogout = logoutHandler(baseHandleLogout);
const handleCallback = callbackHandler(baseHandleCallback, nextConfig);
@@ -89,10 +170,10 @@ export const _initAuth = (params?: ConfigParameters): SignInWithAuth0 & { sessio
return {
sessionCache,
getSession,
+ updateSession,
getAccessToken,
withApiAuthRequired,
withPageAuthRequired,
- getServerSidePropsWrapper,
handleLogin,
handleLogout,
handleCallback,
@@ -101,34 +182,40 @@ export const _initAuth = (params?: ConfigParameters): SignInWithAuth0 & { sessio
};
};
-export const initAuth0: InitAuth0 = (params) => {
- const { sessionCache, ...publicApi } = _initAuth(params);
- return publicApi;
-};
-
+/* c8 ignore start */
const getSessionCache = () => getInstance().sessionCache;
export const getSession: GetSession = (...args) => getInstance().getSession(...args);
+export const updateSession: UpdateSession = (...args) => getInstance().updateSession(...args);
export const getAccessToken: GetAccessToken = (...args) => getInstance().getAccessToken(...args);
export const withApiAuthRequired: WithApiAuthRequired = (...args) => getInstance().withApiAuthRequired(...args);
export const withPageAuthRequired: WithPageAuthRequired = withPageAuthRequiredFactory(getLoginUrl(), getSessionCache);
-export const getServerSidePropsWrapper: GetServerSidePropsWrapper = getServerSidePropsWrapperFactory(getSessionCache);
-export const handleLogin: HandleLogin = (...args) => getInstance().handleLogin(...args);
-export const handleLogout: HandleLogout = (...args) => getInstance().handleLogout(...args);
-export const handleCallback: HandleCallback = (...args) => getInstance().handleCallback(...args);
-export const handleProfile: HandleProfile = (...args) => getInstance().handleProfile(...args);
+export const handleLogin: HandleLogin = ((...args: Parameters) =>
+ getInstance().handleLogin(...args)) as HandleLogin;
+export const handleLogout: HandleLogout = ((...args: Parameters) =>
+ getInstance().handleLogout(...args)) as HandleLogout;
+export const handleCallback: HandleCallback = ((...args: Parameters) =>
+ getInstance().handleCallback(...args)) as HandleCallback;
+export const handleProfile: HandleProfile = ((...args: Parameters) =>
+ getInstance().handleProfile(...args)) as HandleProfile;
export const handleAuth: HandleAuth = (...args) => getInstance().handleAuth(...args);
export {
- UserProvider,
- UserProviderProps,
- UserProfile,
- UserContext,
- RequestError,
- useUser,
- WithPageAuthRequiredProps
-} from './frontend';
+ AuthError,
+ AccessTokenErrorCode,
+ AccessTokenError,
+ HandlerError,
+ CallbackHandlerError,
+ LoginHandlerError,
+ LogoutHandlerError,
+ ProfileHandlerError
+} from './utils/errors';
-export { AccessTokenError, HandlerError } from './utils/errors';
+export {
+ MissingStateCookieError,
+ MissingStateParamError,
+ IdentityProviderError,
+ ApplicationError
+} from './auth0-session';
export {
ConfigParameters,
@@ -144,9 +231,9 @@ export {
PageRoute,
WithApiAuthRequired,
WithPageAuthRequired,
- GetServerSidePropsWrapper,
SessionCache,
GetSession,
+ UpdateSession,
GetAccessToken,
Session,
Claims,
@@ -157,5 +244,7 @@ export {
AfterRefetch,
LoginOptions,
LogoutOptions,
- GetLoginState
+ GetLoginState,
+ OnError
};
+/* c8 ignore stop */
diff --git a/src/instance.ts b/src/instance.ts
deleted file mode 100644
index 462768bfd..000000000
--- a/src/instance.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { GetSession, GetAccessToken } from './session';
-import { GetServerSidePropsWrapper, WithApiAuthRequired, WithPageAuthRequired } from './helpers';
-import { HandleAuth, HandleCallback, HandleLogin, HandleLogout, HandleProfile } from './handlers';
-import { ConfigParameters } from './auth0-session';
-
-/**
- * The SDK server instance.
- *
- * This is created for you when you use the named exports, or you can create your own using {@link InitAuth0}
- *
- * See {@link Config} fro more info.
- *
- * @category Server
- */
-export interface SignInWithAuth0 {
- /**
- * Session getter
- */
- getSession: GetSession;
-
- /**
- * Access Token getter
- */
- getAccessToken: GetAccessToken;
-
- /**
- * Login handler which will redirect the user to Auth0.
- */
- handleLogin: HandleLogin;
-
- /**
- * Callback handler which will complete the transaction and create a local session.
- */
- handleCallback: HandleCallback;
-
- /**
- * Logout handler which will clear the local session and the Auth0 session.
- */
- handleLogout: HandleLogout;
-
- /**
- * Profile handler which return profile information about the user.
- */
- handleProfile: HandleProfile;
-
- /**
- * Helper that adds auth to an API Route
- */
- withApiAuthRequired: WithApiAuthRequired;
-
- /**
- * Helper that adds auth to a Page Route
- */
- withPageAuthRequired: WithPageAuthRequired;
-
- /**
- * Wrap `getServerSideProps` to avoid accessing `res` after getServerSideProps resolves,
- * see {@link GetServerSidePropsWrapper}
- */
- getServerSidePropsWrapper: GetServerSidePropsWrapper;
-
- /**
- * Create the main handlers for your api routes
- */
- handleAuth: HandleAuth;
-}
-
-/**
- * Initialise your own instance of the SDK.
- *
- * See {@link Config}
- *
- * @category Server
- */
-export type InitAuth0 = (params?: ConfigParameters) => SignInWithAuth0;
diff --git a/src/session/cache.ts b/src/session/cache.ts
index b8217987c..291a81d88 100644
--- a/src/session/cache.ts
+++ b/src/session/cache.ts
@@ -1,66 +1,68 @@
import { IncomingMessage, ServerResponse } from 'http';
import { NextApiRequest, NextApiResponse } from 'next';
-import { TokenSet } from 'openid-client';
-import onHeaders from 'on-headers';
+import type { TokenSet } from 'openid-client';
import { Config, SessionCache as ISessionCache, CookieStore } from '../auth0-session';
import Session, { fromJson, fromTokenSet } from './session';
-type NextApiOrPageRequest = IncomingMessage | NextApiRequest;
-type NextApiOrPageResponse = ServerResponse | NextApiResponse;
+export default class SessionCache<
+ Req extends object = IncomingMessage | NextApiRequest, // eslint-disable-line @typescript-eslint/ban-types
+ Res extends object = ServerResponse | NextApiResponse // eslint-disable-line @typescript-eslint/ban-types
+> implements ISessionCache
+{
+ private cache: WeakMap;
+ private iatCache: WeakMap;
-export default class SessionCache implements ISessionCache {
- private cache: WeakMap;
- private iatCache: WeakMap;
-
- constructor(private config: Config, private cookieStore: CookieStore) {
+ constructor(private config: Config, private cookieStore: CookieStore) {
this.cache = new WeakMap();
this.iatCache = new WeakMap();
}
- init(req: NextApiOrPageRequest, res: NextApiOrPageResponse, autoSave = true): void {
+ private async init(req: Req, res: Res, autoSave = true): Promise {
if (!this.cache.has(req)) {
- const [json, iat] = this.cookieStore.read(req);
+ const [json, iat] = await this.cookieStore.read(req);
this.iatCache.set(req, iat);
this.cache.set(req, fromJson(json));
- if (autoSave) {
- onHeaders(res, () => this.save(req, res));
+ if (this.config.session.rolling && autoSave) {
+ await this.save(req, res);
}
}
}
- save(req: NextApiOrPageRequest, res: NextApiOrPageResponse): void {
- this.cookieStore.save(req, res, this.cache.get(req), this.iatCache.get(req));
+ async save(req: Req, res: Res): Promise {
+ await this.cookieStore.save(req, res, this.cache.get(req), this.iatCache.get(req));
}
- create(req: NextApiOrPageRequest, res: NextApiOrPageResponse, session: Session): void {
+ async create(req: Req, res: Res, session: Session): Promise {
this.cache.set(req, session);
- onHeaders(res, () => this.save(req, res));
+ await this.save(req, res);
}
- delete(req: NextApiOrPageRequest, res: NextApiOrPageResponse): void {
- this.init(req, res);
+ async delete(req: Req, res: Res): Promise {
+ await this.init(req, res, false);
this.cache.set(req, null);
+ await this.save(req, res);
}
- isAuthenticated(req: NextApiOrPageRequest, res: NextApiOrPageResponse): boolean {
- this.init(req, res);
+ async isAuthenticated(req: Req, res: Res): Promise {
+ await this.init(req, res);
const session = this.cache.get(req);
return !!session?.user;
}
- getIdToken(req: NextApiOrPageRequest, res: NextApiOrPageResponse): string | undefined {
- this.init(req, res);
+ async getIdToken(req: Req, res: Res): Promise {
+ await this.init(req, res);
const session = this.cache.get(req);
return session?.idToken;
}
- set(req: NextApiOrPageRequest, res: NextApiOrPageResponse, session: Session | null): void {
- this.init(req, res);
+ async set(req: Req, res: Res, session: Session | null): Promise {
+ await this.init(req, res, false);
this.cache.set(req, session);
+ await this.save(req, res);
}
- get(req: NextApiOrPageRequest, res: NextApiOrPageResponse): Session | null | undefined {
- this.init(req, res);
+ async get(req: Req, res: Res): Promise {
+ await this.init(req, res);
return this.cache.get(req);
}
diff --git a/src/session/get-access-token.ts b/src/session/get-access-token.ts
index 5697be959..cf99fe83c 100644
--- a/src/session/get-access-token.ts
+++ b/src/session/get-access-token.ts
@@ -1,7 +1,8 @@
import { IncomingMessage, ServerResponse } from 'http';
import { NextApiRequest, NextApiResponse } from 'next';
-import { ClientFactory } from '../auth0-session';
-import { AccessTokenError } from '../utils/errors';
+import type { errors } from 'openid-client';
+import { ClientFactory, IdentityProviderError } from '../auth0-session';
+import { AccessTokenError, AccessTokenErrorCode } from '../utils/errors';
import { intersect, match } from '../utils/array';
import { Session, SessionCache, fromTokenSet } from '../session';
import { AuthorizationParameters, NextConfig } from '../config';
@@ -9,29 +10,33 @@ import { AuthorizationParameters, NextConfig } from '../config';
export type AfterRefresh = (req: NextApiRequest, res: NextApiResponse, session: Session) => Promise | Session;
/**
- * Custom options to get an Access Token.
+ * Custom options to get an access token.
*
* @category Server
*/
export interface AccessTokenRequest {
/**
- * A list of desired scopes for your Access Token.
+ * A list of desired scopes for your access token.
*/
scopes?: string[];
/**
- * If set to `true`, a new Access Token will be requested with the Refresh Token grant, regardless of whether
- * the Access Token has expired or not.
+ * If set to `true`, a new access token will be requested with the refresh token grant, regardless of whether
+ * the access token has expired or not.
+ *
+ * **IMPORTANT** You need to request the `offline_access` scope on login to get a refresh token
+ * from Auth0.
*/
refresh?: boolean;
/**
- * When the Access Token Request refreshes the tokens using the Refresh Grant the Session is updated with new tokens.
+ * When the access token request refreshes the tokens using the refresh grant the session is updated with new tokens.
* Use this to modify the session after it is refreshed.
- * Usually used to keep updates in sync with the {@Link AfterCallback} hook.
- * See also the {@Link AfterRefetch} hook
+ * Usually used to keep updates in sync with the {@link AfterCallback} hook.
+ *
+ * @see also the {@link AfterRefetch} hook.
*
- * ### Modify the session after refresh
+ * @example Modify the session after refresh
*
* ```js
* // pages/api/my-handler.js
@@ -60,7 +65,7 @@ export interface AccessTokenRequest {
}
/**
- * Response from requesting an Access Token.
+ * Response from requesting an access token.
*
* @category Server
*/
@@ -72,9 +77,9 @@ export interface GetAccessTokenResult {
}
/**
- * Get an Access Token to access an external API.
+ * Get an access token to access an external API.
*
- * @throws {@Link AccessTokenError}
+ * @throws {@link AccessTokenError}
*
* @category Server
*/
@@ -93,18 +98,21 @@ export default function accessTokenFactory(
sessionCache: SessionCache
): GetAccessToken {
return async (req, res, accessTokenRequest): Promise => {
- let session = sessionCache.get(req, res);
+ let session = await sessionCache.get(req, res);
if (!session) {
- throw new AccessTokenError('invalid_session', 'The user does not have a valid session.');
+ throw new AccessTokenError(AccessTokenErrorCode.MISSING_SESSION, 'The user does not have a valid session.');
}
if (!session.accessToken && !session.refreshToken) {
- throw new AccessTokenError('invalid_session', 'The user does not have a valid access token.');
+ throw new AccessTokenError(
+ AccessTokenErrorCode.MISSING_ACCESS_TOKEN,
+ 'The user does not have a valid access token.'
+ );
}
if (!session.accessTokenExpiresAt) {
throw new AccessTokenError(
- 'access_token_expired',
+ AccessTokenErrorCode.EXPIRED_ACCESS_TOKEN,
'Expiration information for the access token is not available. The user will need to sign in again.'
);
}
@@ -113,7 +121,7 @@ export default function accessTokenFactory(
const persistedScopes = session.accessTokenScope;
if (!persistedScopes || persistedScopes.length === 0) {
throw new AccessTokenError(
- 'insufficient_scope',
+ AccessTokenErrorCode.INSUFFICIENT_SCOPE,
'An access token with the requested scopes could not be provided. The user will need to sign in again.'
);
}
@@ -121,7 +129,7 @@ export default function accessTokenFactory(
const matchingScopes = intersect(accessTokenRequest.scopes, persistedScopes.split(' '));
if (!match(accessTokenRequest.scopes, [...matchingScopes])) {
throw new AccessTokenError(
- 'insufficient_scope',
+ AccessTokenErrorCode.INSUFFICIENT_SCOPE,
`Could not retrieve an access token with scopes "${accessTokenRequest.scopes.join(
' '
)}". The user will need to sign in again.`
@@ -134,14 +142,14 @@ export default function accessTokenFactory(
// Adding a skew of 1 minute to compensate.
if (!session.refreshToken && session.accessTokenExpiresAt * 1000 - 60000 < Date.now()) {
throw new AccessTokenError(
- 'access_token_expired',
+ AccessTokenErrorCode.EXPIRED_ACCESS_TOKEN,
'The access token expired and a refresh token is not available. The user will need to sign in again.'
);
}
if (accessTokenRequest?.refresh && !session.refreshToken) {
throw new AccessTokenError(
- 'no_refresh_token',
+ AccessTokenErrorCode.MISSING_REFRESH_TOKEN,
'A refresh token is required to refresh the access token, but none is present.'
);
}
@@ -154,9 +162,18 @@ export default function accessTokenFactory(
(session.refreshToken && accessTokenRequest && accessTokenRequest.refresh)
) {
const client = await getClient();
- const tokenSet = await client.refresh(session.refreshToken, {
- exchangeBody: accessTokenRequest?.authorizationParams
- });
+ let tokenSet;
+ try {
+ tokenSet = await client.refresh(session.refreshToken, {
+ exchangeBody: accessTokenRequest?.authorizationParams
+ });
+ } catch (e) {
+ throw new AccessTokenError(
+ AccessTokenErrorCode.FAILED_REFRESH_GRANT,
+ 'The request to refresh the access token failed.',
+ new IdentityProviderError(e as errors.OPError)
+ );
+ }
// Update the session.
const newSession = fromTokenSet(tokenSet, config);
@@ -170,7 +187,7 @@ export default function accessTokenFactory(
session = await accessTokenRequest.afterRefresh(req as NextApiRequest, res as NextApiResponse, session);
}
- sessionCache.set(req, res, session);
+ await sessionCache.set(req, res, session);
// Return the new access token.
return {
@@ -180,10 +197,13 @@ export default function accessTokenFactory(
// We don't have an access token.
if (!session.accessToken) {
- throw new AccessTokenError('invalid_session', 'The user does not have a valid access token.');
+ throw new AccessTokenError(
+ AccessTokenErrorCode.MISSING_ACCESS_TOKEN,
+ 'The user does not have a valid access token.'
+ );
}
- // The access token is not expired and has sufficient scopes;
+ // The access token is not expired and has sufficient scopes.
return {
accessToken: session.accessToken
};
diff --git a/src/session/get-session.ts b/src/session/get-session.ts
index 34f92982c..b813879ba 100644
--- a/src/session/get-session.ts
+++ b/src/session/get-session.ts
@@ -1,6 +1,7 @@
import { IncomingMessage, ServerResponse } from 'http';
import { NextApiRequest, NextApiResponse } from 'next';
import { SessionCache, Session } from '../session';
+import { assertReqRes } from '../utils/assert';
/**
* Get the user's session from the request.
@@ -10,13 +11,14 @@ import { SessionCache, Session } from '../session';
export type GetSession = (
req: IncomingMessage | NextApiRequest,
res: ServerResponse | NextApiResponse
-) => Session | null | undefined;
+) => Promise;
/**
* @ignore
*/
export default function sessionFactory(sessionCache: SessionCache): GetSession {
- return (req, res): Session | null | undefined => {
+ return (req, res) => {
+ assertReqRes(req, res);
return sessionCache.get(req, res);
};
}
diff --git a/src/session/index.ts b/src/session/index.ts
index 23e0ee002..7c8b2dc10 100644
--- a/src/session/index.ts
+++ b/src/session/index.ts
@@ -7,3 +7,4 @@ export {
GetAccessTokenResult
} from './get-access-token';
export { default as SessionCache } from './cache';
+export { default as updateSessionFactory, UpdateSession } from './update-session';
diff --git a/src/session/session.ts b/src/session/session.ts
index 6ad79f6dc..11deaa335 100644
--- a/src/session/session.ts
+++ b/src/session/session.ts
@@ -1,4 +1,4 @@
-import { TokenSet } from 'openid-client';
+import type { TokenSet } from 'openid-client';
import { Config } from '../auth0-session';
import { NextConfig } from '../config';
@@ -12,18 +12,18 @@ export interface Claims {
}
/**
- * The user's session
+ * The user's session.
*
* @category Server
*/
export default class Session {
/**
- * Any of the claims from the id_token.
+ * Any of the claims from the `id_token`.
*/
user: Claims;
/**
- * The id token.
+ * The ID token.
*/
idToken?: string | undefined;
@@ -43,7 +43,10 @@ export default class Session {
accessTokenExpiresAt?: number;
/**
- * The refresh token.
+ * The refresh token, which is used to request a new access token.
+ *
+ * **IMPORTANT** You need to request the `offline_access` scope on login to get a refresh token
+ * from Auth0.
*/
refreshToken?: string | undefined;
@@ -58,22 +61,23 @@ export default class Session {
* @ignore
*/
export function fromTokenSet(tokenSet: TokenSet, config: Config | NextConfig): Session {
- // Get the claims without any OIDC specific claim.
+ // Get the claims without any OIDC-specific claim.
const claims = tokenSet.claims();
config.identityClaimFilter.forEach((claim) => {
delete claims[claim];
});
const { id_token, access_token, scope, expires_at, refresh_token, ...remainder } = tokenSet;
+ const storeIDToken = 'session' in config ? config.session.storeIDToken : true;
return Object.assign(
new Session({ ...claims }),
{
- idToken: id_token,
accessToken: access_token,
accessTokenScope: scope,
accessTokenExpiresAt: expires_at,
- refreshToken: refresh_token
+ refreshToken: refresh_token,
+ ...(storeIDToken && { idToken: id_token })
},
remainder
);
diff --git a/src/session/update-session.ts b/src/session/update-session.ts
new file mode 100644
index 000000000..1138a9203
--- /dev/null
+++ b/src/session/update-session.ts
@@ -0,0 +1,45 @@
+import { IncomingMessage, ServerResponse } from 'http';
+import { NextApiRequest, NextApiResponse } from 'next';
+import { Session, SessionCache } from '../session';
+import { assertReqRes } from '../utils/assert';
+
+/**
+ * Update the session object. The provided `session` object will replace the existing session.
+ *
+ * **Note** you can't use this method to login or logout - you should use the login and logout handlers for this.
+ * If no session is provided, it doesn't contain a user or the user is not authenticated; this is a no-op.
+ *
+ * ```js
+ * // pages/api/update-user.js
+ * import { getSession, updateSession } from '@auth0/nextjs-auth0';
+ *
+ * export default async function updateSession(req, res) {
+ * if (req.method === 'PUT') {
+ * const session = getSession(req, res);
+ * updateSession(req, res, { ...session, user: { ...user, foo: req.query.foo } });
+ * res.json({ success: true });
+ * }
+ * };
+ * ```
+ *
+ * @category Server
+ */
+export type UpdateSession = (
+ req: IncomingMessage | NextApiRequest,
+ res: ServerResponse | NextApiResponse,
+ user: Session
+) => Promise;
+
+/**
+ * @ignore
+ */
+export default function updateSessionFactory(sessionCache: SessionCache): UpdateSession {
+ return async (req, res, newSession) => {
+ assertReqRes(req, res);
+ const session = await sessionCache.get(req, res);
+ if (!session || !newSession || !newSession.user) {
+ return;
+ }
+ await sessionCache.set(req, res, newSession);
+ };
+}
diff --git a/src/utils/assert.ts b/src/utils/assert.ts
index ae6114a00..a18848301 100644
--- a/src/utils/assert.ts
+++ b/src/utils/assert.ts
@@ -1,6 +1,6 @@
-import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from 'next';
+import { GetServerSidePropsContext } from 'next';
-export const assertReqRes = (req: NextApiRequest, res: NextApiResponse): void => {
+export const assertReqRes = (req: unknown, res: unknown): void => {
if (!req) {
throw new Error('Request is not available');
}
@@ -10,10 +10,5 @@ export const assertReqRes = (req: NextApiRequest, res: NextApiResponse): void =>
};
export const assertCtx = ({ req, res }: GetServerSidePropsContext): void => {
- if (!req) {
- throw new Error('Request is not available');
- }
- if (!res) {
- throw new Error('Response is not available');
- }
+ assertReqRes(req, res);
};
diff --git a/src/utils/errors.ts b/src/utils/errors.ts
index 870658a80..fda7b003b 100644
--- a/src/utils/errors.ts
+++ b/src/utils/errors.ts
@@ -1,68 +1,243 @@
import { HttpError } from 'http-errors';
/**
- * The error thrown by {@link GetAccessToken}
+ * @ignore
+ */
+export function appendCause(errorMessage: string, cause?: Error): string {
+ if (!cause) return errorMessage;
+ const separator = errorMessage.endsWith('.') ? '' : '.';
+ return `${errorMessage}${separator} CAUSE: ${cause.message}`;
+}
+
+type AuthErrorOptions = {
+ code: string;
+ message: string;
+ name: string;
+ cause?: Error;
+ status?: number;
+};
+
+/**
+ * The base class for all SDK errors.
+ *
+ * Because part of the error message can come from the OpenID Connect `error` query parameter we
+ * do some basic escaping which makes sure the default error handler is safe from XSS.
+ *
+ * **IMPORTANT** If you write your own error handler, you should **not** render the error
+ * without using a templating engine that will properly escape it for other HTML contexts first.
+ *
+ * Note that the error message of the {@link AuthError.cause | underlying error} is **not** escaped
+ * in any way, so do **not** render it without escaping it first!
*
* @category Server
*/
-export class AccessTokenError extends Error {
- public code: string;
+export abstract class AuthError extends Error {
+ /**
+ * A machine-readable error code that remains stable within a major version of the SDK. You
+ * should rely on this error code to handle errors. In contrast, the error message is not part of
+ * the API and can change anytime. Do **not** parse or otherwise rely on the error message to
+ * handle errors.
+ */
+ public readonly code: string;
+
+ /**
+ * The error class name.
+ */
+ public readonly name: string;
- /* istanbul ignore next */
- constructor(code: string, message: string) {
- super(message);
+ /**
+ * The underlying error, if any.
+ *
+ * **IMPORTANT** When this error is from the Identity Provider ({@Link IdentityProviderError}) it can contain user
+ * input and is only escaped using basic escaping for putting untrusted data directly into the HTML body.
+ *
+ * You should **not** render this error without using a templating engine that will properly escape it for other
+ * HTML contexts first.
+ */
+ public readonly cause?: Error;
- // Saving class name in the property of our custom error as a shortcut.
- this.name = this.constructor.name;
+ /**
+ * The HTTP status code, if any.
+ */
+ public readonly status?: number;
+
+ constructor(options: AuthErrorOptions) {
+ /* c8 ignore next */
+ super(appendCause(options.message, options.cause));
+ this.code = options.code;
+ this.name = options.name;
+ this.cause = options.cause;
+ this.status = options.status;
+ }
+}
+
+/**
+ * Error codes for {@link AccessTokenError}.
+ *
+ * @category Server
+ */
+export enum AccessTokenErrorCode {
+ MISSING_SESSION = 'ERR_MISSING_SESSION',
+ MISSING_ACCESS_TOKEN = 'ERR_MISSING_ACCESS_TOKEN',
+ MISSING_REFRESH_TOKEN = 'ERR_MISSING_REFRESH_TOKEN',
+ EXPIRED_ACCESS_TOKEN = 'ERR_EXPIRED_ACCESS_TOKEN',
+ INSUFFICIENT_SCOPE = 'ERR_INSUFFICIENT_SCOPE',
+ FAILED_REFRESH_GRANT = 'ERR_FAILED_REFRESH_GRANT'
+}
+
+/**
+ * The error thrown by {@link GetAccessToken}.
+ *
+ * @see the {@link AuthError.code | code property} contains a machine-readable error code that
+ * remains stable within a major version of the SDK. You should rely on this error code to handle
+ * errors. In contrast, the error message is not part of the API and can change anytime. Do **not**
+ * parse or otherwise rely on the error message to handle errors.
+ *
+ * @see {@link AccessTokenErrorCode} for the list of all possible error codes.
+ * @category Server
+ */
+export class AccessTokenError extends AuthError {
+ constructor(code: AccessTokenErrorCode, message: string, cause?: Error) {
+ /* c8 ignore next */
+ super({ code: code, message: message, name: 'AccessTokenError', cause });
// Capturing stack trace, excluding constructor call from it.
Error.captureStackTrace(this, this.constructor);
-
- // Machine readable code.
- this.code = code;
Object.setPrototypeOf(this, AccessTokenError.prototype);
}
}
-// eslint-disable-next-line max-len
-// Basic escaping for putting untrusted data directly into the HTML body, per: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-1-html-encode-before-inserting-untrusted-data-into-html-element-content
-export function htmlSafe(input: string): string {
- return input
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
+/**
+ * @ignore
+ */
+export type HandlerErrorCause = Error | AuthError | HttpError;
+
+type HandlerErrorOptions = {
+ code: string;
+ message: string;
+ name: string;
+ cause: HandlerErrorCause;
+};
+
+/**
+ * The base class for errors thrown by API route handlers. It extends {@link AuthError}.
+ *
+ * Because part of the error message can come from the OpenID Connect `error` query parameter we
+ * do some basic escaping which makes sure the default error handler is safe from XSS.
+ *
+ * **IMPORTANT** If you write your own error handler, you should **not** render the error message
+ * without using a templating engine that will properly escape it for other HTML contexts first.
+ *
+ * @see the {@link AuthError.cause | cause property} contains the underlying error.
+ * **IMPORTANT** When this error is from the Identity Provider ({@Link IdentityProviderError}) it can contain user
+ * input and is only escaped using basic escaping for putting untrusted data directly into the HTML body.
+ * You should **not** render this error without using a templating engine that will properly escape it for other
+ * HTML contexts first.
+ *
+ * @see the {@link AuthError.status | status property} contains the HTTP status code of the error,
+ * if any.
+ *
+ * @category Server
+ */
+export class HandlerError extends AuthError {
+ constructor(options: HandlerErrorOptions) {
+ let status: number | undefined;
+ if ('status' in options.cause) status = options.cause.status;
+ /* c8 ignore next */
+ super({ ...options, status });
+ }
}
/**
- * The error thrown by API route handlers.
+ * The error thrown by the callback API route handler. It extends {@link HandlerError}.
*
- * Because the error message can come from the OpenID Connect `error` query parameter we
+ * Because part of the error message can come from the OpenID Connect `error` query parameter we
* do some basic escaping which makes sure the default error handler is safe from XSS.
*
- * If you write your own error handler, you should **not** render the error message
+ * **IMPORTANT** If you write your own error handler, you should **not** render the error message
* without using a templating engine that will properly escape it for other HTML contexts first.
*
+ * @see the {@link AuthError.cause | cause property} contains the underlying error.
+ * **IMPORTANT** When this error is from the Identity Provider ({@Link IdentityProviderError}) it can contain user
+ * input and is only escaped using basic escaping for putting untrusted data directly into the HTML body.
+ * You should **not** render this error without using a templating engine that will properly escape it for other
+ * HTML contexts first.
+ *
+ * @see the {@link AuthError.status | status property} contains the HTTP status code of the error,
+ * if any.
+ *
+ * @category Server
+ */
+export class CallbackHandlerError extends HandlerError {
+ public static readonly code: string = 'ERR_CALLBACK_HANDLER_FAILURE';
+
+ constructor(cause: HandlerErrorCause) {
+ super({
+ code: CallbackHandlerError.code,
+ message: 'Callback handler failed.',
+ name: 'CallbackHandlerError',
+ cause
+ }); /* c8 ignore next */
+ Object.setPrototypeOf(this, CallbackHandlerError.prototype);
+ }
+}
+
+/**
+ * The error thrown by the login API route handler. It extends {@link HandlerError}.
+ *
+ * @see the {@link AuthError.cause | cause property} contains the underlying error.
* @category Server
*/
-export class HandlerError extends Error {
- public status: number | undefined;
- public code: string | undefined;
+export class LoginHandlerError extends HandlerError {
+ public static readonly code: string = 'ERR_LOGIN_HANDLER_FAILURE';
+
+ constructor(cause: HandlerErrorCause) {
+ super({
+ code: LoginHandlerError.code,
+ message: 'Login handler failed.',
+ name: 'LoginHandlerError',
+ cause
+ }); /* c8 ignore next */
+ Object.setPrototypeOf(this, LoginHandlerError.prototype);
+ }
+}
- /* istanbul ignore next */
- constructor(error: Error | AccessTokenError | HttpError) {
- super(htmlSafe(error.message));
+/**
+ * The error thrown by the logout API route handler. It extends {@link HandlerError}.
+ *
+ * @see the {@link AuthError.cause | cause property} contains the underlying error.
+ * @category Server
+ */
+export class LogoutHandlerError extends HandlerError {
+ public static readonly code: string = 'ERR_LOGOUT_HANDLER_FAILURE';
- this.name = error.name;
+ constructor(cause: HandlerErrorCause) {
+ super({
+ code: LogoutHandlerError.code,
+ message: 'Logout handler failed.',
+ name: 'LogoutHandlerError',
+ cause
+ }); /* c8 ignore next */
+ Object.setPrototypeOf(this, LogoutHandlerError.prototype);
+ }
+}
- if ('code' in error) {
- this.code = error.code;
- }
+/**
+ * The error thrown by the profile API route handler. It extends {@link HandlerError}.
+ *
+ * @see the {@link AuthError.cause | cause property} contains the underlying error.
+ * @category Server
+ */
+export class ProfileHandlerError extends HandlerError {
+ public static readonly code: string = 'ERR_PROFILE_HANDLER_FAILURE';
- if ('status' in error) {
- this.status = error.status;
- }
- Object.setPrototypeOf(this, HandlerError.prototype);
+ constructor(cause: HandlerErrorCause) {
+ super({
+ code: ProfileHandlerError.code,
+ message: 'Profile handler failed.',
+ name: 'ProfileHandlerError',
+ cause
+ }); /* c8 ignore next */
+ Object.setPrototypeOf(this, ProfileHandlerError.prototype);
}
}
diff --git a/src/utils/instance-check.ts b/src/utils/instance-check.ts
new file mode 100644
index 000000000..4786da05d
--- /dev/null
+++ b/src/utils/instance-check.ts
@@ -0,0 +1,21 @@
+let isUsingNamedExports = false;
+let isUsingOwnInstance = false;
+
+const instanceCheck = () => {
+ if (isUsingNamedExports && isUsingOwnInstance) {
+ throw new Error(
+ 'You cannot mix creating your own instance with `initAuth0` and using named ' +
+ "exports like `import { handleAuth } from '@auth0/nextjs-auth0'`"
+ );
+ }
+};
+
+export const setIsUsingNamedExports = (): void => {
+ isUsingNamedExports = true;
+ instanceCheck();
+};
+
+export const setIsUsingOwnInstance = (): void => {
+ isUsingOwnInstance = true;
+ instanceCheck();
+};
diff --git a/src/utils/middleware-cookies.ts b/src/utils/middleware-cookies.ts
new file mode 100644
index 000000000..d593bfdf3
--- /dev/null
+++ b/src/utils/middleware-cookies.ts
@@ -0,0 +1,26 @@
+import { Cookies } from '../auth0-session/utils/cookies';
+import { NextRequest, NextResponse } from 'next/server';
+
+export default class MiddlewareCookies extends Cookies {
+ protected getSetCookieHeader(res: NextResponse): string[] {
+ const value = res.headers.get('set-cookie');
+ return value?.split(', ') || [];
+ }
+
+ protected setSetCookieHeader(res: NextResponse, cookies: string[]): void {
+ res.headers.set('set-cookie', cookies.join(', '));
+ }
+
+ getAll(req: NextRequest): Record {
+ const { cookies } = req;
+ if (typeof cookies.getAll === 'function') {
+ return req.cookies.getAll().reduce((memo, { name, value }) => ({ ...memo, [name]: value }), {});
+ }
+ // Edge cookies before Next 13.0.1 have no `getAll` and extend `Map`.
+ const legacyCookies = cookies as unknown as Map;
+ return Array.from(legacyCookies.keys()).reduce((memo: { [key: string]: string }, key) => {
+ memo[key] = legacyCookies.get(key) as string;
+ return memo;
+ }, {});
+ }
+}
diff --git a/src/utils/url-helpers.ts b/src/utils/url-helpers.ts
index 32e5b4b0e..038655feb 100644
--- a/src/utils/url-helpers.ts
+++ b/src/utils/url-helpers.ts
@@ -1,5 +1,6 @@
/**
* Helper which tests if a URL can safely be redirected to. Requires the URL to be relative.
+ *
* @param dangerousRedirect
* @param safeBaseUrl
*/
diff --git a/src/version.ts b/src/version.ts
index 4bc622075..ff603d700 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -1 +1 @@
-export default '1.9.2';
+export default '2.0.0';
diff --git a/testing.d.ts b/testing.d.ts
new file mode 100644
index 000000000..8de841c21
--- /dev/null
+++ b/testing.d.ts
@@ -0,0 +1 @@
+export type * from './dist/helpers/testing';
diff --git a/testing.js b/testing.js
new file mode 100644
index 000000000..135710cec
--- /dev/null
+++ b/testing.js
@@ -0,0 +1 @@
+module.exports = require('./dist/helpers/testing');
diff --git a/tests/auth0-session/client.test.ts b/tests/auth0-session/client.test.ts
index e5b73c4aa..b35c85704 100644
--- a/tests/auth0-session/client.test.ts
+++ b/tests/auth0-session/client.test.ts
@@ -1,6 +1,6 @@
import nock from 'nock';
-import { Client, Issuer } from 'openid-client';
-import { getConfig, clientFactory, ConfigParameters } from '../../src/auth0-session';
+import { Client } from 'openid-client';
+import { getConfig, ConfigParameters } from '../../src/auth0-session';
import { jwks } from './fixtures/cert';
import pkg from '../../package.json';
import wellKnown from './fixtures/well-known.json';
@@ -17,8 +17,13 @@ const defaultConfig = {
}
};
-const getClient = (params: ConfigParameters = {}): Promise =>
- clientFactory(getConfig({ ...defaultConfig, ...params }), { name: 'nextjs-auth0', version })();
+const getClient = async (params: ConfigParameters = {}): Promise => {
+ const { default: clientFactory } = await import('../../src/auth0-session/client');
+ return clientFactory(getConfig({ ...defaultConfig, ...params }), {
+ name: 'nextjs-auth0',
+ version
+ })();
+};
describe('clientFactory', function () {
beforeEach(() => {
@@ -78,7 +83,7 @@ describe('clientFactory', function () {
Authorization: 'Bearer foo'
}
});
- const headerProps = Object.getOwnPropertyNames(JSON.parse(response.body.toString()));
+ const headerProps = Object.getOwnPropertyNames(JSON.parse((response.body as Buffer).toString()));
expect(headerProps).toContain('authorization');
});
@@ -136,15 +141,12 @@ describe('clientFactory', function () {
).resolves.not.toThrow();
});
- it('should not disclose stack trace in AggregateError message when discovery fails', async () => {
+ it('should throw DiscoveryError when discovery fails', async () => {
nock.cleanAll();
nock('https://op.example.com').get('/.well-known/oauth-authorization-server').reply(500);
nock('https://op.example.com').get('/.well-known/openid-configuration').reply(500);
- await expect(getClient()).rejects.toThrowError(new Error('expected 200 OK, got: 500 Internal Server Error'));
- });
-
- it('should not normalize individual errors from discovery', async () => {
- jest.spyOn(Issuer, 'discover').mockRejectedValue(new Error('foo'));
- await expect(getClient()).rejects.toThrowError(new Error('foo'));
+ await expect(getClient()).rejects.toThrow(
+ 'Discovery requests failing for https://op.example.com, expected 200 OK, got: 500 Internal Server Error'
+ );
});
});
diff --git a/tests/auth0-session/config.test.ts b/tests/auth0-session/config.test.ts
index 710dc57f0..35786426c 100644
--- a/tests/auth0-session/config.test.ts
+++ b/tests/auth0-session/config.test.ts
@@ -109,6 +109,7 @@ describe('Config', () => {
expect(config.session).toMatchObject({
rollingDuration: 86400,
name: 'appSession',
+ storeIDToken: true,
cookie: {
sameSite: 'lax',
httpOnly: true,
@@ -124,6 +125,7 @@ describe('Config', () => {
session: {
name: '__test_custom_session_name__',
rollingDuration: 1234567890,
+ storeIDToken: false,
cookie: {
domain: '__test_custom_domain__',
transient: true,
@@ -140,6 +142,7 @@ describe('Config', () => {
rollingDuration: 1234567890,
absoluteDuration: 604800,
rolling: true,
+ storeIDToken: false,
cookie: {
domain: '__test_custom_domain__',
transient: true,
@@ -229,7 +232,7 @@ describe('Config', () => {
...defaultConfig,
session: {
rolling: true,
- rollingDuration: (false as unknown) as undefined // testing invalid configuration
+ rollingDuration: false as unknown as undefined // testing invalid configuration
}
})
).toThrow('"session.rollingDuration" must be provided an integer value when "session.rolling" is true');
@@ -247,13 +250,24 @@ describe('Config', () => {
).toThrowError('"session.absoluteDuration" must be provided an integer value when "session.rolling" is false');
});
+ it('should fail when app session storeIDToken is not a boolean', function () {
+ expect(() =>
+ getConfig({
+ ...defaultConfig,
+ session: {
+ storeIDToken: '__invalid_store_id_token__' as unknown as boolean // testing invalid configuration
+ }
+ })
+ ).toThrowError('"session.storeIDToken" must be a boolean');
+ });
+
it('should fail when app session secret is invalid', function () {
expect(() =>
getConfig({
...defaultConfig,
- secret: ({ key: '__test_session_secret__' } as unknown) as string // testing invalid configuration
+ secret: { key: '__test_session_secret__' } as unknown as string // testing invalid configuration
})
- ).toThrow('"secret" must be one of [string, binary, array]');
+ ).toThrow('"secret" must be one of [string, array]');
});
it('should fail when app session cookie httpOnly is not a boolean', function () {
@@ -262,7 +276,7 @@ describe('Config', () => {
...defaultConfig,
session: {
cookie: {
- httpOnly: ('__invalid_httponly__' as unknown) as boolean // testing invalid configuration
+ httpOnly: '__invalid_httponly__' as unknown as boolean // testing invalid configuration
}
}
})
@@ -276,11 +290,11 @@ describe('Config', () => {
secret: '__test_session_secret__',
session: {
cookie: {
- secure: ('__invalid_secure__' as unknown) as boolean // testing invalid configuration
+ secure: '__invalid_secure__' as unknown as boolean // testing invalid configuration
}
}
})
- ).toThrowError('"session.cookie.secure" must be a boolean');
+ ).toThrowError('Cookies must be secure when base url is https.');
});
it('should fail when app session cookie sameSite is invalid', function () {
@@ -290,7 +304,7 @@ describe('Config', () => {
secret: '__test_session_secret__',
session: {
cookie: {
- sameSite: ('__invalid_samesite__' as unknown) as any // testing invalid configuration
+ sameSite: '__invalid_samesite__' as unknown as any // testing invalid configuration
}
}
})
@@ -304,7 +318,7 @@ describe('Config', () => {
secret: '__test_session_secret__',
session: {
cookie: {
- domain: (false as unknown) as string // testing invalid configuration
+ domain: false as unknown as string // testing invalid configuration
}
}
})
@@ -313,13 +327,13 @@ describe('Config', () => {
it("shouldn't allow a secret of less than 8 chars", () => {
expect(() => getConfig({ ...defaultConfig, secret: 'short' })).toThrowError(
- new TypeError('"secret" does not match any of the allowed types')
+ new TypeError('"secret" length must be at least 8 characters long')
);
expect(() => getConfig({ ...defaultConfig, secret: ['short', 'too'] })).toThrowError(
- new TypeError('"secret[0]" does not match any of the allowed types')
+ new TypeError('"secret[0]" length must be at least 8 characters long')
);
expect(() => getConfig({ ...defaultConfig, secret: Buffer.from('short').toString() })).toThrowError(
- new TypeError('"secret" does not match any of the allowed types')
+ new TypeError('"secret" length must be at least 8 characters long')
);
});
@@ -399,7 +413,7 @@ describe('Config', () => {
});
it('should not allow empty scope', () => {
- expect(() => validateAuthorizationParams({ scope: (null as unknown) as undefined })).toThrowError(
+ expect(() => validateAuthorizationParams({ scope: null as unknown as undefined })).toThrowError(
new TypeError('"authorizationParams.scope" must be a string')
);
expect(() => validateAuthorizationParams({ scope: '' })).toThrowError(
@@ -420,10 +434,10 @@ describe('Config', () => {
});
it('should not allow empty response_type', () => {
- expect(() => validateAuthorizationParams({ response_type: (null as unknown) as undefined })).toThrowError(
+ expect(() => validateAuthorizationParams({ response_type: null as unknown as undefined })).toThrowError(
new TypeError('"authorizationParams.response_type" must be one of [id_token, code id_token, code]')
);
- expect(() => validateAuthorizationParams({ response_type: ('' as unknown) as undefined })).toThrowError(
+ expect(() => validateAuthorizationParams({ response_type: '' as unknown as undefined })).toThrowError(
new TypeError('"authorizationParams.response_type" must be one of [id_token, code id_token, code]')
);
});
@@ -452,16 +466,16 @@ describe('Config', () => {
});
it('should not allow empty response_mode', () => {
- expect(() => validateAuthorizationParams({ response_mode: (null as unknown) as undefined })).toThrowError(
+ expect(() => validateAuthorizationParams({ response_mode: null as unknown as undefined })).toThrowError(
new TypeError('"authorizationParams.response_mode" must be [form_post]')
);
- expect(() => validateAuthorizationParams({ response_mode: ('' as unknown) as undefined })).toThrowError(
+ expect(() => validateAuthorizationParams({ response_mode: '' as unknown as undefined })).toThrowError(
new TypeError('"authorizationParams.response_mode" must be [form_post]')
);
expect(() =>
validateAuthorizationParams({
response_type: 'code',
- response_mode: ('' as unknown) as undefined
+ response_mode: '' as unknown as undefined
})
).toThrowError(new TypeError('"authorizationParams.response_mode" must be one of [query, form_post]'));
});
diff --git a/tests/auth0-session/cookie-store.test.ts b/tests/auth0-session/cookie-store.test.ts
index cba683d60..03020ba7d 100644
--- a/tests/auth0-session/cookie-store.test.ts
+++ b/tests/auth0-session/cookie-store.test.ts
@@ -1,5 +1,5 @@
import { randomBytes } from 'crypto';
-import { JWK, JWE } from 'jose';
+import * as jose from 'jose';
import { IdTokenClaims } from 'openid-client';
import { setup, teardown } from './fixtures/server';
import { defaultConfig, fromCookieJar, get, toCookieJar } from './fixtures/helpers';
@@ -8,28 +8,27 @@ import { makeIdToken } from './fixtures/cert';
const hr = 60 * 60 * 1000;
const day = 24 * hr;
-const key = JWK.asKey(deriveKey(defaultConfig.secret as string));
-const encrypted = (payload: Partial = { sub: '__test_sub__' }): string => {
+const encrypted = async (claims: Partial = { sub: '__test_sub__' }): Promise => {
+ const key = await deriveKey(defaultConfig.secret as string);
const epochNow = (Date.now() / 1000) | 0;
const weekInSeconds = 7 * 24 * 60 * 60;
- return JWE.encrypt(
- JSON.stringify({
- access_token: '__test_access_token__',
- token_type: 'Bearer',
- id_token: makeIdToken(payload),
- refresh_token: '__test_access_token__',
- expires_at: epochNow + weekInSeconds
- }),
- key,
- {
+ const payload = {
+ access_token: '__test_access_token__',
+ token_type: 'Bearer',
+ id_token: await makeIdToken(claims),
+ refresh_token: '__test_access_token__',
+ expires_at: epochNow + weekInSeconds
+ };
+ return new jose.EncryptJWT({ ...payload })
+ .setProtectedHeader({
alg: 'dir',
enc: 'A256GCM',
uat: epochNow,
iat: epochNow,
exp: epochNow + weekInSeconds
- }
- );
+ })
+ .encrypt(key);
};
describe('CookieStore', () => {
@@ -53,13 +52,13 @@ describe('CookieStore', () => {
it('should not error with JWEDecryptionFailed when using old secrets', async () => {
const baseURL = await setup({ ...defaultConfig, secret: ['__invalid_secret__', '__also_invalid__'] });
- const cookieJar = toCookieJar({ appSession: encrypted() }, baseURL);
+ const cookieJar = toCookieJar({ appSession: await encrypted() }, baseURL);
await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized');
});
it('should get an existing session', async () => {
const baseURL = await setup(defaultConfig);
- const appSession = encrypted();
+ const appSession = await encrypted();
const cookieJar = toCookieJar({ appSession }, baseURL);
const session = await get(baseURL, '/session', { cookieJar });
expect(session).toMatchObject({
@@ -82,7 +81,7 @@ describe('CookieStore', () => {
it('should chunk and accept chunked cookies over 4kb', async () => {
const baseURL = await setup(defaultConfig);
- const appSession = encrypted({
+ const appSession = await encrypted({
big_claim: randomBytes(2000).toString('base64')
});
expect(appSession.length).toBeGreaterThan(4000);
@@ -105,7 +104,7 @@ describe('CookieStore', () => {
const path =
'/some-really-really-really-really-really-really-really-really-really-really-really-really-really-long-path';
const baseURL = await setup({ ...defaultConfig, session: { cookie: { path } } });
- const appSession = encrypted({
+ const appSession = await encrypted({
big_claim: randomBytes(5000).toString('base64')
});
expect(appSession.length).toBeGreaterThan(4096);
@@ -117,12 +116,12 @@ describe('CookieStore', () => {
expect(cookies['appSession.0']).toHaveLength(4096);
expect(cookies['appSession.1']).toHaveLength(4096);
expect(cookies['appSession.2']).toHaveLength(4096);
- expect(cookies['appSession.3']).toHaveLength(1568);
+ expect(cookies['appSession.3'].length).toBeLessThan(4096);
});
it('should handle unordered chunked cookies', async () => {
const baseURL = await setup(defaultConfig);
- const appSession = encrypted({ sub: '__chunked_sub__' });
+ const appSession = await encrypted({ sub: '__chunked_sub__' });
const cookieJar = toCookieJar(
{
'appSession.2': appSession.slice(20),
@@ -150,7 +149,7 @@ describe('CookieStore', () => {
it('should clean up single cookie when switching to chunked', async () => {
const baseURL = await setup(defaultConfig);
- const appSession = encrypted({
+ const appSession = await encrypted({
big_claim: randomBytes(2000).toString('base64')
});
expect(appSession.length).toBeGreaterThan(4000);
@@ -164,7 +163,7 @@ describe('CookieStore', () => {
it('should clean up chunked cookies when switching to a single cookie', async () => {
const baseURL = await setup(defaultConfig);
- const appSession = encrypted({ sub: 'foo' });
+ const appSession = await encrypted({ sub: 'foo' });
const cookieJar = toCookieJar(
{
'appSession.0': appSession.slice(0, 100),
@@ -181,7 +180,7 @@ describe('CookieStore', () => {
it('should set the default cookie options on http', async () => {
const baseURL = await setup(defaultConfig);
- const appSession = encrypted();
+ const appSession = await encrypted();
const cookieJar = toCookieJar({ appSession }, baseURL);
await get(baseURL, '/session', { cookieJar });
const [cookie] = cookieJar.getCookiesSync(baseURL);
@@ -198,7 +197,7 @@ describe('CookieStore', () => {
it('should set custom cookie options on http', async () => {
const baseURL = await setup({ ...defaultConfig, session: { cookie: { httpOnly: false } } });
- const appSession = encrypted();
+ const appSession = await encrypted();
const cookieJar = toCookieJar({ appSession }, baseURL);
await get(baseURL, '/session', { cookieJar });
const [cookie] = cookieJar.getCookiesSync(baseURL);
@@ -209,7 +208,7 @@ describe('CookieStore', () => {
it('should set the default cookie options on https', async () => {
const baseURL = await setup(defaultConfig, { https: true });
- const appSession = encrypted();
+ const appSession = await encrypted();
const cookieJar = toCookieJar({ appSession }, baseURL);
await get(baseURL, '/session', { cookieJar });
const [cookie] = cookieJar.getCookiesSync(baseURL);
@@ -224,21 +223,9 @@ describe('CookieStore', () => {
});
});
- it('should set custom secure option on https', async () => {
- const baseURL = await setup({ ...defaultConfig, session: { cookie: { secure: false } } }, { https: true });
- const appSession = encrypted();
- const cookieJar = toCookieJar({ appSession }, baseURL);
- await get(baseURL, '/session', { cookieJar });
- const [cookie] = cookieJar.getCookiesSync(baseURL);
- expect(cookie).toMatchObject({
- sameSite: 'lax',
- secure: false
- });
- });
-
it('should set custom sameSite option on https', async () => {
const baseURL = await setup({ ...defaultConfig, session: { cookie: { sameSite: 'none' } } }, { https: true });
- const appSession = encrypted();
+ const appSession = await encrypted();
const cookieJar = toCookieJar({ appSession }, baseURL);
await get(baseURL, '/session', { cookieJar });
const [cookie] = cookieJar.getCookiesSync(baseURL);
@@ -250,7 +237,7 @@ describe('CookieStore', () => {
it('should use a custom cookie name', async () => {
const baseURL = await setup({ ...defaultConfig, session: { name: 'myCookie' } });
- const appSession = encrypted();
+ const appSession = await encrypted();
const cookieJar = toCookieJar({ myCookie: appSession }, baseURL);
await get(baseURL, '/session', { cookieJar });
const [cookie] = cookieJar.getCookiesSync(baseURL);
@@ -261,7 +248,7 @@ describe('CookieStore', () => {
it('should set an ephemeral cookie', async () => {
const baseURL = await setup({ ...defaultConfig, session: { cookie: { transient: true } } });
- const appSession = encrypted();
+ const appSession = await encrypted();
const cookieJar = toCookieJar({ appSession }, baseURL);
await get(baseURL, '/session', { cookieJar });
const [cookie] = cookieJar.getCookiesSync(baseURL);
@@ -274,12 +261,13 @@ describe('CookieStore', () => {
const clock = jest.useFakeTimers('modern');
const baseURL = await setup(defaultConfig);
- const appSession = encrypted();
+ const appSession = await encrypted();
const cookieJar = toCookieJar({ appSession }, baseURL);
await expect(get(baseURL, '/session', { cookieJar })).resolves.not.toThrow();
jest.advanceTimersByTime(25 * hr);
await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized');
clock.restoreAllMocks();
+ jest.useRealTimers();
});
it('should expire after 7 days regardless of activity by default', async () => {
@@ -287,7 +275,7 @@ describe('CookieStore', () => {
let days = 7;
const baseURL = await setup(defaultConfig);
- const appSession = encrypted();
+ const appSession = await encrypted();
const cookieJar = toCookieJar({ appSession }, baseURL);
while (days--) {
jest.advanceTimersByTime(23 * hr);
@@ -296,6 +284,7 @@ describe('CookieStore', () => {
jest.advanceTimersByTime(23 * hr);
await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized');
clock.restoreAllMocks();
+ jest.useRealTimers();
});
it('should expire only after custom absoluteDuration', async () => {
@@ -308,7 +297,7 @@ describe('CookieStore', () => {
absoluteDuration: (10 * day) / 1000
}
});
- const appSession = encrypted();
+ const appSession = await encrypted();
const cookieJar = toCookieJar({ appSession }, baseURL);
await expect(get(baseURL, '/session', { cookieJar })).resolves.not.toThrow();
jest.advanceTimersByTime(9 * day);
@@ -316,6 +305,7 @@ describe('CookieStore', () => {
jest.advanceTimersByTime(2 * day);
await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized');
clock.restoreAllMocks();
+ jest.useRealTimers();
});
it('should expire only after defined rollingDuration period of inactivty', async () => {
@@ -331,7 +321,7 @@ describe('CookieStore', () => {
}
}
});
- const appSession = encrypted();
+ const appSession = await encrypted();
const cookieJar = toCookieJar({ appSession }, baseURL);
let days = 30;
while (days--) {
@@ -341,5 +331,35 @@ describe('CookieStore', () => {
jest.advanceTimersByTime(25 * hr);
await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized');
clock.restoreAllMocks();
+ jest.useRealTimers();
+ });
+
+ it('should not logout v1 users', async () => {
+ // Cookie generated with v1 cookie store tests with v long absolute exp
+ const V1_COOKIE =
+ 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwidWF0IjoxNjYyNDY2OTc2LCJpYXQiOjE2NjI0NjY5NzYsImV4cCI6NDgxODIyNjk3Nn0..3H_btn3Vk4SQhA0v.1tA8Olxj_1QTXRJYgY3FUtq1it-PunBKn2YKiO5cMCyf4ARF6sry4jfkq36aavaUYTh6w9mvAQawhcduOTzWOWtSbvRMOIlrOZTzUNohuLakKZA6ME2EgdLx1sMhuhtRdA1qACSDqly4qPw9IcOo1IUsYRzhtyI8MaYncjLzRHpo1Lvq_F5vtg5PIDTkYVnrhRX-SPsx6jbCr0rXxU3Cp9X8YYt-tl5yW-TLBPAeBy0TR-iiYJWMNyTMPE00o2LqsC2NQN7AySNtaaURb_a0cSpkF2X1fAb_iAKw-bg1wTruKUulErXkwTKPzZW6L0sGtnWN4qTg8gfxnoZxxrf7s-x2xCzKefiR0_8qpdfo0zhtE-PTYCFZxTU46yIkGZbJgVaH-tavoe1G3YhKMLEau49KV29agjVlN6eB5beEK2H70BgbaSPM4rcOhfqVeB5dku9olKCppI4UAtahaQqwnQrf1vd0W-qbslN_KO84QaBf8YlzGDbnfOAgXobqNnMu_-BoEInODK4azk_d9BquukhEm0g47XNYZuVCmgqNLo3Nul15lmHzZPGQ2ITivG7Dfb7sCLrKM6omioUjVCs6K6TCp100ndxQZuKUXYF2JkQoJhEge6MBSZDMF0cwIZlg1w8ArbPKl16zdZl8MYqDR_Vtwx7feT8sOvqST5he7oXp0yH2SvcG0dQbJLgPrmOOfDZIbjae11mcoIKa5oVVf4O_h-yHSVYyky8zLX-r7QP_H8CwMi19SysQa7S0b5BDlc5fn4ndf75TR7Zgg7r8PxzQiQghYoJXMZgzDpsaq8i33z2KMrwiGZPiDTuLmeOoV2BKNAVpBpad86BN_d2K7wAmPGx5ysWTc4mxSTv0b1E4G5_ZGDF59wl1m4o1zCSgMqZ56VCqb5qksPPhjpWjnbLnLw_6R_i4aqAxwHkdHOzbbSAGfwpBQF8PRDlmkIlZRQ9QRoLuWVdc3lJfX_Xp_ZKY9j8rKtOOC8BGq2yAZDIv0ezJPwLYEgi8_zdfufyogTLPOs0tIcImJIMasba5MqpHzOcKCsjnptUt2OF83Vyinw.NLZleTxnLwai5TN2wvcXhg';
+ const baseURL = await setup({
+ ...defaultConfig,
+ session: { rolling: false, absoluteDuration: day * 365 * 100 }
+ });
+ const appSession = V1_COOKIE;
+ const cookieJar = toCookieJar({ appSession }, baseURL);
+ const session = await get(baseURL, '/session', { cookieJar });
+ expect(session).toMatchObject({
+ access_token: '__test_access_token__',
+ token_type: 'Bearer',
+ id_token: expect.any(String),
+ refresh_token: '__test_access_token__',
+ expires_at: expect.any(Number),
+ claims: {
+ nickname: '__test_nickname__',
+ sub: '__test_sub__',
+ iss: 'https://op.example.com/',
+ aud: '__test_client_id__',
+ iat: expect.any(Number),
+ exp: expect.any(Number),
+ nonce: '__test_nonce__'
+ }
+ });
});
});
diff --git a/tests/auth0-session/fixtures/cert.ts b/tests/auth0-session/fixtures/cert.ts
index 2820d7f77..0fd225698 100644
--- a/tests/auth0-session/fixtures/cert.ts
+++ b/tests/auth0-session/fixtures/cert.ts
@@ -1,13 +1,19 @@
-import { JWK, JWKS, JWT } from 'jose';
+import * as jose from 'jose';
import { IdTokenClaims } from 'openid-client';
-const k = JWK.asKey({
+const publicKey = {
e: 'AQAB',
n:
'wQrThQ9HKf8ksCQEzqOu0ofF8DtLJgexeFSQBNnMQetACzt4TbHPpjhTWUIlD8bFCkyx88d2_QV3TewMtfS649Pn5hV6adeYW2TxweAA8HVJxskc' +
'qTSa_ktojQ-cD43HIStsbqJhHoFv0UY6z5pwJrVPT-yt38ciKo9Oc9IhEl6TSw-zAnuNW0zPOhKjuiIqpAk1lT3e6cYv83ahx82vpx3ZnV83dT9u' +
'RbIbcgIpK4W64YnYb5uDH7hGI8-4GnalZDfdApTu-9Y8lg_1v5ul-eQDsLCkUCPkqBaNiCG3gfZUAKp9rrFRE_cJTv_MJn-y_XSTMWILvTY7vdSM' +
'RMo4kQ',
+ kty: 'RSA',
+ use: 'sig',
+ alg: 'RS256'
+};
+
+const privateKey = {
d:
'EMHY1K8b1VhxndyykiGBVoM0uoLbJiT60eA9VD53za0XNSJncg8iYGJ5UcE9KF5v0lIQDIJfIN2tmpUIEW96HbbSZZWtt6xgbGaZ2eOREU6NJfVl' +
'SIbpgXOYUs5tFKiRBZ8YXY448gX4Z-k5x7W3UJTimqSH_2nw3FLuU32FI2vtf4ToUKEcoUdrIqoAwZ1et19E7Q_NCG2y1nez0LpD8PKgfeX1OVHd' +
@@ -28,17 +34,12 @@ const k = JWK.asKey({
qi:
'8hAW25CmPjLAXpzkMpXpXsvJKdgql0Zjt-OeSVwzQN5dLYmu-Q98Xl5n8H-Nfr8aOmPfHBQ8M9FOMpxbgg8gbqixpkrxcTIGjpuH8RFYXj_0TYSB' +
'kCSOoc7tAP7YjOUOGJMqFHDYZVD-gmsCuRwWx3jKFxRrWLS5b8kWzkON0bM',
- kty: 'RSA',
- use: 'sig',
- alg: 'RS256'
-});
-
-export const jwks = new JWKS.KeyStore([k]).toJWKS(false);
+ ...publicKey
+};
-export const key = k.toPEM(true);
-export const kid = k.kid;
+export const jwks = { keys: [publicKey] };
-export const makeIdToken = (payload?: Partial): string => {
+export const makeIdToken = async (payload?: Partial): Promise => {
payload = Object.assign(
{
nickname: '__test_nickname__',
@@ -52,8 +53,7 @@ export const makeIdToken = (payload?: Partial): string => {
payload
);
- return JWT.sign(payload, k.toPEM(true), {
- algorithm: 'RS256',
- header: { kid: k.kid }
- });
+ return new jose.SignJWT(payload as IdTokenClaims)
+ .setProtectedHeader({ alg: 'RS256' })
+ .sign(await jose.importJWK(privateKey));
};
diff --git a/tests/auth0-session/fixtures/helpers.ts b/tests/auth0-session/fixtures/helpers.ts
index c35e8e19f..41af9e3ad 100644
--- a/tests/auth0-session/fixtures/helpers.ts
+++ b/tests/auth0-session/fixtures/helpers.ts
@@ -1,5 +1,4 @@
import { Cookie, CookieJar } from 'tough-cookie';
-import { JWK } from 'jose';
import { signing as deriveKey } from '../../../src/auth0-session/utils/hkdf';
import { generateCookieValue } from '../../../src/auth0-session/transient-store';
import { IncomingMessage, request as nodeHttpRequest } from 'http';
@@ -17,11 +16,11 @@ export const defaultConfig: Omit = {
}
};
-export const toSignedCookieJar = (cookies: { [key: string]: string }, url: string): CookieJar => {
+export const toSignedCookieJar = async (cookies: { [key: string]: string }, url: string): Promise => {
const cookieJar = new CookieJar();
- const jwk = JWK.asKey(deriveKey(secret));
+ const signingKey = await deriveKey(secret);
for (const [key, value] of Object.entries(cookies)) {
- cookieJar.setCookieSync(`${key}=${generateCookieValue(key, value, jwk)}`, url);
+ cookieJar.setCookieSync(`${key}=${await generateCookieValue(key, value, signingKey)}`, url);
}
return cookieJar;
};
@@ -60,6 +59,9 @@ const request = (
rejectUnauthorized: false
},
(res) => {
+ if (cookieJar) {
+ (res.headers['set-cookie'] || []).forEach((cookie: string) => cookieJar.setCookieSync(cookie, url));
+ }
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 400)) {
return reject(new Error(res.statusMessage));
}
@@ -81,9 +83,6 @@ const request = (
resolve(data);
}
});
- if (cookieJar) {
- (res.headers['set-cookie'] || []).forEach((cookie: string) => cookieJar.setCookieSync(cookie, url));
- }
}
);
req.setHeader('content-type', 'application/json');
@@ -112,5 +111,5 @@ export const post = async (
cookieJar,
body,
fullResponse
- }: { body: { [key: string]: string }; cookieJar?: CookieJar; fullResponse?: boolean; https?: boolean }
+ }: { body: { [key: string]: any }; cookieJar?: CookieJar; fullResponse?: boolean; https?: boolean }
): Promise => request(`${baseURL}${path}`, 'POST', { body, cookieJar, fullResponse });
diff --git a/tests/auth0-session/fixtures/server.ts b/tests/auth0-session/fixtures/server.ts
index 665dffed8..fce41372b 100644
--- a/tests/auth0-session/fixtures/server.ts
+++ b/tests/auth0-session/fixtures/server.ts
@@ -4,9 +4,9 @@ import { createServer as createHttpsServer, Server as HttpsServer } from 'https'
import url from 'url';
import nock from 'nock';
import { TokenSet, TokenSetParameters } from 'openid-client';
-import onHeaders from 'on-headers';
import bodyParser from 'body-parser';
import {
+ NodeCookies as Cookies,
loginHandler,
getConfig,
ConfigParameters,
@@ -29,21 +29,20 @@ import version from '../../../src/version';
export type SessionResponse = TokenSetParameters & { claims: Claims };
class TestSessionCache implements SessionCache {
- public cache: WeakMap;
- constructor() {
- this.cache = new WeakMap();
+ constructor(private cookieStore: CookieStore) {}
+ async create(req: IncomingMessage, res: ServerResponse, tokenSet: TokenSet): Promise {
+ await this.cookieStore.save(req, res, tokenSet);
}
- create(req: IncomingMessage, _res: ServerResponse, tokenSet: TokenSet): void {
- this.cache.set(req, tokenSet);
+ async delete(req: IncomingMessage, res: ServerResponse): Promise {
+ await this.cookieStore.save(req, res, null);
}
- delete(req: IncomingMessage): void {
- this.cache.delete(req);
+ async isAuthenticated(req: IncomingMessage): Promise {
+ const [session] = await this.cookieStore.read(req);
+ return !!session?.id_token;
}
- isAuthenticated(req: IncomingMessage): boolean {
- return !!this.cache.get(req)?.id_token;
- }
- getIdToken(req: IncomingMessage): string | undefined {
- return this.cache.get(req)?.id_token;
+ async getIdToken(req: IncomingMessage): Promise {
+ const [session] = await this.cookieStore.read(req);
+ return session?.id_token;
}
fromTokenSet(tokenSet: TokenSet): { [p: string]: any } {
return tokenSet;
@@ -61,32 +60,25 @@ const createHandlers = (params: ConfigParameters): Handlers => {
const config = getConfig(params);
const getClient = clientFactory(config, { name: 'nextjs-auth0', version });
const transientStore = new TransientStore(config);
- const cookieStore = new CookieStore(config);
- const sessionCache = new TestSessionCache();
-
- const applyCookies = (fn: Function) => (req: IncomingMessage, res: ServerResponse, ...args: []): any => {
- if (!sessionCache.cache.has(req)) {
- const [json, iat] = cookieStore.read(req);
- sessionCache.cache.set(req, new TokenSet(json));
- onHeaders(res, () => cookieStore.save(req, res, sessionCache.cache.get(req), iat));
- }
- return fn(req, res, ...args);
- };
+ const cookieStore = new CookieStore(config, Cookies);
+ const sessionCache = new TestSessionCache(cookieStore);
return {
- handleLogin: applyCookies(loginHandler(config, getClient, transientStore)),
- handleLogout: applyCookies(logoutHandler(config, getClient, sessionCache)),
- handleCallback: applyCookies(callbackHandler(config, getClient, sessionCache, transientStore)),
- handleSession: applyCookies((req: IncomingMessage, res: ServerResponse) => {
- if (!sessionCache.isAuthenticated(req)) {
+ handleLogin: loginHandler(config, getClient, transientStore),
+ handleLogout: logoutHandler(config, getClient, sessionCache),
+ handleCallback: callbackHandler(config, getClient, sessionCache, transientStore),
+ handleSession: async (req: IncomingMessage, res: ServerResponse) => {
+ const [json, iat] = await cookieStore.read(req);
+ if (!json?.id_token) {
res.writeHead(401);
res.end();
return;
}
- const session = sessionCache.cache.get(req);
+ const session = new TokenSet(json);
+ await cookieStore.save(req, res, session, iat);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ ...session, claims: session?.claims() } as SessionResponse));
- })
+ }
};
};
@@ -102,36 +94,38 @@ const parseJson = (req: IncomingMessage, res: ServerResponse): Promise async (req: IncomingMessage, res: ServerResponse): Promise => {
- const { pathname } = url.parse(req.url as string, true);
- const parsedReq = await parseJson(req, res);
-
- try {
- switch (pathname) {
- case '/login':
- return await handlers.handleLogin(parsedReq, res, loginOptions);
- case '/logout':
- return await handlers.handleLogout(parsedReq, res, logoutOptions);
- case '/callback':
- return await handlers.handleCallback(parsedReq, res, callbackOptions);
- case '/session':
- return await handlers.handleSession(parsedReq, res);
- default:
- res.writeHead(404);
- res.end();
+const requestListener =
+ (
+ handlers: Handlers,
+ {
+ callbackOptions,
+ loginOptions,
+ logoutOptions
+ }: { callbackOptions?: CallbackOptions; loginOptions?: LoginOptions; logoutOptions?: LogoutOptions }
+ ) =>
+ async (req: IncomingMessage, res: ServerResponse): Promise => {
+ const { pathname } = url.parse(req.url as string, true);
+ const parsedReq = await parseJson(req, res);
+
+ try {
+ switch (pathname) {
+ case '/login':
+ return await handlers.handleLogin(parsedReq, res, loginOptions);
+ case '/logout':
+ return await handlers.handleLogout(parsedReq, res, logoutOptions);
+ case '/callback':
+ return await handlers.handleCallback(parsedReq, res, callbackOptions);
+ case '/session':
+ return await handlers.handleSession(parsedReq, res);
+ default:
+ res.writeHead(404);
+ res.end();
+ }
+ } catch (e) {
+ res.writeHead(e.statusCode || 500, e.message);
+ res.end();
}
- } catch (e) {
- res.writeHead(e.statusCode || 500, e.message);
- res.end();
- }
-};
+ };
let server: HttpServer | HttpsServer;
diff --git a/tests/auth0-session/handlers/callback.test.ts b/tests/auth0-session/handlers/callback.test.ts
index 894b10363..0baa252e6 100644
--- a/tests/auth0-session/handlers/callback.test.ts
+++ b/tests/auth0-session/handlers/callback.test.ts
@@ -1,10 +1,12 @@
import nock from 'nock';
import { CookieJar } from 'tough-cookie';
-import { JWT } from 'jose';
-import { encodeState } from '../../../src/auth0-session/hooks/get-login-state';
+import * as jose from 'jose';
+import { signing as deriveKey } from '../../../src/auth0-session/utils/hkdf';
+import { encodeState } from '../../../src/auth0-session/utils/encoding';
import { SessionResponse, setup, teardown } from '../fixtures/server';
import { makeIdToken } from '../fixtures/cert';
import { toSignedCookieJar, get, post, defaultConfig } from '../fixtures/helpers';
+import { ServerResponse } from 'http';
const expectedDefaultState = encodeState({ returnTo: 'https://example.org' });
@@ -14,7 +16,7 @@ describe('callback', () => {
it('should error when the body is empty', async () => {
const baseURL = await setup(defaultConfig);
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
nonce: '__test_nonce__',
state: '__test_state__'
@@ -22,8 +24,8 @@ describe('callback', () => {
baseURL
);
- await expect(post(baseURL, '/callback', { body: {}, cookieJar })).rejects.toThrowError(
- 'state missing from the response'
+ await expect(post(baseURL, '/callback', { body: {}, cookieJar })).rejects.toThrow(
+ 'Missing state parameter in Authorization Response.'
);
});
@@ -38,13 +40,15 @@ describe('callback', () => {
},
cookieJar: new CookieJar()
})
- ).rejects.toThrowError('checks.state argument is missing');
+ ).rejects.toThrowError(
+ 'Missing state cookie from login request (check login URL, callback URL and cookie config).'
+ );
});
it("should error when state doesn't match", async () => {
const baseURL = await setup(defaultConfig);
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
nonce: '__valid_nonce__',
state: '__valid_state__'
@@ -66,7 +70,7 @@ describe('callback', () => {
it("should error when id_token can't be parsed", async () => {
const baseURL = await setup(defaultConfig);
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
nonce: '__valid_nonce__',
state: '__valid_state__'
@@ -82,13 +86,13 @@ describe('callback', () => {
},
cookieJar
})
- ).rejects.toThrowError('failed to decode JWT (JWTMalformed: JWTs must have three components)');
+ ).rejects.toThrowError('failed to decode JWT (Error: JWTs must have three components)');
});
it('should error when id_token has invalid alg', async () => {
const baseURL = await setup(defaultConfig);
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
nonce: '__valid_nonce__',
state: '__valid_state__'
@@ -100,9 +104,9 @@ describe('callback', () => {
post(baseURL, '/callback', {
body: {
state: '__valid_state__',
- id_token: JWT.sign({ sub: '__test_sub__' }, 'secret', {
- algorithm: 'HS256'
- })
+ id_token: await new jose.SignJWT({ sub: '__test_sub__' })
+ .setProtectedHeader({ alg: 'HS256' })
+ .sign(await deriveKey('secret'))
},
cookieJar
})
@@ -112,7 +116,7 @@ describe('callback', () => {
it('should error when id_token is missing issuer', async () => {
const baseURL = await setup(defaultConfig);
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
nonce: '__valid_nonce__',
state: '__valid_state__'
@@ -124,7 +128,7 @@ describe('callback', () => {
post(baseURL, '/callback', {
body: {
state: '__valid_state__',
- id_token: makeIdToken({ iss: undefined })
+ id_token: await makeIdToken({ iss: undefined })
},
cookieJar
})
@@ -134,7 +138,7 @@ describe('callback', () => {
it('should error when nonce is missing from cookies', async () => {
const baseURL = await setup(defaultConfig);
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state: '__valid_state__'
},
@@ -145,7 +149,7 @@ describe('callback', () => {
post(baseURL, '/callback', {
body: {
state: '__valid_state__',
- id_token: makeIdToken({ nonce: '__test_nonce__' })
+ id_token: await makeIdToken({ nonce: '__test_nonce__' })
},
cookieJar
})
@@ -155,7 +159,7 @@ describe('callback', () => {
it('should error when legacy samesite fallback is off', async () => {
const baseURL = await setup({ ...defaultConfig, legacySameSiteCookie: false });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
_state: '__valid_state__'
},
@@ -166,14 +170,16 @@ describe('callback', () => {
post(baseURL, '/callback', {
body: {
state: '__valid_state__',
- id_token: makeIdToken()
+ id_token: await makeIdToken()
},
cookieJar
})
- ).rejects.toThrowError('checks.state argument is missing');
+ ).rejects.toThrowError(
+ 'Missing state cookie from login request (check login URL, callback URL and cookie config).'
+ );
});
- it('should error for expired ID Token', async () => {
+ it('should error for expired ID token', async () => {
const baseURL = await setup({ ...defaultConfig, legacySameSiteCookie: false });
const expected = {
@@ -185,7 +191,7 @@ describe('callback', () => {
auth_time: 10
};
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state: expectedDefaultState,
nonce: '__test_nonce__',
@@ -198,7 +204,7 @@ describe('callback', () => {
post(baseURL, '/callback', {
body: {
state: expectedDefaultState,
- id_token: makeIdToken(expected)
+ id_token: await makeIdToken(expected)
},
cookieJar
})
@@ -216,7 +222,7 @@ describe('callback', () => {
nonce: '__test_nonce__'
};
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state: expectedDefaultState,
nonce: '__test_nonce__'
@@ -227,7 +233,7 @@ describe('callback', () => {
const { res } = await post(baseURL, '/callback', {
body: {
state: expectedDefaultState,
- id_token: makeIdToken(expected)
+ id_token: await makeIdToken(expected)
},
cookieJar,
fullResponse: true
@@ -239,6 +245,30 @@ describe('callback', () => {
expect(session.claims).toEqual(expect.objectContaining(expected));
});
+ it("should fail when the Authorization Response params don't match the response_type", async () => {
+ const baseURL = await setup({ ...defaultConfig, authorizationParams: { response_type: 'id_token' } });
+
+ const cookieJar = await toSignedCookieJar(
+ {
+ state: expectedDefaultState,
+ nonce: '__test_nonce__',
+ response_type: 'code id_token'
+ },
+ baseURL
+ );
+
+ await expect(
+ post(baseURL, '/callback', {
+ body: {
+ state: expectedDefaultState,
+ id_token: await makeIdToken()
+ },
+ cookieJar,
+ fullResponse: true
+ })
+ ).rejects.toThrowError('code missing from response');
+ });
+
it("should expose all tokens when id_token is valid and response_type is 'code id_token'", async () => {
const baseURL = await setup({
...defaultConfig,
@@ -250,7 +280,7 @@ describe('callback', () => {
}
});
- const idToken = makeIdToken({
+ const idToken = await makeIdToken({
c_hash: '77QmUPtjPfzWtF2AnpK9RQ'
});
@@ -264,7 +294,7 @@ describe('callback', () => {
expires_in: 86400
}));
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state: expectedDefaultState,
nonce: '__test_nonce__'
@@ -294,7 +324,7 @@ describe('callback', () => {
});
it('should use basic auth on token endpoint when using code flow', async () => {
- const idToken = makeIdToken({
+ const idToken = await makeIdToken({
c_hash: '77QmUPtjPfzWtF2AnpK9RQ'
});
@@ -324,7 +354,7 @@ describe('callback', () => {
};
});
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state: expectedDefaultState,
nonce: '__test_nonce__'
@@ -352,7 +382,7 @@ describe('callback', () => {
const baseURL = await setup(defaultConfig);
const state = encodeState({ foo: 'bar' });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state: state,
nonce: '__test_nonce__'
@@ -363,7 +393,7 @@ describe('callback', () => {
const { res } = await post(baseURL, '/callback', {
body: {
state: state,
- id_token: makeIdToken()
+ id_token: await makeIdToken()
},
cookieJar,
fullResponse: true
@@ -377,11 +407,11 @@ describe('callback', () => {
const redirectUri = 'http://messi:3000/api/auth/callback/runtime';
const baseURL = await setup(defaultConfig, { callbackOptions: { redirectUri } });
const state = encodeState({ foo: 'bar' });
- const cookieJar = toSignedCookieJar({ state, nonce: '__test_nonce__' }, baseURL);
+ const cookieJar = await toSignedCookieJar({ state, nonce: '__test_nonce__' }, baseURL);
const { res } = await post(baseURL, '/callback', {
body: {
state: state,
- id_token: makeIdToken()
+ id_token: await makeIdToken()
},
cookieJar,
fullResponse: true
@@ -390,4 +420,146 @@ describe('callback', () => {
expect(res.statusCode).toEqual(302);
expect(res.headers.location).toEqual(baseURL);
});
+
+ it('should not overwrite location header if set in after callback', async () => {
+ const baseURL = await setup(defaultConfig, {
+ callbackOptions: {
+ afterCallback(_req, res: ServerResponse, session) {
+ res.setHeader('Location', '/foo');
+ return session;
+ }
+ }
+ });
+
+ const state = encodeState({ foo: 'bar' });
+ const cookieJar = await toSignedCookieJar(
+ {
+ state: state,
+ nonce: '__test_nonce__'
+ },
+ baseURL
+ );
+
+ const { res } = await post(baseURL, '/callback', {
+ body: {
+ state: state,
+ id_token: await makeIdToken()
+ },
+ cookieJar,
+ fullResponse: true
+ });
+
+ expect(res.statusCode).toEqual(302);
+ expect(res.headers.location).toEqual('/foo');
+ expect(cookieJar.getCookieStringSync(baseURL)).toMatch(/^appSession=.*/);
+ });
+
+ it('should terminate the request in after callback and not set session if none returned', async () => {
+ const baseURL = await setup(defaultConfig, {
+ callbackOptions: {
+ afterCallback(_req, res: ServerResponse) {
+ res.writeHead(401).end();
+ }
+ }
+ });
+
+ const state = encodeState({ foo: 'bar' });
+ const cookieJar = await toSignedCookieJar(
+ {
+ state: state,
+ nonce: '__test_nonce__'
+ },
+ baseURL
+ );
+
+ await expect(
+ post(baseURL, '/callback', {
+ body: {
+ state: state,
+ id_token: await makeIdToken()
+ },
+ cookieJar,
+ fullResponse: true
+ })
+ ).rejects.toThrow('Unauthorized');
+ expect(cookieJar.getCookieStringSync(baseURL)).toBeFalsy();
+ });
+
+ it('should escape Identity Provider error', async () => {
+ const baseURL = await setup(defaultConfig);
+
+ const cookieJar = await toSignedCookieJar(
+ {
+ state: expectedDefaultState,
+ nonce: '__test_nonce__',
+ response_type: 'code id_token'
+ },
+ baseURL
+ );
+
+ await expect(
+ post(baseURL, '/callback', {
+ body: {
+ state: expectedDefaultState,
+ error: '',
+ error_description: ''
+ },
+ cookieJar,
+ fullResponse: true
+ })
+ ).rejects.toThrowError('<script>alert(1)</script> (<script>alert(2)</script>)');
+ });
+
+ it('should escape application error', async () => {
+ const baseURL = await setup(defaultConfig);
+
+ const cookieJar = await toSignedCookieJar(
+ {
+ state: expectedDefaultState,
+ nonce: '__test_nonce__',
+ response_type: 'code id_token'
+ },
+ baseURL
+ );
+
+ await expect(
+ post(baseURL, '/callback', {
+ body: {
+ state: '',
+ id_token: await makeIdToken()
+ },
+ cookieJar,
+ fullResponse: true
+ })
+ ).rejects.toThrowError(
+ `state mismatch, expected ${expectedDefaultState}, got: <script>alert(1)</script>`
+ );
+ });
+
+ it('should handle discovery error', async () => {
+ const baseURL = await setup({ ...defaultConfig, issuerBaseURL: 'https://op2.example.com' });
+ nock('https://op2.example.com').get('/.well-known/openid-configuration').reply(500);
+
+ const cookieJar = await toSignedCookieJar(
+ {
+ state: expectedDefaultState,
+ nonce: '__test_nonce__',
+ response_type: 'code id_token'
+ },
+ baseURL
+ );
+
+ await expect(
+ post(baseURL, '/callback', {
+ body: {
+ state: expectedDefaultState,
+ id_token: await makeIdToken()
+ },
+ cookieJar,
+ fullResponse: true
+ })
+ ).rejects.toThrowError(
+ 'Discovery requests failing for https://op2.example.com, expected 200 OK, got: 500 Internal Server Error'
+ );
+ });
});
diff --git a/tests/auth0-session/handlers/login.test.ts b/tests/auth0-session/handlers/login.test.ts
index 6d2ed956b..dd13ad555 100644
--- a/tests/auth0-session/handlers/login.test.ts
+++ b/tests/auth0-session/handlers/login.test.ts
@@ -2,7 +2,7 @@ import { parse } from 'url';
import { CookieJar } from 'tough-cookie';
import { setup, teardown } from '../fixtures/server';
import { defaultConfig, fromCookieJar, get, getCookie } from '../fixtures/helpers';
-import { decodeState, encodeState } from '../../../src/auth0-session/hooks/get-login-state';
+import { decodeState, encodeState } from '../../../src/auth0-session/utils/encoding';
import { LoginOptions } from '../../../src/auth0-session';
import { IncomingMessage } from 'http';
@@ -34,7 +34,6 @@ describe('login', () => {
});
expect(fromCookieJar(cookieJar, baseURL)).toMatchObject({
- appSession: expect.any(String),
_state: parsed.query.state,
_nonce: parsed.query.nonce
});
@@ -72,7 +71,6 @@ describe('login', () => {
});
expect(fromCookieJar(cookieJar, baseURL)).toMatchObject({
- appSession: expect.any(String),
code_verifier: expect.any(String),
state: parsed.query.state,
nonce: parsed.query.nonce
@@ -111,7 +109,6 @@ describe('login', () => {
});
expect(fromCookieJar(cookieJar, baseURL)).toMatchObject({
- appSession: expect.any(String),
_code_verifier: expect.any(String),
_state: parsed.query.state,
_nonce: parsed.query.nonce
@@ -164,6 +161,23 @@ describe('login', () => {
);
});
+ it('should store response_type if different from config', async function () {
+ const cookieJar = new CookieJar();
+ const baseURL = await setup(
+ {
+ ...defaultConfig,
+ clientSecret: '__test_client_secret__',
+ authorizationParams: { response_type: 'code id_token' }
+ },
+ {
+ loginOptions: { authorizationParams: { response_type: 'code' } }
+ }
+ );
+
+ await get(baseURL, '/login', { cookieJar });
+ expect(fromCookieJar(cookieJar, baseURL)._response_type).toEqual('code');
+ });
+
it('should use a custom state builder', async () => {
const baseURL = await setup({
...defaultConfig,
diff --git a/tests/auth0-session/handlers/logout.test.ts b/tests/auth0-session/handlers/logout.test.ts
index 74fafc459..d55394d80 100644
--- a/tests/auth0-session/handlers/logout.test.ts
+++ b/tests/auth0-session/handlers/logout.test.ts
@@ -1,18 +1,20 @@
import { parse } from 'url';
+import nock from 'nock';
import { CookieJar } from 'tough-cookie';
import { SessionResponse, setup, teardown } from '../fixtures/server';
import { toSignedCookieJar, defaultConfig, get, post, fromCookieJar } from '../fixtures/helpers';
import { makeIdToken } from '../fixtures/cert';
-import { encodeState } from '../../../src/auth0-session/hooks/get-login-state';
+import { encodeState } from '../../../src/auth0-session/utils/encoding';
+import wellKnown from '../fixtures/well-known.json';
const login = async (baseURL: string): Promise => {
const nonce = '__test_nonce__';
const state = encodeState({ returnTo: 'https://example.org' });
- const cookieJar = toSignedCookieJar({ state, nonce }, baseURL);
+ const cookieJar = await toSignedCookieJar({ state, nonce }, baseURL);
await post(baseURL, '/callback', {
body: {
state,
- id_token: makeIdToken({ nonce })
+ id_token: await makeIdToken({ nonce })
},
cookieJar
});
@@ -87,11 +89,11 @@ describe('logout route', () => {
});
const nonce = '__test_nonce__';
const state = encodeState({ returnTo: 'https://example.org' });
- const cookieJar = toSignedCookieJar({ state, nonce }, baseURL);
+ const cookieJar = await toSignedCookieJar({ state, nonce }, baseURL);
await post(baseURL, '/callback', {
body: {
state,
- id_token: makeIdToken({ nonce, iss: 'https://test.eu.auth0.com/' })
+ id_token: await makeIdToken({ nonce, iss: 'https://test.eu.auth0.com/' })
},
cookieJar
});
@@ -180,4 +182,93 @@ describe('logout route', () => {
expect(cookies).toHaveProperty('foo');
expect(cookies).not.toHaveProperty('appSession');
});
+
+ it('should pass logout params to idp', async () => {
+ const baseURL = await setup(
+ { ...defaultConfig, idpLogout: true },
+ { logoutOptions: { logoutParams: { foo: 'bar' } } }
+ );
+ const cookieJar = await login(baseURL);
+
+ const session: SessionResponse = await get(baseURL, '/session', { cookieJar });
+ expect(session.id_token).toBeTruthy();
+
+ const { res } = await get(baseURL, '/logout', { cookieJar, fullResponse: true });
+
+ await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized');
+
+ expect(res.statusCode).toEqual(302);
+ const redirect = parse(res.headers.location, true);
+ expect(redirect).toMatchObject({
+ hostname: 'op.example.com',
+ pathname: '/session/end',
+ protocol: 'https:',
+ query: {
+ post_logout_redirect_uri: baseURL,
+ id_token_hint: session.id_token,
+ foo: 'bar'
+ }
+ });
+ });
+
+ it('should pass logout params to auth0', async () => {
+ const baseURL = await setup(
+ { ...defaultConfig, issuerBaseURL: 'https://op.auth0.com', idpLogout: true, auth0Logout: true },
+ { logoutOptions: { logoutParams: { foo: 'bar' } } }
+ );
+ const { end_session_endpoint, ...a0WellKnown } = wellKnown;
+ nock('https://op.auth0.com').get('/.well-known/openid-configuration').reply(200, a0WellKnown);
+ const cookieJar = await login(baseURL);
+
+ const session: SessionResponse = await get(baseURL, '/session', { cookieJar });
+ expect(session.id_token).toBeTruthy();
+
+ const { res } = await get(baseURL, '/logout', { cookieJar, fullResponse: true });
+
+ await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized');
+
+ expect(res.statusCode).toEqual(302);
+ const redirect = parse(res.headers.location, true);
+ expect(redirect).toMatchObject({
+ hostname: 'op.example.com',
+ pathname: '/v2/logout',
+ protocol: 'https:',
+ query: {
+ client_id: defaultConfig.clientID,
+ foo: 'bar'
+ }
+ });
+ });
+
+ it('should ignore null logout params', async () => {
+ const baseURL = await setup(
+ { ...defaultConfig, issuerBaseURL: 'https://op.auth0.com', idpLogout: true, auth0Logout: true },
+ { logoutOptions: { logoutParams: { foo: 'bar', baz: null, qux: undefined, federated: '' } } }
+ );
+ const { end_session_endpoint, ...a0WellKnown } = wellKnown;
+ nock('https://op.auth0.com').get('/.well-known/openid-configuration').reply(200, a0WellKnown);
+ const cookieJar = await login(baseURL);
+
+ const session: SessionResponse = await get(baseURL, '/session', { cookieJar });
+ expect(session.id_token).toBeTruthy();
+
+ const { res } = await get(baseURL, '/logout', { cookieJar, fullResponse: true });
+
+ await expect(get(baseURL, '/session', { cookieJar })).rejects.toThrowError('Unauthorized');
+
+ expect(res.statusCode).toEqual(302);
+ const redirect = parse(res.headers.location, true);
+ expect(redirect).toMatchObject({
+ hostname: 'op.example.com',
+ pathname: '/v2/logout',
+ protocol: 'https:',
+ query: {
+ client_id: defaultConfig.clientID,
+ foo: 'bar',
+ federated: ''
+ }
+ });
+ expect(redirect.query).not.toHaveProperty('baz');
+ expect(redirect.query).not.toHaveProperty('qux');
+ });
});
diff --git a/tests/auth0-session/transient-store.test.ts b/tests/auth0-session/transient-store.test.ts
index de0d8ea01..7f33dcb3b 100644
--- a/tests/auth0-session/transient-store.test.ts
+++ b/tests/auth0-session/transient-store.test.ts
@@ -1,24 +1,24 @@
import { IncomingMessage, ServerResponse } from 'http';
-import { JWK, JWS } from 'jose';
+import * as jose from 'jose';
import { CookieJar } from 'tough-cookie';
-import { getConfig, TransientStore } from '../../src/auth0-session';
+import { getConfig, TransientStore } from '../../src/auth0-session/';
import { signing as deriveKey } from '../../src/auth0-session/utils/hkdf';
import { defaultConfig, fromCookieJar, get, getCookie, toSignedCookieJar } from './fixtures/helpers';
import { setup as createServer, teardown } from './fixtures/server';
-const generateSignature = (cookie: string, value: string): string => {
- const key = JWK.asKey(deriveKey(defaultConfig.secret as string));
- return JWS.sign.flattened(Buffer.from(`${cookie}=${value}`), key, {
- alg: 'HS256',
- b64: false,
- crit: ['b64']
- }).signature;
+const generateSignature = async (cookie: string, value: string): Promise => {
+ const key = await deriveKey(defaultConfig.secret as string);
+ const { signature } = await new jose.FlattenedSign(new TextEncoder().encode(`${cookie}=${value}`))
+ .setProtectedHeader({ alg: 'HS256', b64: false, crit: ['b64'] })
+ .sign(key);
+ return signature;
};
const setup = async (params = defaultConfig, cb: Function, https = true): Promise =>
createServer(params, {
- customListener: (req, res) => {
- res.end(JSON.stringify({ value: cb(req, res) }));
+ customListener: async (req, res) => {
+ const value = await cb(req, res);
+ res.end(JSON.stringify({ value }));
},
https
});
@@ -27,8 +27,10 @@ describe('TransientStore', () => {
afterEach(teardown);
it('should use the passed-in key to set the cookies', async () => {
- const baseURL = await setup(defaultConfig, (req: IncomingMessage, res: ServerResponse) =>
- transientStore.save('test_key', req, res, { value: 'foo' })
+ const baseURL = await setup(
+ defaultConfig,
+ async (req: IncomingMessage, res: ServerResponse) =>
+ await transientStore.save('test_key', req, res, { value: 'foo' })
);
const transientStore = new TransientStore(getConfig({ ...defaultConfig, baseURL }));
const cookieJar = new CookieJar();
@@ -40,11 +42,11 @@ describe('TransientStore', () => {
});
it('should accept list of secrets', async () => {
- const baseURL = await setup(
- { ...defaultConfig, secret: ['__old_secret__', defaultConfig.secret as string] },
- (req: IncomingMessage, res: ServerResponse) => transientStore.save('test_key', req, res, { value: 'foo' })
+ const config = { ...defaultConfig, secret: ['__old_secret__', defaultConfig.secret as string] };
+ const baseURL = await setup(config, (req: IncomingMessage, res: ServerResponse) =>
+ transientStore.save('test_key', req, res, { value: 'foo' })
);
- const transientStore = new TransientStore(getConfig({ ...defaultConfig, baseURL }));
+ const transientStore = new TransientStore(getConfig({ ...config, baseURL }));
const cookieJar = new CookieJar();
const { value } = await get(baseURL, '/', { cookieJar });
const cookies = fromCookieJar(cookieJar, baseURL);
@@ -65,20 +67,6 @@ describe('TransientStore', () => {
expect(cookie?.secure).toEqual(true);
});
- it('should override the secure setting when specified', async () => {
- const baseURL = await setup(defaultConfig, (req: IncomingMessage, res: ServerResponse) =>
- transientStore.save('test_key', req, res, { sameSite: 'lax', value: 'foo' })
- );
- const transientStore = new TransientStore(
- getConfig({ ...defaultConfig, baseURL, session: { cookie: { secure: false } } })
- );
- const cookieJar = new CookieJar();
- const { value } = await get(baseURL, '/', { cookieJar });
- const cookie = getCookie('test_key', cookieJar, baseURL);
- expect(value).toEqual('foo');
- expect(cookie?.secure).toEqual(false);
- });
-
it('should set cookie to not secure when baseURL protocol is http and SameSite=Lax', async () => {
const baseURL = await setup(
defaultConfig,
@@ -157,10 +145,10 @@ describe('TransientStore', () => {
transientStore.read('test_key', req, res)
);
const transientStore = new TransientStore(getConfig({ ...defaultConfig, baseURL }));
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
- test_key: `foo.${generateSignature('test_key', 'foo')}`,
- _test_key: `foo.${generateSignature('_test_key', 'foo')}`
+ test_key: `foo.${await generateSignature('test_key', 'foo')}`,
+ _test_key: `foo.${await generateSignature('_test_key', 'foo')}`
},
baseURL
);
@@ -177,9 +165,9 @@ describe('TransientStore', () => {
transientStore.read('test_key', req, res)
);
const transientStore = new TransientStore(getConfig({ ...defaultConfig, baseURL }));
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
- _test_key: `foo.${generateSignature('_test_key', 'foo')}`
+ _test_key: `foo.${await generateSignature('_test_key', 'foo')}`
},
baseURL
);
@@ -196,9 +184,9 @@ describe('TransientStore', () => {
(req: IncomingMessage, res: ServerResponse) => transientStore.read('test_key', req, res)
);
const transientStore = new TransientStore(getConfig({ ...defaultConfig, baseURL, legacySameSiteCookie: false }));
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
- _test_key: `foo.${generateSignature('_test_key', 'foo')}`
+ _test_key: `foo.${await generateSignature('_test_key', 'foo')}`
},
baseURL
);
@@ -213,7 +201,7 @@ describe('TransientStore', () => {
transientStore.read('test_key', req, res)
);
const transientStore = new TransientStore(getConfig({ ...defaultConfig, baseURL }));
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
test_key: 'foo.bar',
_test_key: 'foo.bar'
diff --git a/tests/auth0-session/utils/cookie.test.ts b/tests/auth0-session/utils/cookie.test.ts
index ab13032f9..04b1e1c7d 100644
--- a/tests/auth0-session/utils/cookie.test.ts
+++ b/tests/auth0-session/utils/cookie.test.ts
@@ -1,6 +1,6 @@
import { AddressInfo } from 'net';
import { createServer, get as getRequest, IncomingMessage, ServerResponse } from 'http';
-import { getAll, get, set, clear } from '../../../src/auth0-session/utils/cookies';
+import NodeCookies from '../../../src/auth0-session/utils/cookies';
const setup = (): Promise<[IncomingMessage, ServerResponse, Function]> =>
new Promise((resolve) => {
@@ -25,27 +25,31 @@ describe('cookie', () => {
it('should get all cookies', async () => {
const [req, , teardown] = await setup();
req.headers.cookie = 'foo=bar; bar=baz;';
- expect(getAll(req)).toMatchObject({ foo: 'bar', bar: 'baz' });
+ expect(new NodeCookies().getAll(req)).toMatchObject({ foo: 'bar', bar: 'baz' });
await teardown();
});
it('should get a cookie by name', async () => {
const [req, , teardown] = await setup();
req.headers.cookie = 'foo=bar; bar=baz;';
- expect(get(req, 'foo')).toEqual('bar');
+ expect(new NodeCookies().getAll(req)['foo']).toEqual('bar');
await teardown();
});
it('should set a cookie', async () => {
const [, res, teardown] = await setup();
- set(res, 'foo', 'bar');
+ const setter = new NodeCookies();
+ setter.set('foo', 'bar');
+ setter.commit(res);
expect(res.getHeader('Set-Cookie')).toEqual(['foo=bar']);
await teardown();
});
it('should set a cookie with opts', async () => {
const [, res, teardown] = await setup();
- set(res, 'foo', 'bar', { httpOnly: true, sameSite: 'strict' });
+ const setter = new NodeCookies();
+ setter.set('foo', 'bar', { httpOnly: true, sameSite: 'strict' });
+ setter.commit(res);
expect(res.getHeader('Set-Cookie')).toEqual(['foo=bar; HttpOnly; SameSite=Strict']);
await teardown();
});
@@ -53,22 +57,38 @@ describe('cookie', () => {
it('should not overwrite existing set cookie', async () => {
const [, res, teardown] = await setup();
res.setHeader('Set-Cookie', 'foo=bar');
- set(res, 'baz', 'qux');
+ const setter = new NodeCookies();
+ setter.set('baz', 'qux');
+ setter.commit(res);
expect(res.getHeader('Set-Cookie')).toEqual(['foo=bar', 'baz=qux']);
await teardown();
});
- it('should not overwrite existing set cookie array', async () => {
+ it('should override existing cookies that equal name', async () => {
const [, res, teardown] = await setup();
- set(res, 'foo', 'bar');
- set(res, 'baz', 'qux');
- expect(res.getHeader('Set-Cookie')).toEqual(['foo=bar', 'baz=qux']);
+ res.setHeader('Set-Cookie', ['foo=bar', 'baz=qux']);
+ const setter = new NodeCookies();
+ setter.set('foo', 'qux');
+ setter.commit(res, 'foo');
+ expect(res.getHeader('Set-Cookie')).toEqual(['baz=qux', 'foo=qux']);
+ await teardown();
+ });
+
+ it('should override existing cookies that match name', async () => {
+ const [, res, teardown] = await setup();
+ res.setHeader('Set-Cookie', ['foo.1=bar', 'foo.2=baz']);
+ const setter = new NodeCookies();
+ setter.set('foo', 'qux');
+ setter.commit(res, 'foo');
+ expect(res.getHeader('Set-Cookie')).toEqual(['foo=qux']);
await teardown();
});
it('should clear cookies', async () => {
const [, res, teardown] = await setup();
- clear(res, 'foo');
+ const setter = new NodeCookies();
+ setter.clear('foo');
+ setter.commit(res);
expect(res.getHeader('Set-Cookie')).toEqual(['foo=; Max-Age=0']);
await teardown();
});
diff --git a/tests/auth0-session/utils/errors.test.ts b/tests/auth0-session/utils/errors.test.ts
new file mode 100644
index 000000000..8981fc809
--- /dev/null
+++ b/tests/auth0-session/utils/errors.test.ts
@@ -0,0 +1,16 @@
+import { IdentityProviderError } from '../../../src';
+
+describe('IdentityProviderError', () => {
+ test('should escape error fields', () => {
+ const error = new IdentityProviderError({
+ name: 'RPError',
+ message: "",
+ error: "",
+ error_description: ""
+ });
+
+ expect(error.message).toEqual('<script>alert('foo')</script>');
+ expect(error.error).toEqual('<script>alert('foo')</script>');
+ expect(error.errorDescription).toEqual('<script>alert('foo')</script>');
+ });
+});
diff --git a/tests/config.test.ts b/tests/config.test.ts
index 70c199312..e29ec1be4 100644
--- a/tests/config.test.ts
+++ b/tests/config.test.ts
@@ -48,6 +48,7 @@ describe('config params', () => {
rolling: true,
rollingDuration: 86400,
absoluteDuration: 604800,
+ storeIDToken: true,
cookie: {
domain: undefined,
path: '/',
@@ -91,7 +92,8 @@ describe('config params', () => {
routes: {
login: '/api/auth/login',
callback: '/api/auth/callback',
- postLogoutRedirect: ''
+ postLogoutRedirect: '',
+ unauthorized: '/api/auth/401'
},
organization: undefined
});
@@ -107,7 +109,8 @@ describe('config params', () => {
AUTH0_COOKIE_HTTP_ONLY: 'on',
AUTH0_COOKIE_SAME_SITE: 'lax',
AUTH0_COOKIE_SECURE: 'ok',
- AUTH0_SESSION_ABSOLUTE_DURATION: 'no'
+ AUTH0_SESSION_ABSOLUTE_DURATION: 'no',
+ AUTH0_SESSION_STORE_ID_TOKEN: '0'
}).baseConfig
).toMatchObject({
auth0Logout: false,
@@ -116,6 +119,7 @@ describe('config params', () => {
legacySameSiteCookie: false,
session: {
absoluteDuration: false,
+ storeIDToken: false,
cookie: {
httpOnly: true,
sameSite: 'lax',
@@ -182,6 +186,7 @@ describe('config params', () => {
},
session: {
absoluteDuration: 100,
+ storeIDToken: false,
cookie: {
transient: false
},
@@ -201,6 +206,7 @@ describe('config params', () => {
},
session: {
absoluteDuration: 100,
+ storeIDToken: false,
cookie: {
transient: false
},
diff --git a/tests/fixtures/frontend.tsx b/tests/fixtures/frontend.tsx
index 222964b5a..5d105fa5d 100644
--- a/tests/fixtures/frontend.tsx
+++ b/tests/fixtures/frontend.tsx
@@ -1,7 +1,7 @@
import React from 'react';
-import { UserProvider, UserProviderProps, UserProfile } from '../../src';
-import { ConfigProvider, ConfigProviderProps, RequestError } from '../../src/frontend';
+import { RequestError, UserProvider, UserProviderProps, UserProfile } from '../../src/client';
+import { default as ConfigProvider, ConfigProviderProps } from '../../src/client/use-config';
type FetchUserMock = {
ok: boolean;
@@ -40,8 +40,8 @@ export const fetchUserMock = (): Promise => {
export const fetchUserUnauthorizedMock = (): Promise => {
return Promise.resolve({
- ok: false,
- status: 401,
+ ok: true,
+ status: 204,
json: () => Promise.resolve(undefined)
});
};
diff --git a/tests/fixtures/global.d.ts b/tests/fixtures/global.d.ts
new file mode 100644
index 000000000..885406e54
--- /dev/null
+++ b/tests/fixtures/global.d.ts
@@ -0,0 +1,17 @@
+declare global {
+ namespace NodeJS {
+ interface Global {
+ getSession?: Function;
+ updateSession?: Function;
+ handleAuth?: Function;
+ withApiAuthRequired?: Function;
+ withPageAuthRequired?: Function;
+ withPageAuthRequiredCSR?: Function;
+ getAccessToken?: Function;
+ asyncProps?: boolean;
+ onError?: Function;
+ }
+ }
+}
+
+export {};
diff --git a/tests/fixtures/oidc-nocks.ts b/tests/fixtures/oidc-nocks.ts
index 2b19cf96d..843a24544 100644
--- a/tests/fixtures/oidc-nocks.ts
+++ b/tests/fixtures/oidc-nocks.ts
@@ -97,13 +97,13 @@ export function codeExchange(params: ConfigParameters, idToken: string, code = '
});
}
-export function refreshTokenExchange(
+export async function refreshTokenExchange(
params: ConfigParameters,
refreshToken: string,
payload: Record,
newToken?: string
-): nock.Scope {
- const idToken = makeIdToken({
+): Promise {
+ const idToken = await makeIdToken({
iss: `${params.issuerBaseURL}/`,
aud: params.clientID,
...payload
@@ -120,14 +120,25 @@ export function refreshTokenExchange(
});
}
-export function refreshTokenRotationExchange(
+export async function failedRefreshTokenExchange(
+ params: ConfigParameters,
+ refreshToken: string,
+ payload: Record,
+ status = 401
+): Promise {
+ return nock(`${params.issuerBaseURL}`)
+ .post('/oauth/token', `grant_type=refresh_token&refresh_token=${refreshToken}`)
+ .reply(status, payload);
+}
+
+export async function refreshTokenRotationExchange(
params: ConfigParameters,
refreshToken: string,
payload: Record,
newToken?: string,
newrefreshToken?: string
-): nock.Scope {
- const idToken = makeIdToken({
+): Promise {
+ const idToken = await makeIdToken({
iss: `${params.issuerBaseURL}/`,
aud: params.clientID,
...payload
diff --git a/tests/fixtures/setup.ts b/tests/fixtures/setup.ts
index 5cc912038..8a8c99dcc 100644
--- a/tests/fixtures/setup.ts
+++ b/tests/fixtures/setup.ts
@@ -1,3 +1,5 @@
+import { IncomingMessage, ServerResponse } from 'http';
+import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
import nock from 'nock';
import { CookieJar } from 'tough-cookie';
import {
@@ -10,38 +12,54 @@ import {
initAuth0,
AccessTokenRequest,
Claims,
- GetAccessTokenResult
+ OnError,
+ Handlers
} from '../../src';
import { codeExchange, discovery, jwksEndpoint, userInfo } from './oidc-nocks';
import { jwks, makeIdToken } from '../auth0-session/fixtures/cert';
import { start, stop } from './server';
-import { encodeState } from '../../src/auth0-session/hooks/get-login-state';
+import { encodeState } from '../../src/auth0-session/utils/encoding';
import { post, toSignedCookieJar } from '../auth0-session/fixtures/helpers';
-import { NextApiRequest, NextApiResponse } from 'next';
+import { HandleLogin, HandleLogout, HandleCallback, HandleProfile } from '../../src';
export type SetupOptions = {
idTokenClaims?: Claims;
+ callbackHandler?: HandleCallback;
callbackOptions?: CallbackOptions;
+ loginHandler?: HandleLogin;
loginOptions?: LoginOptions;
+ logoutHandler?: HandleLogout;
logoutOptions?: LogoutOptions;
+ profileHandler?: HandleProfile;
profileOptions?: ProfileOptions;
withPageAuthRequiredOptions?: WithPageAuthRequiredOptions;
getAccessTokenOptions?: AccessTokenRequest;
+ onError?: OnError;
discoveryOptions?: Record;
userInfoPayload?: Record;
userInfoToken?: string;
asyncProps?: boolean;
};
+export const defaultOnError: OnError = (_req, res, error) => {
+ res.statusMessage = error.message;
+ res.status(error.status || 500).end(error.message);
+};
+
export const setup = async (
config: ConfigParameters,
{
idTokenClaims,
+ callbackHandler,
callbackOptions,
+ logoutHandler,
logoutOptions,
+ loginHandler,
loginOptions,
+ profileHandler,
profileOptions,
withPageAuthRequiredOptions,
+ onError = defaultOnError,
getAccessTokenOptions,
discoveryOptions,
userInfoPayload = {},
@@ -51,7 +69,7 @@ export const setup = async (
): Promise => {
discovery(config, discoveryOptions);
jwksEndpoint(config, jwks);
- codeExchange(config, makeIdToken({ iss: 'https://acme.auth0.local/', ...idTokenClaims }));
+ codeExchange(config, await makeIdToken({ iss: 'https://acme.auth0.local/', ...idTokenClaims }));
userInfo(config, userInfoToken, userInfoPayload);
const {
handleAuth,
@@ -60,73 +78,47 @@ export const setup = async (
handleLogout,
handleProfile,
getSession,
+ updateSession,
getAccessToken,
withApiAuthRequired,
- withPageAuthRequired,
- getServerSidePropsWrapper
- } = await initAuth0(config);
- (global as any).handleAuth = handleAuth.bind(null, {
- async callback(req, res) {
- try {
- await handleCallback(req, res, callbackOptions);
- } catch (error) {
- res.statusMessage = error.message;
- res.status(error.status || 500).end(error.message);
- }
- },
- async login(req, res) {
- try {
- await handleLogin(req, res, loginOptions);
- } catch (error) {
- res.statusMessage = error.message;
- res.status(error.status || 500).end(error.message);
- }
- },
- async logout(req, res) {
- try {
- await handleLogout(req, res, logoutOptions);
- } catch (error) {
- res.status(error.status || 500).end(error.message);
- }
- },
- async profile(req, res) {
- try {
- await handleProfile(req, res, profileOptions);
- } catch (error) {
- res.statusMessage = error.message;
- res.status(error.status || 500).end(error.message);
- }
- }
- });
-
- (global as any).getSession = getSession;
- (global as any).withApiAuthRequired = withApiAuthRequired;
- (global as any).withPageAuthRequired = (): any => withPageAuthRequired(withPageAuthRequiredOptions);
- (global as any).withPageAuthRequiredCSR = withPageAuthRequired;
- (global as any).getAccessToken = (req: NextApiRequest, res: NextApiResponse): Promise =>
+ withPageAuthRequired
+ } = initAuth0(config);
+ const callback: NextApiHandler = (...args) => (callbackHandler || handleCallback)(...args, callbackOptions);
+ const login: NextApiHandler = (...args) => (loginHandler || handleLogin)(...args, loginOptions);
+ const logout: NextApiHandler = (...args) => (logoutHandler || handleLogout)(...args, logoutOptions);
+ const profile: NextApiHandler = (...args) => (profileHandler || handleProfile)(...args, profileOptions);
+ const handlers: Handlers = { onError, callback, login, logout, profile };
+ global.handleAuth = handleAuth.bind(null, handlers);
+ global.getSession = getSession;
+ global.updateSession = updateSession;
+ global.withApiAuthRequired = withApiAuthRequired;
+ global.withPageAuthRequired = (): any => withPageAuthRequired(withPageAuthRequiredOptions);
+ global.withPageAuthRequiredCSR = withPageAuthRequired;
+ global.getAccessToken = (req: IncomingMessage | NextApiRequest, res: ServerResponse | NextApiResponse) =>
getAccessToken(req, res, getAccessTokenOptions);
- (global as any).getServerSidePropsWrapper = getServerSidePropsWrapper;
- (global as any).asyncProps = asyncProps;
+ global.onError = onError;
+ global.asyncProps = asyncProps;
return start();
};
export const teardown = async (): Promise => {
nock.cleanAll();
await stop();
- delete (global as any).getSession;
- delete (global as any).handleAuth;
- delete (global as any).withApiAuthRequired;
- delete (global as any).withPageAuthRequired;
- delete (global as any).withPageAuthRequiredCSR;
- delete (global as any).getAccessToken;
- delete (global as any).getServerSidePropsWrapper;
- delete (global as any).asyncProps;
+ delete global.getSession;
+ delete global.updateSession;
+ delete global.handleAuth;
+ delete global.withApiAuthRequired;
+ delete global.withPageAuthRequired;
+ delete global.withPageAuthRequiredCSR;
+ delete global.getAccessToken;
+ delete global.onError;
+ delete global.asyncProps;
};
export const login = async (baseUrl: string): Promise => {
const nonce = '__test_nonce__';
const state = encodeState({ returnTo: '/' });
- const cookieJar = toSignedCookieJar({ state, nonce }, baseUrl);
+ const cookieJar = await toSignedCookieJar({ state, nonce }, baseUrl);
await post(baseUrl, '/api/auth/callback', {
fullResponse: true,
body: {
diff --git a/tests/fixtures/test-app/pages/api/access-token.ts b/tests/fixtures/test-app/pages/api/access-token.ts
index 6ee4d90ee..001d77506 100644
--- a/tests/fixtures/test-app/pages/api/access-token.ts
+++ b/tests/fixtures/test-app/pages/api/access-token.ts
@@ -2,7 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
export default async function accessTokenHandler(req: NextApiRequest, res: NextApiResponse): Promise {
try {
- const json = await (global as any).getAccessToken(req, res);
+ const json = await global.getAccessToken?.(req, res);
res.status(200).json(json);
} catch (error) {
res.statusMessage = error.message;
diff --git a/tests/fixtures/test-app/pages/api/auth/[...auth0].ts b/tests/fixtures/test-app/pages/api/auth/[...auth0].ts
index d95a4c05b..508718b18 100644
--- a/tests/fixtures/test-app/pages/api/auth/[...auth0].ts
+++ b/tests/fixtures/test-app/pages/api/auth/[...auth0].ts
@@ -1 +1,2 @@
-export default (global as any).handleAuth();
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export default (...args: any) => global.handleAuth?.()(...args);
diff --git a/tests/fixtures/test-app/pages/api/protected.ts b/tests/fixtures/test-app/pages/api/protected.ts
index 090d4f673..6bf27ab1b 100644
--- a/tests/fixtures/test-app/pages/api/protected.ts
+++ b/tests/fixtures/test-app/pages/api/protected.ts
@@ -1,8 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next';
-export default (global as any).withApiAuthRequired(function protectedApiRoute(
- _req: NextApiRequest,
- res: NextApiResponse
-) {
+export default global.withApiAuthRequired?.(function protectedApiRoute(_req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ foo: 'bar' });
});
diff --git a/tests/fixtures/test-app/pages/api/session.ts b/tests/fixtures/test-app/pages/api/session.ts
index d356905af..5fc472471 100644
--- a/tests/fixtures/test-app/pages/api/session.ts
+++ b/tests/fixtures/test-app/pages/api/session.ts
@@ -1,6 +1,6 @@
import { NextApiRequest, NextApiResponse } from 'next';
-export default function sessionHandler(req: NextApiRequest, res: NextApiResponse): void {
- const json = (global as any).getSession(req, res);
+export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse): Promise {
+ const json = await global.getSession?.(req, res);
res.status(200).json(json);
}
diff --git a/tests/fixtures/test-app/pages/api/update-session.ts b/tests/fixtures/test-app/pages/api/update-session.ts
new file mode 100644
index 000000000..b44cdbf80
--- /dev/null
+++ b/tests/fixtures/test-app/pages/api/update-session.ts
@@ -0,0 +1,8 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse): Promise {
+ const session = await global.getSession?.(req, res);
+ const updated = { ...session, ...req.body?.session };
+ await global.updateSession?.(req, res, updated);
+ res.status(200).json(updated);
+}
diff --git a/tests/fixtures/test-app/pages/csr-protected.tsx b/tests/fixtures/test-app/pages/csr-protected.tsx
index 5e4ec646b..0978f503a 100644
--- a/tests/fixtures/test-app/pages/csr-protected.tsx
+++ b/tests/fixtures/test-app/pages/csr-protected.tsx
@@ -3,7 +3,7 @@ import { NextPageContext } from 'next';
// eslint-disable-next-line react/prop-types
export default function protectedPage(): React.ReactElement {
- return (global as any).withPageAuthRequiredCSR(() => Protected Page
);
+ return global.withPageAuthRequiredCSR?.(() => Protected Page
);
}
-export const getServerSideProps = (ctx: NextPageContext): any => (global as any).withPageAuthRequired()(ctx);
+export const getServerSideProps = (ctx: NextPageContext): any => global.withPageAuthRequired?.()(ctx);
diff --git a/tests/fixtures/test-app/pages/global.d.ts b/tests/fixtures/test-app/pages/global.d.ts
new file mode 100644
index 000000000..7ded08bea
--- /dev/null
+++ b/tests/fixtures/test-app/pages/global.d.ts
@@ -0,0 +1 @@
+import '../../global';
diff --git a/tests/fixtures/test-app/pages/protected.tsx b/tests/fixtures/test-app/pages/protected.tsx
index c2867745f..316b8635d 100644
--- a/tests/fixtures/test-app/pages/protected.tsx
+++ b/tests/fixtures/test-app/pages/protected.tsx
@@ -5,4 +5,4 @@ export default function protectedPage({ user }: { user?: { sub: string } }): Rea
return Protected Page {user ? user.sub : ''}
;
}
-export const getServerSideProps = (ctx: NextPageContext): any => (global as any).withPageAuthRequired()(ctx);
+export const getServerSideProps = (ctx: NextPageContext): any => global.withPageAuthRequired?.()(ctx);
diff --git a/tests/fixtures/test-app/pages/wrapped-get-server-side-props.tsx b/tests/fixtures/test-app/pages/wrapped-get-server-side-props.tsx
deleted file mode 100644
index 338682498..000000000
--- a/tests/fixtures/test-app/pages/wrapped-get-server-side-props.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-import { NextPageContext } from 'next';
-
-export default function wrappedGetServerSidePropsPage({
- isAuthenticated
-}: {
- isAuthenticated?: boolean;
-}): React.ReactElement {
- return isAuthenticated: {String(isAuthenticated)}
;
-}
-
-export const getServerSideProps = (_ctx: NextPageContext): any =>
- (global as any).getServerSidePropsWrapper(async (ctx: NextPageContext) => {
- const session = (global as any).getSession(ctx.req, ctx.res);
- const asyncProps = (global as any).asyncProps;
- const props = { isAuthenticated: !!session };
- return { props: asyncProps ? Promise.resolve(props) : props };
- })(_ctx);
diff --git a/tests/frontend/use-config.test.ts b/tests/frontend/use-config.test.ts
index a17210ec9..d79c1a69e 100644
--- a/tests/frontend/use-config.test.ts
+++ b/tests/frontend/use-config.test.ts
@@ -1,7 +1,7 @@
import { renderHook } from '@testing-library/react-hooks';
import { withConfigProvider } from '../fixtures/frontend';
-import { useConfig } from '../../src/frontend/use-config';
+import { useConfig } from '../../src/client/use-config';
describe('context wrapper', () => {
test('should provide the default login url', async () => {
diff --git a/tests/frontend/use-user.test.tsx b/tests/frontend/use-user.test.tsx
index 2219b357d..26da42c02 100644
--- a/tests/frontend/use-user.test.tsx
+++ b/tests/frontend/use-user.test.tsx
@@ -8,8 +8,8 @@ import {
withUserProvider,
user
} from '../fixtures/frontend';
-import { RequestError, useConfig } from '../../src/frontend';
-import { useUser, UserContext } from '../../src';
+import { useUser, UserContext, RequestError } from '../../src/client';
+import { useConfig } from '../../src/client/use-config';
import React from 'react';
describe('context wrapper', () => {
@@ -123,7 +123,7 @@ describe('hook', () => {
expect(result.current.isLoading).toEqual(false);
});
- test('should provide no user when the status code is 401', async () => {
+ test('should provide no user when the status code is 204', async () => {
(global as any).fetch = fetchUserUnauthorizedMock;
const { result, waitForValueToChange } = renderHook(() => useUser(), { wrapper: withUserProvider() });
diff --git a/tests/frontend/with-page-auth-required.test.tsx b/tests/frontend/with-page-auth-required.test.tsx
index 0a6e045d6..eda022aa7 100644
--- a/tests/frontend/with-page-auth-required.test.tsx
+++ b/tests/frontend/with-page-auth-required.test.tsx
@@ -6,7 +6,7 @@ import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { fetchUserErrorMock, withUserProvider, user } from '../fixtures/frontend';
-import { withPageAuthRequired } from '../../src/frontend';
+import { withPageAuthRequired } from '../../src/client';
const windowLocation = window.location;
diff --git a/tests/handlers/auth.test.ts b/tests/handlers/auth.test.ts
new file mode 100644
index 000000000..b867fcd2b
--- /dev/null
+++ b/tests/handlers/auth.test.ts
@@ -0,0 +1,291 @@
+import { IncomingMessage, ServerResponse } from 'http';
+import { ArgumentsOf } from 'ts-jest';
+import { withoutApi } from '../fixtures/default-settings';
+import { login, setup, teardown } from '../fixtures/setup';
+import { get } from '../auth0-session/fixtures/helpers';
+import { initAuth0, OnError, Session } from '../../src';
+import { LoginHandler, LoginOptions } from '../../src/handlers/login';
+import { LogoutHandler, LogoutOptions } from '../../src/handlers/logout';
+import { CallbackHandler, CallbackOptions } from '../../src/handlers/callback';
+import { ProfileHandler, ProfileOptions } from '../../src/handlers/profile';
+import * as baseLoginHandler from '../../src/auth0-session/handlers/login';
+import * as baseLogoutHandler from '../../src/auth0-session/handlers/logout';
+import * as baseCallbackHandler from '../../src/auth0-session/handlers/callback';
+
+const handlerError = () =>
+ expect.objectContaining({
+ status: 400,
+ code: 'ERR_CALLBACK_HANDLER_FAILURE'
+ });
+
+describe('auth handler', () => {
+ afterEach(teardown);
+
+ test('return 500 for unexpected error', async () => {
+ const baseUrl = await setup(withoutApi);
+ global.handleAuth = initAuth0(withoutApi).handleAuth;
+ delete global.onError;
+ jest.spyOn(console, 'error').mockImplementation((error) => {
+ delete error.status;
+ });
+ await expect(get(baseUrl, '/api/auth/callback?error=foo&error_description=bar&state=foo')).rejects.toThrow(
+ 'Internal Server Error'
+ );
+ });
+
+ test('return 404 for unknown routes', async () => {
+ const baseUrl = await setup(withoutApi);
+ global.handleAuth = initAuth0(withoutApi).handleAuth;
+ await expect(get(baseUrl, '/api/auth/foo')).rejects.toThrow('Not Found');
+ });
+
+ test('return 404 for unknown routes including builtin props', async () => {
+ const baseUrl = await setup(withoutApi);
+ global.handleAuth = initAuth0(withoutApi).handleAuth;
+ await expect(get(baseUrl, '/api/auth/__proto__')).rejects.toThrow('Not Found');
+ });
+
+ test('return unauthorized for /401 route', async () => {
+ const baseUrl = await setup(withoutApi);
+ global.handleAuth = initAuth0(withoutApi).handleAuth;
+ await expect(get(baseUrl, '/api/auth/401')).rejects.toThrow('Unauthorized');
+ });
+});
+
+describe('custom error handler', () => {
+ afterEach(teardown);
+
+ test('accept custom error handler', async () => {
+ const onError = jest.fn>((_req, res) => res.end());
+ const baseUrl = await setup(withoutApi, { onError });
+ await get(baseUrl, '/api/auth/callback?error=foo&error_description=bar&state=foo');
+ expect(onError).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), handlerError());
+ });
+
+ test('use default error handler', async () => {
+ const baseUrl = await setup(withoutApi);
+ global.handleAuth = initAuth0(withoutApi).handleAuth;
+ delete global.onError;
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ await expect(get(baseUrl, '/api/auth/callback?error=foo&error_description=bar&state=foo')).rejects.toThrow(
+ 'Bad Request'
+ );
+ expect(console.error).toHaveBeenCalledWith(handlerError());
+ });
+
+ test('finish response if custom error does not', async () => {
+ const onError = jest.fn();
+ const baseUrl = await setup(withoutApi);
+ global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { onError });
+ await expect(
+ get(baseUrl, '/api/auth/callback?error=foo&error_description=bar&state=foo', { fullResponse: true })
+ ).rejects.toThrow('Internal Server Error');
+ expect(onError).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), handlerError());
+ });
+
+ test('finish response with custom error status', async () => {
+ const onError = jest.fn>((_req, res) => res.status(418));
+ const baseUrl = await setup(withoutApi);
+ global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { onError });
+ await expect(
+ get(baseUrl, '/api/auth/callback?error=foo&error_description=bar&state=foo', { fullResponse: true })
+ ).rejects.toThrow("I'm a Teapot");
+ expect(onError).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), handlerError());
+ });
+});
+
+describe('custom handlers', () => {
+ afterEach(teardown);
+
+ test('accept custom login handler', async () => {
+ const login = jest.fn, ArgumentsOf>(async (_req, res) => {
+ res.end();
+ });
+ const baseUrl = await setup(withoutApi);
+ global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { login });
+ await get(baseUrl, '/api/auth/login');
+ expect(login).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse));
+ });
+
+ test('accept custom logout handler', async () => {
+ const logout = jest.fn, ArgumentsOf>(async (_req, res) => {
+ res.end();
+ });
+ const baseUrl = await setup(withoutApi);
+ global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { logout });
+ await get(baseUrl, '/api/auth/logout');
+ expect(logout).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse));
+ });
+
+ test('accept custom callback handler', async () => {
+ const callback = jest.fn, ArgumentsOf>(async (_req, res) => {
+ res.end();
+ });
+ const baseUrl = await setup(withoutApi);
+ global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { callback });
+ await get(baseUrl, '/api/auth/callback');
+ expect(callback).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse));
+ });
+
+ test('accept custom profile handler', async () => {
+ const profile = jest.fn, ArgumentsOf>(async (_req, res) => {
+ res.end();
+ });
+ const baseUrl = await setup(withoutApi);
+ global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { profile });
+ await get(baseUrl, '/api/auth/me');
+ expect(profile).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse));
+ });
+
+ test('accept custom arbitrary handler', async () => {
+ const signup = jest.fn, ArgumentsOf>(async (_req, res) => {
+ res.end();
+ });
+ const baseUrl = await setup(withoutApi);
+ global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { signup });
+ await get(baseUrl, '/api/auth/signup');
+ expect(signup).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse));
+ });
+});
+
+describe('custom options', () => {
+ afterEach(teardown);
+
+ test('accept custom login options', async () => {
+ const loginHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => {
+ res.end();
+ });
+ jest.spyOn(baseLoginHandler, 'default').mockImplementation(() => loginHandler);
+ const options: LoginOptions = { authorizationParams: { scope: 'openid' } };
+ const baseUrl = await setup(withoutApi);
+ const { handleLogin, handleAuth } = initAuth0(withoutApi);
+ global.handleAuth = handleAuth.bind(null, {
+ login: handleLogin(options)
+ });
+ await get(baseUrl, '/api/auth/login');
+ expect(loginHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), options);
+ });
+
+ test('accept custom logout options', async () => {
+ const logoutHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => {
+ res.end();
+ });
+ jest.spyOn(baseLogoutHandler, 'default').mockImplementation(() => logoutHandler);
+ const options: LogoutOptions = { returnTo: '/foo' };
+ const baseUrl = await setup(withoutApi);
+ const { handleLogout, handleAuth } = initAuth0(withoutApi);
+ global.handleAuth = handleAuth.bind(null, {
+ logout: handleLogout(options)
+ });
+ await get(baseUrl, '/api/auth/logout');
+ expect(logoutHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), options);
+ });
+
+ test('accept custom callback options', async () => {
+ const callbackHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => {
+ res.end();
+ });
+ jest.spyOn(baseCallbackHandler, 'default').mockImplementation(() => callbackHandler);
+ const options: CallbackOptions = { redirectUri: '/foo' };
+ const baseUrl = await setup(withoutApi);
+ const { handleCallback, handleAuth } = initAuth0(withoutApi);
+ global.handleAuth = handleAuth.bind(null, {
+ callback: handleCallback(options)
+ });
+ await get(baseUrl, '/api/auth/callback');
+ expect(callbackHandler).toHaveBeenCalledWith(
+ expect.any(IncomingMessage),
+ expect.any(ServerResponse),
+ expect.objectContaining(options)
+ );
+ });
+
+ test('accept custom profile options', async () => {
+ const afterRefetch = jest.fn(async (_req: IncomingMessage, _res: ServerResponse, session: Session) => session);
+ const options: ProfileOptions = { refetch: true, afterRefetch };
+ const baseUrl = await setup(withoutApi);
+ const { handleProfile, handleAuth } = initAuth0(withoutApi);
+ global.handleAuth = handleAuth.bind(null, {
+ profile: handleProfile(options)
+ });
+ const cookieJar = await login(baseUrl);
+ await get(baseUrl, '/api/auth/me', { cookieJar });
+ expect(afterRefetch).toHaveBeenCalled();
+ });
+});
+
+describe('custom options providers', () => {
+ afterEach(teardown);
+
+ test('accept custom login options provider', async () => {
+ const loginHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => {
+ res.end();
+ });
+ jest.spyOn(baseLoginHandler, 'default').mockImplementation(() => loginHandler);
+ const options = { authorizationParams: { scope: 'openid' } };
+ const optionsProvider = jest.fn(() => options);
+ const baseUrl = await setup(withoutApi);
+ const { handleLogin, handleAuth } = initAuth0(withoutApi);
+
+ global.handleAuth = handleAuth.bind(null, {
+ login: handleLogin(optionsProvider)
+ });
+ await get(baseUrl, '/api/auth/login');
+ expect(optionsProvider).toHaveBeenCalled();
+ expect(loginHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), options);
+ });
+
+ test('accept custom logout options provider', async () => {
+ const logoutHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => {
+ res.end();
+ });
+ jest.spyOn(baseLogoutHandler, 'default').mockImplementation(() => logoutHandler);
+ const options: LogoutOptions = { returnTo: '/foo' };
+ const optionsProvider = jest.fn(() => options);
+ const baseUrl = await setup(withoutApi);
+ const { handleLogout, handleAuth } = initAuth0(withoutApi);
+ global.handleAuth = handleAuth.bind(null, {
+ logout: handleLogout(optionsProvider)
+ });
+ await get(baseUrl, '/api/auth/logout');
+ expect(optionsProvider).toHaveBeenCalled();
+ expect(logoutHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), options);
+ });
+
+ test('accept custom callback options provider', async () => {
+ const callbackHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => {
+ res.end();
+ });
+ jest.spyOn(baseCallbackHandler, 'default').mockImplementation(() => callbackHandler);
+ const options: CallbackOptions = { redirectUri: '/foo' };
+ const optionsProvider = jest.fn(() => options);
+ const baseUrl = await setup(withoutApi);
+ const { handleCallback, handleAuth } = initAuth0(withoutApi);
+ global.handleAuth = handleAuth.bind(null, {
+ callback: handleCallback(optionsProvider)
+ });
+ await get(baseUrl, '/api/auth/callback');
+ expect(optionsProvider).toHaveBeenCalled();
+ expect(callbackHandler).toHaveBeenCalledWith(
+ expect.any(IncomingMessage),
+ expect.any(ServerResponse),
+ expect.objectContaining(options)
+ );
+ });
+
+ test('accept custom profile options provider', async () => {
+ const afterRefetch = jest.fn(async (_req: IncomingMessage, _res: ServerResponse, session: Session) => session);
+ const options: ProfileOptions = { refetch: true, afterRefetch };
+ const optionsProvider = jest.fn(() => options);
+ const baseUrl = await setup(withoutApi);
+ const { handleProfile, handleAuth } = initAuth0(withoutApi);
+ global.handleAuth = handleAuth.bind(null, {
+ profile: handleProfile(optionsProvider)
+ });
+ const cookieJar = await login(baseUrl);
+ await get(baseUrl, '/api/auth/me', { cookieJar });
+ expect(optionsProvider).toHaveBeenCalled();
+ expect(afterRefetch).toHaveBeenCalled();
+ });
+});
diff --git a/tests/handlers/callback.test.ts b/tests/handlers/callback.test.ts
index 5b2a5bba8..79be303d2 100644
--- a/tests/handlers/callback.test.ts
+++ b/tests/handlers/callback.test.ts
@@ -1,12 +1,14 @@
import { CookieJar } from 'tough-cookie';
-import timekeeper = require('timekeeper');
+import * as jose from 'jose';
+import timekeeper from 'timekeeper';
import { withApi, withoutApi } from '../fixtures/default-settings';
import { makeIdToken } from '../auth0-session/fixtures/cert';
-import { get, post, toSignedCookieJar } from '../auth0-session/fixtures/helpers';
-import { encodeState } from '../../src/auth0-session/hooks/get-login-state';
-import { setup, teardown } from '../fixtures/setup';
-import { Session, AfterCallback } from '../../src';
+import { defaultConfig, get, post, toSignedCookieJar } from '../auth0-session/fixtures/helpers';
+import { encodeState } from '../../src/auth0-session/utils/encoding';
+import { defaultOnError, setup, teardown } from '../fixtures/setup';
+import { Session, AfterCallback, MissingStateCookieError } from '../../src';
import nock from 'nock';
+import { signing as deriveKey } from '../../src/auth0-session/utils/hkdf';
const callback = (baseUrl: string, body: any, cookieJar?: CookieJar): Promise =>
post(baseUrl, `/api/auth/callback`, {
@@ -15,21 +17,37 @@ const callback = (baseUrl: string, body: any, cookieJar?: CookieJar): Promise => {
+ const key = await deriveKey(defaultConfig.secret as string);
+ const { signature } = await new jose.FlattenedSign(new TextEncoder().encode(`${cookie}=${value}`))
+ .setProtectedHeader({ alg: 'HS256', b64: false, crit: ['b64'] })
+ .sign(key);
+ return signature;
+};
+
describe('callback handler', () => {
afterEach(teardown);
test('should require a state', async () => {
- const baseUrl = await setup(withoutApi);
+ expect.assertions(2);
+ const baseUrl = await setup(withoutApi, {
+ onError(req, res, err) {
+ expect(err.cause).toBeInstanceOf(MissingStateCookieError);
+ defaultOnError(req, res, err);
+ }
+ });
await expect(
callback(baseUrl, {
state: '__test_state__'
})
- ).rejects.toThrow('checks.state argument is missing');
+ ).rejects.toThrow(
+ 'Callback handler failed. CAUSE: Missing state cookie from login request (check login URL, callback URL and cookie config).'
+ );
});
test('should validate the state', async () => {
const baseUrl = await setup(withoutApi);
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state: '__other_state__'
},
@@ -49,7 +67,7 @@ describe('callback handler', () => {
test('should validate the audience', async () => {
const baseUrl = await setup(withoutApi, { idTokenClaims: { aud: 'bar' } });
const state = encodeState({ returnTo: baseUrl });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state,
nonce: '__test_nonce__'
@@ -71,7 +89,7 @@ describe('callback handler', () => {
test('should validate the issuer', async () => {
const baseUrl = await setup(withoutApi, { idTokenClaims: { aud: 'bar', iss: 'other-issuer' } });
const state = encodeState({ returnTo: baseUrl });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state,
nonce: '__test_nonce__'
@@ -92,15 +110,21 @@ describe('callback handler', () => {
it('should escape html in error qp', async () => {
const baseUrl = await setup(withoutApi);
- await expect(get(baseUrl, `/api/auth/callback?error=%3Cscript%3Ealert(%27xss%27)%3C%2Fscript%3E`)).rejects.toThrow(
- '<script>alert('xss')</script>'
+ const cookieJar = await toSignedCookieJar(
+ {
+ state: `foo.${await generateSignature('state', 'foo')}`
+ },
+ baseUrl
);
+ await expect(
+ get(baseUrl, `/api/auth/callback?error=%3Cscript%3Ealert(%27xss%27)%3C%2Fscript%3E&state=foo`, { cookieJar })
+ ).rejects.toThrow('<script>alert('xss')</script>');
});
test('should create the session without OIDC claims', async () => {
const baseUrl = await setup(withoutApi);
const state = encodeState({ returnTo: baseUrl });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state,
nonce: '__test_nonce__'
@@ -117,7 +141,6 @@ describe('callback handler', () => {
);
expect(res.statusCode).toBe(302);
const body = await get(baseUrl, `/api/session`, { cookieJar });
-
expect(body.user).toStrictEqual({
nickname: '__test_nickname__',
sub: '__test_sub__'
@@ -128,7 +151,7 @@ describe('callback handler', () => {
timekeeper.freeze(0);
const baseUrl = await setup(withoutApi);
const state = encodeState({ returnTo: baseUrl });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state,
nonce: '__test_nonce__'
@@ -155,7 +178,7 @@ describe('callback handler', () => {
timekeeper.freeze(0);
const baseUrl = await setup(withApi);
const state = encodeState({ returnTo: baseUrl });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state,
nonce: '__test_nonce__'
@@ -177,9 +200,9 @@ describe('callback handler', () => {
accessToken: 'eyJz93a...k4laUWw',
accessTokenExpiresAt: 750,
accessTokenScope: 'read:foo delete:foo',
- idToken: makeIdToken({ iss: 'https://acme.auth0.local/' }),
token_type: 'Bearer',
refreshToken: 'GEbRxBN...edjnXbL',
+ idToken: await makeIdToken({ iss: 'https://acme.auth0.local/' }),
user: {
nickname: '__test_nickname__',
sub: '__test_sub__'
@@ -188,7 +211,7 @@ describe('callback handler', () => {
timekeeper.reset();
});
- test('remove tokens with afterCallback hook', async () => {
+ test('remove properties from session with afterCallback hook', async () => {
timekeeper.freeze(0);
const afterCallback: AfterCallback = (_req, _res, session: Session): Session => {
delete session.accessToken;
@@ -197,7 +220,7 @@ describe('callback handler', () => {
};
const baseUrl = await setup(withApi, { callbackOptions: { afterCallback } });
const state = encodeState({ returnTo: baseUrl });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state,
nonce: '__test_nonce__'
@@ -218,7 +241,7 @@ describe('callback handler', () => {
expect(session).toStrictEqual({
accessTokenExpiresAt: 750,
accessTokenScope: 'read:foo delete:foo',
- idToken: makeIdToken({ iss: 'https://acme.auth0.local/' }),
+ idToken: await makeIdToken({ iss: 'https://acme.auth0.local/' }),
token_type: 'Bearer',
user: {
nickname: '__test_nickname__',
@@ -236,7 +259,7 @@ describe('callback handler', () => {
};
const baseUrl = await setup(withApi, { callbackOptions: { afterCallback } });
const state = encodeState({ returnTo: baseUrl });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state,
nonce: '__test_nonce__'
@@ -270,7 +293,7 @@ describe('callback handler', () => {
};
const baseUrl = await setup(withApi, { callbackOptions: { afterCallback } });
const state = encodeState({ returnTo: baseUrl });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state,
nonce: '__test_nonce__'
@@ -292,7 +315,7 @@ describe('callback handler', () => {
test('throws for missing org_id claim', async () => {
const baseUrl = await setup({ ...withApi, organization: 'foo' });
const state = encodeState({ returnTo: baseUrl });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state,
nonce: '__test_nonce__'
@@ -314,7 +337,7 @@ describe('callback handler', () => {
test('throws for org_id claim mismatch', async () => {
const baseUrl = await setup({ ...withApi, organization: 'foo' }, { idTokenClaims: { org_id: 'bar' } });
const state = encodeState({ returnTo: baseUrl });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state,
nonce: '__test_nonce__'
@@ -330,9 +353,7 @@ describe('callback handler', () => {
},
cookieJar
)
- ).rejects.toThrow(
- 'Organization Id (org_id) claim value mismatch in the ID token; expected "foo", found "bar"'
- );
+ ).rejects.toThrow('Organization Id (org_id) claim value mismatch in the ID token; expected "foo", found "bar"');
});
test('accepts a valid organization', async () => {
@@ -341,7 +362,7 @@ describe('callback handler', () => {
callbackOptions: { organization: 'foo' }
});
const state = encodeState({ returnTo: baseUrl });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state,
nonce: '__test_nonce__'
@@ -370,7 +391,7 @@ describe('callback handler', () => {
}
});
const state = encodeState({ returnTo: baseUrl });
- const cookieJar = toSignedCookieJar(
+ const cookieJar = await toSignedCookieJar(
{
state,
nonce: '__test_nonce__'
@@ -381,14 +402,14 @@ describe('callback handler', () => {
nock(`${withoutApi.issuerBaseURL}`)
.post('/oauth/token', /grant_type=authorization_code/)
- .reply(200, (_, body) => {
+ .reply(200, async (_, body) => {
spy(body);
return {
access_token: 'eyJz93a...k4laUWw',
expires_in: 750,
scope: 'read:foo delete:foo',
refresh_token: 'GEbRxBN...edjnXbL',
- id_token: makeIdToken({ iss: `${withoutApi.issuerBaseURL}/` }),
+ id_token: await makeIdToken({ iss: `${withoutApi.issuerBaseURL}/` }),
token_type: 'Bearer'
};
});
diff --git a/tests/handlers/login.test.ts b/tests/handlers/login.test.ts
index dc95ee445..81ecfb3ce 100644
--- a/tests/handlers/login.test.ts
+++ b/tests/handlers/login.test.ts
@@ -1,6 +1,6 @@
import { parse as urlParse } from 'url';
import { withoutApi, withApi } from '../fixtures/default-settings';
-import { decodeState } from '../../src/auth0-session/hooks/get-login-state';
+import { decodeState } from '../../src/auth0-session/utils/encoding';
import { setup, teardown } from '../fixtures/setup';
import { get, getCookie } from '../auth0-session/fixtures/helpers';
import { Cookie, CookieJar } from 'tough-cookie';
@@ -282,14 +282,6 @@ describe('login handler', () => {
});
});
- test('should escape html in errors', async () => {
- const baseUrl = await setup(withoutApi, { discoveryOptions: { error: '' } });
-
- await expect(get(baseUrl, '/api/auth/login', { fullResponse: true })).rejects.toThrow(
- '<script>alert("xss")</script>'
- );
- });
-
test('should allow the returnTo to be be overwritten by getState() when provided in the querystring', async () => {
const loginOptions = {
returnTo: '/profile',
diff --git a/tests/handlers/logout.test.ts b/tests/handlers/logout.test.ts
index 111dfc63e..c8e30dd56 100644
--- a/tests/handlers/logout.test.ts
+++ b/tests/handlers/logout.test.ts
@@ -1,17 +1,8 @@
import { parse } from 'cookie';
-import { parse as parseUrl, URL } from 'url';
+import { parse as parseUrl } from 'url';
import { withoutApi } from '../fixtures/default-settings';
+import { get } from '../auth0-session/fixtures/helpers';
import { setup, teardown, login } from '../fixtures/setup';
-import { IncomingMessage } from 'http';
-
-jest.mock('../../src/utils/assert', () => ({
- assertReqRes(req: IncomingMessage) {
- if (req.url?.includes('error=')) {
- const url = new URL(req.url, 'http://example.com');
- throw new Error(url.searchParams.get('error') as string);
- }
- }
-}));
describe('logout handler', () => {
afterEach(teardown);
@@ -20,15 +11,15 @@ describe('logout handler', () => {
const baseUrl = await setup(withoutApi);
const cookieJar = await login(baseUrl);
- const { status, headers } = await fetch(`${baseUrl}/api/auth/logout`, {
- redirect: 'manual',
- headers: {
- cookie: cookieJar.getCookieStringSync(baseUrl)
- }
+ const {
+ res: { statusCode, headers }
+ } = await get(baseUrl, '/api/auth/logout', {
+ cookieJar,
+ fullResponse: true
});
- expect(status).toBe(302);
- expect(parseUrl(headers.get('location') as string, true)).toMatchObject({
+ expect(statusCode).toBe(302);
+ expect(parseUrl(headers['location'], true)).toMatchObject({
protocol: 'https:',
host: 'acme.auth0.local',
query: {
@@ -39,6 +30,30 @@ describe('logout handler', () => {
});
});
+ test('should pass logout params to the identity provider', async () => {
+ const baseUrl = await setup(withoutApi, { logoutOptions: { logoutParams: { foo: 'bar' } } });
+ const cookieJar = await login(baseUrl);
+
+ const {
+ res: { statusCode, headers }
+ } = await get(baseUrl, '/api/auth/logout', {
+ cookieJar,
+ fullResponse: true
+ });
+
+ expect(statusCode).toBe(302);
+ expect(parseUrl(headers['location'], true)).toMatchObject({
+ protocol: 'https:',
+ host: 'acme.auth0.local',
+ query: {
+ returnTo: 'http://www.acme.com',
+ client_id: '__test_client_id__',
+ foo: 'bar'
+ },
+ pathname: '/v2/logout'
+ });
+ });
+
test('should return to the custom path', async () => {
const customReturnTo = 'https://www.foo.bar';
const baseUrl = await setup(withoutApi, {
@@ -46,15 +61,15 @@ describe('logout handler', () => {
});
const cookieJar = await login(baseUrl);
- const { status, headers } = await fetch(`${baseUrl}/api/auth/logout`, {
- redirect: 'manual',
- headers: {
- cookie: cookieJar.getCookieStringSync(baseUrl)
- }
+ const {
+ res: { statusCode, headers }
+ } = await get(baseUrl, '/api/auth/logout', {
+ cookieJar,
+ fullResponse: true
});
- expect(status).toBe(302);
- expect(parseUrl(headers.get('location') as string, true).query).toMatchObject({
+ expect(statusCode).toBe(302);
+ expect(parseUrl(headers['location'], true).query).toMatchObject({
returnTo: 'https://www.foo.bar'
});
});
@@ -65,15 +80,15 @@ describe('logout handler', () => {
});
const cookieJar = await login(baseUrl);
- const { status, headers } = await fetch(`${baseUrl}/api/auth/logout`, {
- redirect: 'manual',
- headers: {
- cookie: cookieJar.getCookieStringSync(baseUrl)
- }
+ const {
+ res: { statusCode, headers }
+ } = await get(baseUrl, '/api/auth/logout', {
+ cookieJar,
+ fullResponse: true
});
- expect(status).toBe(302);
- expect(parseUrl(headers.get('location') as string)).toMatchObject({
+ expect(statusCode).toBe(302);
+ expect(parseUrl(headers['location'])).toMatchObject({
host: 'my-end-session-endpoint',
pathname: '/logout'
});
@@ -85,25 +100,17 @@ describe('logout handler', () => {
});
const cookieJar = await login(baseUrl);
- const res = await fetch(`${baseUrl}/api/auth/logout`, {
- redirect: 'manual',
- headers: {
- cookie: cookieJar.getCookieStringSync(baseUrl)
- }
+ const {
+ res: { headers }
+ } = await get(baseUrl, '/api/auth/logout', {
+ cookieJar,
+ fullResponse: true
});
- expect(parse(res.headers.get('set-cookie') as string)).toMatchObject({
+ expect(parse(headers['set-cookie'][0])).toMatchObject({
appSession: '',
'Max-Age': '0',
Path: '/'
});
});
-
- test('should escape html in errors', async () => {
- const baseUrl = await setup(withoutApi);
-
- const res = await fetch(`${baseUrl}/api/auth/logout?error=%3Cscript%3Ealert(%27xss%27)%3C%2Fscript%3E`);
-
- expect(await res.text()).toEqual('<script>alert('xss')</script>');
- });
});
diff --git a/tests/handlers/profile.test.ts b/tests/handlers/profile.test.ts
index 5e83ecb8b..90fad9e56 100644
--- a/tests/handlers/profile.test.ts
+++ b/tests/handlers/profile.test.ts
@@ -5,17 +5,6 @@ import { get } from '../auth0-session/fixtures/helpers';
import { setup, teardown, login } from '../fixtures/setup';
import { Session, AfterCallback } from '../../src';
import { makeIdToken } from '../auth0-session/fixtures/cert';
-import { IncomingMessage } from 'http';
-import { URL } from 'url';
-
-jest.mock('../../src/utils/assert', () => ({
- assertReqRes(req: IncomingMessage) {
- if (req.url?.includes('error=')) {
- const url = new URL(req.url, 'http://example.com');
- throw new Error(url.searchParams.get('error') as string);
- }
- }
-}));
describe('profile handler', () => {
afterEach(teardown);
@@ -23,7 +12,7 @@ describe('profile handler', () => {
test('should throw an error when not logged in', async () => {
const baseUrl = await setup(withoutApi);
- await expect(get(baseUrl, '/api/auth/me')).rejects.toThrow('Unauthorized');
+ await expect(get(baseUrl, '/api/auth/me')).resolves.toBe('');
});
test('should return the profile when logged in', async () => {
@@ -50,7 +39,7 @@ describe('profile handler', () => {
expect(res.headers['cache-control']).toEqual('no-store');
});
- test('should throw if re-fetching with no Access Token', async () => {
+ test('should throw if re-fetching with no access token', async () => {
const afterCallback: AfterCallback = (_req, _res, session: Session): Session => {
delete session.accessToken;
return session;
@@ -92,7 +81,7 @@ describe('profile handler', () => {
nock(`${withoutApi.issuerBaseURL}`)
.post('/oauth/token', `grant_type=refresh_token&refresh_token=GEbRxBN...edjnXbL`)
.reply(200, {
- id_token: makeIdToken({ iss: 'https://acme.auth0.local/' }),
+ id_token: await makeIdToken({ iss: 'https://acme.auth0.local/' }),
token_type: 'Bearer',
expires_in: 750,
scope: 'read:foo write:foo'
@@ -115,7 +104,7 @@ describe('profile handler', () => {
},
userInfoToken: 'new-access-token'
});
- refreshTokenRotationExchange(withApi, 'GEbRxBN...edjnXbL', {}, 'new-access-token', 'new-refresh-token');
+ await refreshTokenRotationExchange(withApi, 'GEbRxBN...edjnXbL', {}, 'new-access-token', 'new-refresh-token');
const cookieJar = await login(baseUrl);
const profile = await get(baseUrl, '/api/auth/me', { cookieJar });
expect(profile).toMatchObject({ foo: 'bar' });
@@ -153,12 +142,4 @@ describe('profile handler', () => {
await expect(get(baseUrl, '/api/auth/me', { cookieJar })).rejects.toThrowError('some validation error');
});
-
- test('should escape html in errors', async () => {
- const baseUrl = await setup(withoutApi);
-
- const res = await fetch(`${baseUrl}/api/auth/me?error=%3Cscript%3Ealert(%27xss%27)%3C%2Fscript%3E`);
-
- expect(await res.text()).toEqual('<script>alert('xss')</script>');
- });
});
diff --git a/tests/helpers/get-server-side-props-wrapper.test.ts b/tests/helpers/get-server-side-props-wrapper.test.ts
deleted file mode 100644
index 82a0ca13b..000000000
--- a/tests/helpers/get-server-side-props-wrapper.test.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { login, setup, teardown } from '../fixtures/setup';
-import { withoutApi } from '../fixtures/default-settings';
-import { get } from '../auth0-session/fixtures/helpers';
-
-describe('get-server-side-props-wrapper', () => {
- afterEach(teardown);
-
- test('wrap getServerSideProps', async () => {
- const baseUrl = await setup(withoutApi);
-
- const {
- res: { statusCode },
- data
- } = await get(baseUrl, '/wrapped-get-server-side-props', { fullResponse: true });
- expect(statusCode).toBe(200);
- expect(data).toMatch(/isAuthenticated: .*false/);
- });
-
- test('wrap getServerSideProps with session', async () => {
- const baseUrl = await setup(withoutApi);
- const cookieJar = await login(baseUrl);
-
- const {
- res: { statusCode },
- data
- } = await get(baseUrl, '/wrapped-get-server-side-props', { fullResponse: true, cookieJar });
- expect(statusCode).toBe(200);
- expect(data).toMatch(/isAuthenticated: .*true/);
- });
-
- test('wrap getServerSideProps with async props', async () => {
- const baseUrl = await setup(withoutApi, { asyncProps: true });
-
- const {
- res: { statusCode },
- data
- } = await get(baseUrl, '/wrapped-get-server-side-props', { fullResponse: true });
- expect(statusCode).toBe(200);
- expect(data).toMatch(/isAuthenticated: .*false/);
- });
-
- test('wrap getServerSideProps with async props and session', async () => {
- const baseUrl = await setup(withoutApi, { asyncProps: true });
- const cookieJar = await login(baseUrl);
-
- const {
- res: { statusCode },
- data
- } = await get(baseUrl, '/wrapped-get-server-side-props', { fullResponse: true, cookieJar });
- expect(statusCode).toBe(200);
- expect(data).toMatch(/isAuthenticated: .*true/);
- });
-});
diff --git a/tests/helpers/testing.test.ts b/tests/helpers/testing.test.ts
new file mode 100644
index 000000000..aecd41035
--- /dev/null
+++ b/tests/helpers/testing.test.ts
@@ -0,0 +1,84 @@
+import CookieStore from '../../src/auth0-session/cookie-store';
+import { generateSessionCookie } from '../../src/helpers/testing';
+
+jest.mock('../../src/auth0-session/cookie-store');
+
+const encryptMock = jest.spyOn(CookieStore.prototype, 'encrypt');
+const weekInSeconds = 7 * 24 * 60 * 60;
+
+describe('generate-session-cookie', () => {
+ test('use the provided secret', async () => {
+ await generateSessionCookie({}, { secret: '__test_secret__' });
+ expect(CookieStore).toHaveBeenCalledWith(
+ expect.objectContaining({ secret: '__test_secret__' }),
+ expect.any(Function)
+ );
+ });
+
+ test('use the default session configuration values', async () => {
+ await generateSessionCookie({}, { secret: '' });
+ expect(CookieStore).toHaveBeenCalledWith(
+ expect.objectContaining({
+ session: { absoluteDuration: weekInSeconds, cookie: {} }
+ }),
+ expect.any(Function)
+ );
+ });
+
+ test('use the provided session configuration values', async () => {
+ await generateSessionCookie(
+ {},
+ {
+ secret: '',
+ duration: 1000,
+ domain: '__test_domain__',
+ path: '__test_path__',
+ transient: true,
+ httpOnly: false,
+ secure: false,
+ sameSite: 'none'
+ }
+ );
+ expect(CookieStore).toHaveBeenCalledWith(
+ expect.objectContaining({
+ session: {
+ absoluteDuration: 1000,
+ cookie: {
+ domain: '__test_domain__',
+ path: '__test_path__',
+ transient: true,
+ httpOnly: false,
+ secure: false,
+ sameSite: 'none'
+ }
+ }
+ }),
+ expect.any(Function)
+ );
+ });
+
+ test('use the provided session', async () => {
+ await generateSessionCookie({ user: { foo: 'bar' } }, { secret: '' });
+ expect(encryptMock).toHaveBeenCalledWith({ user: { foo: 'bar' } }, expect.anything());
+ });
+
+ test('use the current time for the header values', async () => {
+ const now = Date.now();
+ const current = (now / 1000) | 0;
+ const clock = jest.useFakeTimers('modern');
+ clock.setSystemTime(now);
+ await generateSessionCookie({}, { secret: '' });
+ expect(encryptMock).toHaveBeenCalledWith(expect.anything(), {
+ iat: current,
+ uat: current,
+ exp: current + weekInSeconds
+ });
+ clock.restoreAllMocks();
+ jest.useRealTimers();
+ });
+
+ test('return the encrypted cookie', async () => {
+ encryptMock.mockResolvedValueOnce('foo');
+ expect(generateSessionCookie({}, { secret: '' })).resolves.toBe('foo');
+ });
+});
diff --git a/tests/helpers/with-middleware-auth-required.test.ts b/tests/helpers/with-middleware-auth-required.test.ts
new file mode 100644
index 000000000..51e801ab6
--- /dev/null
+++ b/tests/helpers/with-middleware-auth-required.test.ts
@@ -0,0 +1,216 @@
+/**
+ * @jest-environment @edge-runtime/jest-environment
+ */
+import { NextRequest, NextResponse } from 'next/server';
+import { NextFetchEvent } from 'next/dist/server/web/spec-extension/fetch-event';
+import { initAuth0 } from '../../src/edge';
+import { withoutApi } from '../fixtures/default-settings';
+import { IdTokenClaims } from 'openid-client';
+import { encryption as deriveKey } from '../../src/auth0-session/utils/hkdf';
+import { defaultConfig } from '../auth0-session/fixtures/helpers';
+import { makeIdToken } from '../auth0-session/fixtures/cert';
+import * as jose from 'jose';
+
+const encrypted = async (claims: Partial = { sub: '__test_sub__' }): Promise => {
+ const key = await deriveKey(defaultConfig.secret as string);
+ const epochNow = (Date.now() / 1000) | 0;
+ const weekInSeconds = 7 * 24 * 60 * 60;
+ const payload = {
+ user: claims,
+ access_token: '__test_access_token__',
+ token_type: 'Bearer',
+ id_token: await makeIdToken(claims),
+ refresh_token: '__test_access_token__',
+ expires_at: epochNow + weekInSeconds
+ };
+ return new jose.EncryptJWT({ ...payload })
+ .setProtectedHeader({
+ alg: 'dir',
+ enc: 'A256GCM',
+ uat: epochNow,
+ iat: epochNow,
+ exp: epochNow + weekInSeconds
+ })
+ .encrypt(key);
+};
+
+const setup = async ({ url = 'http://example.com', config = withoutApi, user, middleware }: any = {}) => {
+ const mw = initAuth0(config).withMiddlewareAuthRequired(middleware);
+ const request = new NextRequest(new URL(url));
+ if (user) {
+ request.cookies.set('appSession', await encrypted({ sub: 'foo' }));
+ }
+ return mw(request, new NextFetchEvent({ request, page: '/' })) as NextResponse;
+};
+
+describe('with-middleware-auth-required', () => {
+ test('require auth on anonymous request', async () => {
+ const res = await setup();
+ expect(res.status).toEqual(307);
+ const redirect = new URL(res.headers.get('location') as string);
+ expect(redirect).toMatchObject({
+ hostname: 'example.com',
+ pathname: '/api/auth/login'
+ });
+ expect(redirect.searchParams.get('returnTo')).toEqual('http://example.com/');
+ });
+
+ test('require auth on anonymous requests to api routes', async () => {
+ const res = await setup({ url: 'http://example.com/api/foo' });
+ expect(res.status).toEqual(401);
+ expect(res.headers.get('x-middleware-rewrite')).toEqual('http://example.com/api/auth/401');
+ });
+
+ test('require auth on anonymous requests to api routes with custom 401', async () => {
+ const res = await setup({
+ url: 'http://example.com/api/foo',
+ config: { ...withoutApi, routes: { unauthorized: '/api/foo-401' } }
+ });
+ expect(res.status).toEqual(401);
+ expect(res.headers.get('x-middleware-rewrite')).toEqual('http://example.com/api/foo-401');
+ });
+
+ test('return to previous url', async () => {
+ const res = await setup({ url: 'http://example.com/foo/bar?baz=hello' });
+ const redirect = new URL(res.headers.get('location') as string);
+ expect(redirect).toMatchObject({
+ hostname: 'example.com',
+ pathname: '/api/auth/login'
+ });
+ expect(redirect.searchParams.get('returnTo')).toEqual('http://example.com/foo/bar?baz=hello');
+ });
+
+ test('should ignore static urls', async () => {
+ const res = await setup({ url: 'http://example.com/_next/style.css' });
+ expect(res).toBeUndefined();
+ });
+
+ test('should ignore default sdk urls', async () => {
+ const res = await setup({ url: 'http://example.com/api/auth/login' });
+ expect(res).toBeUndefined();
+ });
+
+ test('should ignore custom sdk urls', async () => {
+ const res = await setup({
+ url: 'http://example.com/api/custom-login',
+ config: {
+ ...withoutApi,
+ routes: { ...withoutApi.routes, login: '/api/custom-login' }
+ }
+ });
+ expect(res).toBeUndefined();
+ });
+
+ test('should redirect to custom sdk urls', async () => {
+ const res = await setup({
+ url: 'http://example.com/my-page',
+ config: {
+ ...withoutApi,
+ routes: { ...withoutApi.routes, login: '/api/custom-login' }
+ }
+ });
+ const redirect = new URL(res.headers.get('location') as string);
+ expect(redirect).toMatchObject({
+ hostname: 'example.com',
+ pathname: '/api/custom-login'
+ });
+ });
+
+ test('should not redirect to 3rd party domain', async () => {
+ const res = await setup({ url: 'http://example.com//evil.com' });
+ const redirect = new URL(res.headers.get('location') as string);
+ expect(redirect).toMatchObject({
+ hostname: 'example.com'
+ });
+ });
+
+ test('should not run custom middleware for unauthenticated users', async () => {
+ const middleware = jest.fn();
+ await setup({ middleware });
+ expect(middleware).not.toHaveBeenCalled();
+ });
+
+ test('should allow authenticated sessions to pass', async () => {
+ const res = await setup({ user: { name: 'dave' } });
+ expect(res.status).toEqual(200);
+ });
+
+ test('should run custom middleware for authenticated users', async () => {
+ const middleware = jest.fn();
+ await setup({ middleware, user: { name: 'dave' } });
+ expect(middleware).toHaveBeenCalled();
+ });
+
+ test('should honor redirects in custom middleware for authenticated users', async () => {
+ const middleware = jest.fn().mockImplementation(() => {
+ return NextResponse.redirect('https://example.com/redirect');
+ });
+ const res = await setup({ middleware, user: { name: 'dave' } });
+ expect(middleware).toHaveBeenCalled();
+ expect(res.status).toEqual(307);
+ expect(res.headers.get('location')).toEqual('https://example.com/redirect');
+ expect(res.headers.get('set-cookie')).toMatch(/^appSession=/);
+ });
+
+ test('should honor rewrites in custom middleware for authenticated users', async () => {
+ const middleware = jest.fn().mockImplementation(() => {
+ return NextResponse.rewrite('https://example.com/rewrite');
+ });
+ const res = await setup({ middleware, user: { name: 'dave' } });
+ expect(middleware).toHaveBeenCalled();
+ expect(res.status).toEqual(200);
+ expect(res.headers.get('x-middleware-rewrite')).toEqual('https://example.com/rewrite');
+ expect(res.headers.get('set-cookie')).toMatch(/^appSession=/);
+ });
+
+ test('should set a session cookie if session is rolling', async () => {
+ const res = await setup({ user: { name: 'dave' } });
+ expect(res.status).toEqual(200);
+ expect(res.headers.get('set-cookie')).toMatch(/^appSession=/);
+ });
+
+ test('should not set a session cookie if session is not rolling', async () => {
+ const res = await setup({ user: { name: 'dave' }, config: { ...withoutApi, session: { rolling: false } } });
+ expect(res.status).toEqual(200);
+ expect(res.headers.get('set-cookie')).toBeNull();
+ });
+
+ test('should set a session cookie and a custom cookie', async () => {
+ const middleware = () => {
+ const res = NextResponse.next();
+ res.cookies.set('foo', 'bar');
+ return res;
+ };
+ const res = await setup({ user: { name: 'dave' }, middleware });
+ expect(res.status).toEqual(200);
+ expect(res.headers.get('set-cookie')).toMatch(/^appSession=.+, foo=bar;/);
+ });
+
+ test('should set just a custom cookie when session is not rolling', async () => {
+ const middleware = () => {
+ const res = NextResponse.next();
+ res.cookies.set('foo', 'bar');
+ return res;
+ };
+ const res = await setup({
+ user: { name: 'dave' },
+ config: { ...withoutApi, session: { rolling: false } },
+ middleware
+ });
+ expect(res.status).toEqual(200);
+ expect(res.headers.get('set-cookie')).toEqual('foo=bar; Path=/');
+ });
+
+ test('should not set a custom cookie or session cookie when session is not rolling', async () => {
+ const middleware = () => {
+ return NextResponse.next();
+ };
+ const res = await setup({
+ user: { name: 'dave' },
+ config: { ...withoutApi, session: { rolling: false } },
+ middleware
+ });
+ expect(res.status).toEqual(200);
+ expect(res.headers.get('set-cookie')).toBeNull();
+ });
+});
diff --git a/tests/helpers/with-page-auth-required.test.ts b/tests/helpers/with-page-auth-required.test.ts
index 049f749a0..732eb42d8 100644
--- a/tests/helpers/with-page-auth-required.test.ts
+++ b/tests/helpers/with-page-auth-required.test.ts
@@ -51,6 +51,32 @@ describe('with-page-auth-required ssr', () => {
expect(spy).toHaveBeenCalledWith(expect.objectContaining({ req: expect.anything(), res: expect.anything() }));
});
+ test('allow to override the user prop', async () => {
+ const baseUrl = await setup(withoutApi, {
+ withPageAuthRequiredOptions: {
+ async getServerSideProps() {
+ return { props: { user: { sub: 'foo' } } };
+ }
+ }
+ });
+ const cookieJar = await login(baseUrl);
+ const { data } = await get(baseUrl, '/protected', { cookieJar, fullResponse: true });
+ expect(data).toMatch(/Protected Page.*foo/);
+ });
+
+ test('allow to override the user prop when using aync props', async () => {
+ const baseUrl = await setup(withoutApi, {
+ withPageAuthRequiredOptions: {
+ async getServerSideProps() {
+ return { props: Promise.resolve({ user: { sub: 'foo' } }) };
+ }
+ }
+ });
+ const cookieJar = await login(baseUrl);
+ const { data } = await get(baseUrl, '/protected', { cookieJar, fullResponse: true });
+ expect(data).toMatch(/Protected Page.*foo/);
+ });
+
test('use a custom login url', async () => {
process.env.NEXT_PUBLIC_AUTH0_LOGIN = '/api/foo';
const baseUrl = await setup(withoutApi);
@@ -62,7 +88,7 @@ describe('with-page-auth-required ssr', () => {
delete process.env.NEXT_PUBLIC_AUTH0_LOGIN;
});
- test('is a noop when invoked as a client-side protection from the server', async () => {
+ test('is a no-op when invoked as a client-side protection from the server', async () => {
const baseUrl = await setup(withoutApi);
const cookieJar = await login(baseUrl);
const {
@@ -106,8 +132,8 @@ describe('with-page-auth-required ssr', () => {
withPageAuthRequiredOptions: {
async getServerSideProps(ctx) {
await Promise.resolve();
- const session = (global as any).getSession(ctx.req, ctx.res);
- session.test = 'Hello World!';
+ const session = await (global as any).getSession(ctx.req, ctx.res);
+ await (global as any).updateSession(ctx.req, ctx.res, { ...session, test: 'Hello World!' });
return { props: {} };
}
}
diff --git a/tests/index.test.ts b/tests/index.test.ts
index e8b8026c7..87cfcc679 100644
--- a/tests/index.test.ts
+++ b/tests/index.test.ts
@@ -1,11 +1,67 @@
-import { withPageAuthRequired, withApiAuthRequired } from '../src';
+import { IncomingMessage, ServerResponse } from 'http';
+import { Socket } from 'net';
+import { withoutApi } from './fixtures/default-settings';
+import { WithApiAuthRequired, WithPageAuthRequired, InitAuth0, GetSession, ConfigParameters } from '../src';
describe('index', () => {
+ let withPageAuthRequired: WithPageAuthRequired,
+ withApiAuthRequired: WithApiAuthRequired,
+ initAuth0: InitAuth0,
+ getSession: GetSession;
+ let env: NodeJS.ProcessEnv;
+
+ const updateEnv = (opts: ConfigParameters) => {
+ process.env = {
+ ...env,
+ AUTH0_ISSUER_BASE_URL: opts.issuerBaseURL,
+ AUTH0_CLIENT_ID: opts.clientID,
+ AUTH0_CLIENT_SECRET: opts.clientSecret,
+ AUTH0_BASE_URL: opts.baseURL,
+ AUTH0_SECRET: opts.secret as string
+ };
+ };
+
+ beforeEach(async () => {
+ env = process.env;
+ ({ withPageAuthRequired, withApiAuthRequired, initAuth0, getSession } = await import('../src'));
+ });
+
+ afterEach(() => {
+ process.env = env;
+ jest.resetModules();
+ });
+
test('withPageAuthRequired should not create an SDK instance at build time', () => {
- const secret = process.env.AUTH0_SECRET;
- delete process.env.AUTH0_SECRET;
+ process.env = { ...env, AUTH0_SECRET: undefined };
expect(() => withApiAuthRequired(jest.fn())).toThrow('"secret" is required');
expect(() => withPageAuthRequired()).not.toThrow();
- process.env.AUTH0_SECRET = secret;
+ });
+
+ test('should error when mixing named exports and own instance', async () => {
+ const instance = initAuth0(withoutApi);
+ const req = new IncomingMessage(new Socket());
+ const res = new ServerResponse(req);
+ await expect(instance.getSession(req, res)).resolves.toBeNull();
+ expect(() => getSession(req, res)).toThrow(
+ "You cannot mix creating your own instance with `initAuth0` and using named exports like `import { handleAuth } from '@auth0/nextjs-auth0'`"
+ );
+ });
+
+ test('should error when mixing own instance and named exports', async () => {
+ updateEnv(withoutApi);
+ const req = new IncomingMessage(new Socket());
+ const res = new ServerResponse(req);
+ await expect(getSession(req, res)).resolves.toBeNull();
+ expect(() => initAuth0()).toThrow(
+ "You cannot mix creating your own instance with `initAuth0` and using named exports like `import { handleAuth } from '@auth0/nextjs-auth0'`"
+ );
+ });
+
+ test('should share instance when using named exports', async () => {
+ updateEnv(withoutApi);
+ const req = new IncomingMessage(new Socket());
+ const res = new ServerResponse(req);
+ await expect(getSession(req, res)).resolves.toBeNull();
+ await expect(getSession(req, res)).resolves.toBeNull();
});
});
diff --git a/tests/session/cache.test.ts b/tests/session/cache.test.ts
index 903923529..acf05b425 100644
--- a/tests/session/cache.test.ts
+++ b/tests/session/cache.test.ts
@@ -1,12 +1,10 @@
import { IncomingMessage, ServerResponse } from 'http';
import { Socket } from 'net';
import { mocked } from 'ts-jest/utils';
-import { CookieStore, getConfig } from '../../src/auth0-session';
-import { Session, SessionCache } from '../../src';
+import { NodeCookies as Cookies, CookieStore, getConfig } from '../../src/auth0-session';
+import { ConfigParameters, Session, SessionCache } from '../../src';
import { withoutApi } from '../fixtures/default-settings';
-jest.mock('on-headers', () => (_res: ServerResponse, cb: Function): void => cb());
-
describe('SessionCache', () => {
let cache: SessionCache;
let req: IncomingMessage;
@@ -14,58 +12,73 @@ describe('SessionCache', () => {
let session: Session;
let cookieStore: CookieStore;
- beforeEach(() => {
- const config = getConfig(withoutApi);
- cookieStore = mocked(new CookieStore(config));
+ const setup = (conf: ConfigParameters) => {
+ const config = getConfig(conf);
+ cookieStore = mocked(new CookieStore(config, Cookies));
cookieStore.save = jest.fn();
session = new Session({ sub: '__test_user__' });
session.idToken = '__test_id_token__';
cache = new SessionCache(config, cookieStore);
req = mocked(new IncomingMessage(new Socket()));
res = mocked(new ServerResponse(req));
+ };
+
+ beforeEach(() => {
+ setup(withoutApi);
});
test('should create an instance', () => {
expect(cache).toBeInstanceOf(SessionCache);
});
- test('should create the session entry', () => {
- cache.create(req, res, session);
- expect(cache.get(req, res)).toEqual(session);
+ test('should create the session entry', async () => {
+ await cache.create(req, res, session);
+ expect(await cache.get(req, res)).toEqual(session);
expect(cookieStore.save).toHaveBeenCalledWith(req, res, session, undefined);
});
- test('should delete the session entry', () => {
- cache.create(req, res, session);
- expect(cache.get(req, res)).toEqual(session);
- cache.delete(req, res);
- expect(cache.get(req, res)).toBeNull();
+ test('should delete the session entry', async () => {
+ await cache.create(req, res, session);
+ expect(await cache.get(req, res)).toEqual(session);
+ await cache.delete(req, res);
+ expect(await cache.get(req, res)).toBeNull();
});
- test('should set authenticated for authenticated user', () => {
- cache.create(req, res, session);
- expect(cache.isAuthenticated(req, res)).toEqual(true);
+ test('should set authenticated for authenticated user', async () => {
+ await cache.create(req, res, session);
+ expect(await cache.isAuthenticated(req, res)).toEqual(true);
});
- test('should set unauthenticated for anonymous user', () => {
- expect(cache.isAuthenticated(req, res)).toEqual(false);
+ test('should set unauthenticated for anonymous user', async () => {
+ expect(await cache.isAuthenticated(req, res)).toEqual(false);
});
- test('should get an id token for authenticated user', () => {
- cache.create(req, res, session);
- expect(cache.getIdToken(req, res)).toEqual('__test_id_token__');
+ test('should get an id token for authenticated user', async () => {
+ await cache.create(req, res, session);
+ expect(await cache.getIdToken(req, res)).toEqual('__test_id_token__');
});
- test('should get no id token for anonymous user', () => {
- expect(cache.getIdToken(req, res)).toBeUndefined();
+ test('should get no id token for anonymous user', async () => {
+ expect(await cache.getIdToken(req, res)).toBeUndefined();
+ });
+
+ test('should save the session on read and update with a rolling session', async () => {
+ cookieStore.read = jest.fn().mockResolvedValue([{ user: { sub: '__test_user__' } }, 500]);
+ expect(await cache.isAuthenticated(req, res)).toEqual(true);
+ expect((await cache.get(req, res))?.user).toEqual({ sub: '__test_user__' });
+ await cache.set(req, res, new Session({ sub: '__new_user__' }));
+ expect((await cache.get(req, res))?.user).toEqual({ sub: '__new_user__' });
+ expect(cookieStore.read).toHaveBeenCalledTimes(1);
+ expect(cookieStore.save).toHaveBeenCalledTimes(2);
});
- test('should read and update the session', () => {
- cookieStore.read = jest.fn().mockReturnValue([{ user: { sub: '__test_user__' } }, 500]);
- expect(cache.isAuthenticated(req, res)).toEqual(true);
- expect(cache.get(req, res)?.user).toEqual({ sub: '__test_user__' });
+ test('should save the session only on update without a rolling session', async () => {
+ setup({ ...withoutApi, session: { rolling: false } });
+ cookieStore.read = jest.fn().mockResolvedValue([{ user: { sub: '__test_user__' } }, 500]);
+ expect(await cache.isAuthenticated(req, res)).toEqual(true);
+ expect((await cache.get(req, res))?.user).toEqual({ sub: '__test_user__' });
cache.set(req, res, new Session({ sub: '__new_user__' }));
- expect(cache.get(req, res)?.user).toEqual({ sub: '__new_user__' });
+ expect((await cache.get(req, res))?.user).toEqual({ sub: '__new_user__' });
expect(cookieStore.read).toHaveBeenCalledTimes(1);
expect(cookieStore.save).toHaveBeenCalledTimes(1);
});
diff --git a/tests/session/get-access-token.test.ts b/tests/session/get-access-token.test.ts
index 05df43e71..d6445fe43 100644
--- a/tests/session/get-access-token.test.ts
+++ b/tests/session/get-access-token.test.ts
@@ -2,7 +2,7 @@ import { login, setup, teardown } from '../fixtures/setup';
import { withApi } from '../fixtures/default-settings';
import { get } from '../auth0-session/fixtures/helpers';
import { Session } from '../../src';
-import { refreshTokenExchange, refreshTokenRotationExchange } from '../fixtures/oidc-nocks';
+import { failedRefreshTokenExchange, refreshTokenExchange, refreshTokenRotationExchange } from '../fixtures/oidc-nocks';
import { makeIdToken } from '../auth0-session/fixtures/cert';
import nock from 'nock';
@@ -122,7 +122,7 @@ describe('get access token', () => {
});
test('should retrieve a new access token if the old one is expired and update the profile', async () => {
- refreshTokenExchange(
+ await refreshTokenExchange(
withApi,
'GEbRxBN...edjnXbL',
{
@@ -148,7 +148,7 @@ describe('get access token', () => {
});
test('should retrieve a new access token if force refresh is set', async () => {
- refreshTokenExchange(
+ await refreshTokenExchange(
withApi,
'GEbRxBN...edjnXbL',
{
@@ -166,8 +166,45 @@ describe('get access token', () => {
expect(refreshToken).toEqual('GEbRxBN...edjnXbL');
});
+ test('should fail when refresh grant fails', async () => {
+ await failedRefreshTokenExchange(withApi, 'GEbRxBN...edjnXbL', {}, 500);
+ const baseUrl = await setup(withApi, { getAccessTokenOptions: { refresh: true } });
+ const cookieJar = await login(baseUrl);
+ await expect(get(baseUrl, '/api/access-token', { cookieJar })).rejects.toThrow(
+ 'The request to refresh the access token failed. CAUSE: expected 200 OK, got: 500 Internal Server Error'
+ );
+ });
+
+ test('should fail when refresh grant fails with oauth error', async () => {
+ await failedRefreshTokenExchange(
+ withApi,
+ 'GEbRxBN...edjnXbL',
+ { error: 'invalid_grant', error_description: 'Unknown or invalid refresh token.' },
+ 401
+ );
+ const baseUrl = await setup(withApi, { getAccessTokenOptions: { refresh: true } });
+ const cookieJar = await login(baseUrl);
+ await expect(get(baseUrl, '/api/access-token', { cookieJar })).rejects.toThrow(
+ 'The request to refresh the access token failed. CAUSE: invalid_grant (Unknown or invalid refresh token.)'
+ );
+ });
+
+ test('should escape oauth error', async () => {
+ await failedRefreshTokenExchange(
+ withApi,
+ 'GEbRxBN...edjnXbL',
+ { error: '', error_description: '' },
+ 401
+ );
+ const baseUrl = await setup(withApi, { getAccessTokenOptions: { refresh: true } });
+ const cookieJar = await login(baseUrl);
+ await expect(get(baseUrl, '/api/access-token', { cookieJar })).rejects.toThrow(
+ 'The request to refresh the access token failed. CAUSE: <script>alert(1)</script> (<script>alert(2)</script>)'
+ );
+ });
+
test('should retrieve a new access token and rotate the refresh token', async () => {
- refreshTokenRotationExchange(
+ await refreshTokenRotationExchange(
withApi,
'GEbRxBN...edjnXbL',
{
@@ -194,7 +231,7 @@ describe('get access token', () => {
});
test('should not overwrite custom session properties when applying a new access token', async () => {
- refreshTokenExchange(
+ await refreshTokenExchange(
withApi,
'GEbRxBN...edjnXbL',
{
@@ -233,36 +270,29 @@ describe('get access token', () => {
});
test('should retrieve a new access token and update the session based on afterRefresh', async () => {
- refreshTokenExchange(
- withApi,
- 'GEbRxBN...edjnXbL',
- {
- email: 'john@test.com',
- name: 'john doe',
- sub: '123'
- },
- 'new-token'
- );
+ await refreshTokenExchange(withApi, 'GEbRxBN...edjnXbL', {}, 'new-token');
const baseUrl = await setup(withApi, {
getAccessTokenOptions: {
refresh: true,
afterRefresh(_req, _res, session) {
- delete session.idToken;
+ delete session.accessTokenScope;
return session;
}
}
});
const cookieJar = await login(baseUrl);
- const { idToken } = await get(baseUrl, '/api/session', { cookieJar });
- expect(idToken).not.toBeUndefined();
+ const { accessTokenScope } = await get(baseUrl, '/api/session', { cookieJar });
+ expect(accessTokenScope).not.toBeUndefined();
const { accessToken } = await get(baseUrl, '/api/access-token', { cookieJar });
expect(accessToken).toEqual('new-token');
- const { idToken: newIdToken } = await get(baseUrl, '/api/session', { cookieJar });
- expect(newIdToken).toBeUndefined();
+ const { accessTokenScope: newAccessTokenScope } = await get(baseUrl, '/api/session', {
+ cookieJar
+ });
+ expect(newAccessTokenScope).toBeUndefined();
});
test('should pass custom auth params in refresh grant request body', async () => {
- const idToken = makeIdToken({
+ const idToken = await makeIdToken({
iss: `${withApi.issuerBaseURL}/`,
aud: withApi.clientID,
email: 'john@test.com',
diff --git a/tests/session/session.test.ts b/tests/session/session.test.ts
index faed75bbd..7a3764a81 100644
--- a/tests/session/session.test.ts
+++ b/tests/session/session.test.ts
@@ -3,31 +3,63 @@ import { fromJson, fromTokenSet } from '../../src/session';
import { makeIdToken } from '../auth0-session/fixtures/cert';
import { Session } from '../../src';
+const routes = { login: '', callback: '', postLogoutRedirect: '', unauthorized: '' };
+
describe('session', () => {
test('should construct a session with a user', async () => {
expect(new Session({ foo: 'bar' }).user).toEqual({ foo: 'bar' });
});
- test('should construct a session from a tokenSet', () => {
- expect(
- fromTokenSet(new TokenSet({ id_token: makeIdToken({ foo: 'bar', bax: 'qux' }) }), {
- identityClaimFilter: ['baz'],
- routes: { login: '', callback: '', postLogoutRedirect: '' }
- }).user
- ).toEqual({
- aud: '__test_client_id__',
- bax: 'qux',
- exp: expect.any(Number),
- foo: 'bar',
- iat: expect.any(Number),
- iss: 'https://op.example.com/',
- nickname: '__test_nickname__',
- nonce: '__test_nonce__',
- sub: '__test_sub__'
+ describe('from tokenSet', () => {
+ test('should construct a session from a tokenSet', async () => {
+ expect(
+ fromTokenSet(new TokenSet({ id_token: await makeIdToken({ foo: 'bar', bax: 'qux' }) }), {
+ identityClaimFilter: ['baz'],
+ routes
+ }).user
+ ).toEqual({
+ aud: '__test_client_id__',
+ bax: 'qux',
+ exp: expect.any(Number),
+ foo: 'bar',
+ iat: expect.any(Number),
+ iss: 'https://op.example.com/',
+ nickname: '__test_nickname__',
+ nonce: '__test_nonce__',
+ sub: '__test_sub__'
+ });
+ });
+
+ test('should store the ID Token by default', async () => {
+ expect(
+ fromTokenSet(new TokenSet({ id_token: await makeIdToken({ foo: 'bar' }) }), {
+ identityClaimFilter: ['baz'],
+ routes
+ }).idToken
+ ).toBeDefined();
+ });
+
+ test('should not store the ID Token', async () => {
+ expect(
+ fromTokenSet(new TokenSet({ id_token: await makeIdToken({ foo: 'bar' }) }), {
+ session: {
+ storeIDToken: false,
+ name: '',
+ rolling: false,
+ rollingDuration: 0,
+ absoluteDuration: 0,
+ cookie: { transient: false, httpOnly: false, sameSite: 'lax' }
+ },
+ identityClaimFilter: ['baz'],
+ routes
+ }).idToken
+ ).toBeUndefined();
});
});
- test('should construct a session from json', () => {
- expect(fromJson({ user: { foo: 'bar' } })?.user).toEqual({ foo: 'bar' });
+ describe('from json', () => {
+ test('should construct a session from json', () => {
+ expect(fromJson({ user: { foo: 'bar' } })?.user).toEqual({ foo: 'bar' });
+ });
});
});
diff --git a/tests/session/update-session.test.ts b/tests/session/update-session.test.ts
new file mode 100644
index 000000000..4d8de02d2
--- /dev/null
+++ b/tests/session/update-session.test.ts
@@ -0,0 +1,49 @@
+import { login, setup, teardown } from '../fixtures/setup';
+import { withoutApi } from '../fixtures/default-settings';
+import { get, post } from '../auth0-session/fixtures/helpers';
+import { CookieJar } from 'tough-cookie';
+
+describe('update-user', () => {
+ afterEach(teardown);
+
+ test('should update session', async () => {
+ const baseUrl = await setup(withoutApi);
+ const cookieJar = await login(baseUrl);
+ const user = await get(baseUrl, '/api/auth/me', { cookieJar });
+ expect(user).toEqual({ nickname: '__test_nickname__', sub: '__test_sub__' });
+ await post(baseUrl, '/api/update-session', { cookieJar, body: { session: { foo: 'bar' } } });
+ const updatedSession = await get(baseUrl, '/api/session', { cookieJar });
+ expect(updatedSession).toMatchObject({
+ foo: 'bar',
+ user: expect.objectContaining({ nickname: '__test_nickname__', sub: '__test_sub__' })
+ });
+ });
+
+ test('should ignore updates if session is not defined', async () => {
+ const baseUrl = await setup(withoutApi);
+ const cookieJar = await login(baseUrl);
+ const user = await get(baseUrl, '/api/auth/me', { cookieJar });
+ expect(user).toEqual({ nickname: '__test_nickname__', sub: '__test_sub__' });
+ await post(baseUrl, '/api/update-session', { cookieJar, body: { session: undefined } });
+ const updatedUser = await get(baseUrl, '/api/auth/me', { cookieJar });
+ expect(updatedUser).toEqual({ nickname: '__test_nickname__', sub: '__test_sub__' });
+ });
+
+ test('should ignore updates if user is not logged in', async () => {
+ const baseUrl = await setup(withoutApi);
+ const cookieJar = new CookieJar();
+ await expect(get(baseUrl, '/api/auth/me', { cookieJar })).resolves.toBe('');
+ await post(baseUrl, '/api/update-session', { body: { session: { sub: 'foo' } }, cookieJar });
+ await expect(get(baseUrl, '/api/auth/me', { cookieJar })).resolves.toBe('');
+ });
+
+ test('should ignore updates if user is not defined in update', async () => {
+ const baseUrl = await setup(withoutApi);
+ const cookieJar = await login(baseUrl);
+ const user = await get(baseUrl, '/api/auth/me', { cookieJar });
+ expect(user).toEqual({ nickname: '__test_nickname__', sub: '__test_sub__' });
+ await post(baseUrl, '/api/update-session', { cookieJar, body: { session: { user: undefined } } });
+ const updatedUser = await get(baseUrl, '/api/auth/me', { cookieJar });
+ expect(updatedUser).toEqual({ nickname: '__test_nickname__', sub: '__test_sub__' });
+ });
+});
diff --git a/tests/setup.ts b/tests/setup.ts
index b56534d0b..3f6b547ea 100644
--- a/tests/setup.ts
+++ b/tests/setup.ts
@@ -1,6 +1,20 @@
+import { Buffer } from 'buffer';
+
+if (typeof TextDecoder !== 'undefined') {
+ // Monkey patch Text Decoder to workaround https://github.com/vercel/edge-runtime/issues/62
+ // This can be removed when https://github.com/vercel/edge-runtime/pull/80 is merged
+ const tmp = TextDecoder.prototype.decode;
+ TextDecoder.prototype.decode = function (input, options) {
+ if (Buffer.isBuffer(input)) {
+ return tmp.call(this, new TextEncoder().encode(input.toString()), options);
+ }
+ return tmp.call(this, input, options);
+ };
+}
+
beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation(() => {
- // noop
+ // no-op
});
});
diff --git a/tests/utils/errors.test.ts b/tests/utils/errors.test.ts
index 8bc1392bf..da76adbb0 100644
--- a/tests/utils/errors.test.ts
+++ b/tests/utils/errors.test.ts
@@ -1,8 +1,183 @@
-import { AccessTokenError, HandlerError } from '../../src/utils/errors';
+import {
+ AccessTokenError,
+ AccessTokenErrorCode,
+ appendCause,
+ AuthError,
+ CallbackHandlerError,
+ HandlerError,
+ LoginHandlerError,
+ LogoutHandlerError,
+ ProfileHandlerError
+} from '../../src/utils/errors';
-describe('errors', () => {
- test('should be instance of themselves', () => {
- expect(new AccessTokenError('code', 'message')).toBeInstanceOf(AccessTokenError);
- expect(new HandlerError(new Error('message'))).toBeInstanceOf(HandlerError);
+describe('appendCause', () => {
+ test('should append the cause error message', () => {
+ const message = 'foo';
+ const cause = new Error('bar');
+
+ expect(appendCause(message, cause)).toEqual(`${message}. CAUSE: ${cause.message}`);
+ });
+
+ test('should not add a period if there is one already', () => {
+ const message = 'foo.';
+ const cause = new Error('bar');
+
+ expect(appendCause(message, cause)).toEqual(`${message} CAUSE: ${cause.message}`);
+ });
+
+ test('should return the error message when there is no cause', () => {
+ const message = 'foo';
+
+ expect(appendCause(message, undefined)).toEqual(message);
+ });
+});
+
+describe('AccessTokenError', () => {
+ test('should be instance of itself', () => {
+ expect(new AccessTokenError(AccessTokenErrorCode.MISSING_SESSION, '')).toBeInstanceOf(AccessTokenError);
+ });
+
+ test('should be instance of AuthError', () => {
+ expect(new AccessTokenError(AccessTokenErrorCode.MISSING_SESSION, '')).toBeInstanceOf(AuthError);
+ });
+
+ test('should set all properties', () => {
+ const message = 'foo';
+ const error = new AccessTokenError(AccessTokenErrorCode.MISSING_ACCESS_TOKEN, message);
+
+ expect(error.code).toEqual(AccessTokenErrorCode.MISSING_ACCESS_TOKEN);
+ expect(error.message).toEqual(message);
+ expect(error.name).toEqual('AccessTokenError');
+ expect(error.cause).toBeUndefined();
+ expect(error.status).toBeUndefined();
+ });
+});
+
+describe('HandlerError', () => {
+ test('should not be instance of itself', () => {
+ expect(new HandlerError({ code: '', message: '', name: '', cause: new Error() })).not.toBeInstanceOf(HandlerError);
+ });
+
+ test('should set all required properties', () => {
+ const code = 'foo';
+ const message = 'bar';
+ const name = 'baz';
+ const cause = new Error('qux');
+ const error = new HandlerError({ code, message, name, cause });
+
+ expect(error.code).toEqual(code);
+ expect(error.message).toEqual(`${message}. CAUSE: ${cause.message}`);
+ expect(error.name).toEqual(name);
+ expect(error.cause).toEqual(cause);
+ expect(error.status).toBeUndefined();
+ });
+
+ test('should set status', () => {
+ const cause: Error & { status?: number } = new Error();
+ cause.status = 400;
+ const error = new HandlerError({ code: '', message: '', name: '', cause });
+
+ expect(error.cause).toEqual(cause);
+ expect(error.status).toEqual(cause.status);
+ });
+});
+
+describe('CallbackHandlerError', () => {
+ test('should be instance of itself', () => {
+ expect(new CallbackHandlerError(new Error())).toBeInstanceOf(CallbackHandlerError);
+ });
+
+ test('should be instance of HandlerError', () => {
+ expect(new CallbackHandlerError(new Error())).toBeInstanceOf(HandlerError);
+ });
+
+ test('should be instance of AuthError', () => {
+ expect(new CallbackHandlerError(new Error())).toBeInstanceOf(AuthError);
+ });
+
+ test('should set all properties', () => {
+ const cause = new Error('foo');
+ const error = new CallbackHandlerError(cause);
+
+ expect(error.code).toEqual(CallbackHandlerError.code);
+ expect(error.message).toEqual(`Callback handler failed. CAUSE: ${cause.message}`);
+ expect(error.name).toEqual('CallbackHandlerError');
+ expect(error.cause).toEqual(cause);
+ expect(error.status).toBeUndefined();
+ });
+});
+
+describe('LoginHandlerError', () => {
+ test('should be instance of itself', () => {
+ expect(new LoginHandlerError(new Error())).toBeInstanceOf(LoginHandlerError);
+ });
+
+ test('should be instance of HandlerError', () => {
+ expect(new LoginHandlerError(new Error())).toBeInstanceOf(HandlerError);
+ });
+
+ test('should be instance of AuthError', () => {
+ expect(new LoginHandlerError(new Error())).toBeInstanceOf(AuthError);
+ });
+
+ test('should set all properties', () => {
+ const cause = new Error('foo');
+ const error = new LoginHandlerError(cause);
+
+ expect(error.code).toEqual(LoginHandlerError.code);
+ expect(error.message).toEqual(`Login handler failed. CAUSE: ${cause.message}`);
+ expect(error.name).toEqual('LoginHandlerError');
+ expect(error.cause).toEqual(cause);
+ expect(error.status).toBeUndefined();
+ });
+});
+
+describe('LogoutHandlerError', () => {
+ test('should be instance of itself', () => {
+ expect(new LogoutHandlerError(new Error())).toBeInstanceOf(LogoutHandlerError);
+ });
+
+ test('should be instance of HandlerError', () => {
+ expect(new LogoutHandlerError(new Error())).toBeInstanceOf(HandlerError);
+ });
+
+ test('should be instance of AuthError', () => {
+ expect(new LogoutHandlerError(new Error())).toBeInstanceOf(AuthError);
+ });
+
+ test('should set all properties', () => {
+ const cause = new Error('foo');
+ const error = new LogoutHandlerError(cause);
+
+ expect(error.code).toEqual(LogoutHandlerError.code);
+ expect(error.message).toEqual(`Logout handler failed. CAUSE: ${cause.message}`);
+ expect(error.name).toEqual('LogoutHandlerError');
+ expect(error.cause).toEqual(cause);
+ expect(error.status).toBeUndefined();
+ });
+});
+
+describe('ProfileHandlerError', () => {
+ test('should be instance of itself', () => {
+ expect(new ProfileHandlerError(new Error())).toBeInstanceOf(ProfileHandlerError);
+ });
+
+ test('should be instance of HandlerError', () => {
+ expect(new ProfileHandlerError(new Error())).toBeInstanceOf(HandlerError);
+ });
+
+ test('should be instance of AuthError', () => {
+ expect(new ProfileHandlerError(new Error())).toBeInstanceOf(AuthError);
+ });
+
+ test('should set all properties', () => {
+ const cause = new Error('foo');
+ const error = new ProfileHandlerError(cause);
+
+ expect(error.code).toEqual(ProfileHandlerError.code);
+ expect(error.message).toEqual(`Profile handler failed. CAUSE: ${cause.message}`);
+ expect(error.name).toEqual('ProfileHandlerError');
+ expect(error.cause).toEqual(cause);
+ expect(error.status).toBeUndefined();
});
});
diff --git a/tests/utils/middleware-cookies.test.ts b/tests/utils/middleware-cookies.test.ts
new file mode 100644
index 000000000..7ee1e01cb
--- /dev/null
+++ b/tests/utils/middleware-cookies.test.ts
@@ -0,0 +1,82 @@
+/**
+ * @jest-environment @edge-runtime/jest-environment
+ */
+import MiddlewareCookies from '../../src/utils/middleware-cookies';
+import { NextRequest, NextResponse } from 'next/server';
+
+const setup = (reqInit?: RequestInit): [NextRequest, NextResponse] => {
+ return [new NextRequest(new URL('http://example.com'), reqInit), NextResponse.next()];
+};
+
+describe('cookie', () => {
+ it('should get all cookies', async () => {
+ const [req] = setup({ headers: { cookie: 'foo=bar; bar=baz;' } });
+ expect(new MiddlewareCookies().getAll(req)).toMatchObject({ foo: 'bar', bar: 'baz' });
+ });
+
+ it('should get all cookies in Next < 13.0.1', async () => {
+ const req = {
+ cookies: new Map([
+ ['foo', 'bar'],
+ ['bar', 'baz']
+ ])
+ } as unknown as NextRequest;
+ expect(new MiddlewareCookies().getAll(req)).toMatchObject({ foo: 'bar', bar: 'baz' });
+ });
+
+ it('should get a cookie by name', async () => {
+ const [req] = setup({ headers: { cookie: 'foo=bar; bar=baz;' } });
+ expect(new MiddlewareCookies().getAll(req)['foo']).toEqual('bar');
+ });
+
+ it('should set a cookie', async () => {
+ const [, res] = setup();
+ const setter = new MiddlewareCookies();
+ setter.set('foo', 'bar');
+ setter.commit(res);
+ expect(res.headers.get('set-cookie')).toEqual('foo=bar');
+ });
+
+ it('should set a cookie with opts', async () => {
+ const [, res] = setup();
+ const setter = new MiddlewareCookies();
+ setter.set('foo', 'bar', { httpOnly: true, sameSite: 'strict' });
+ setter.commit(res);
+ expect(res.headers.get('set-cookie')).toEqual('foo=bar; HttpOnly; SameSite=Strict');
+ });
+
+ it('should not overwrite existing set cookie', async () => {
+ const [, res] = setup();
+ res.headers.set('set-cookie', 'foo=bar');
+ const setter = new MiddlewareCookies();
+ setter.set('baz', 'qux');
+ setter.commit(res);
+ expect(res.headers.get('set-cookie')).toEqual(['foo=bar', 'baz=qux'].join(', '));
+ });
+
+ it('should override existing cookies that equal name', async () => {
+ const [, res] = setup();
+ res.headers.set('set-cookie', ['foo=bar', 'baz=qux'].join(', '));
+ const setter = new MiddlewareCookies();
+ setter.set('foo', 'qux');
+ setter.commit(res, 'foo');
+ expect(res.headers.get('set-cookie')).toEqual(['baz=qux', 'foo=qux'].join(', '));
+ });
+
+ it('should override existing cookies that match name', async () => {
+ const [, res] = setup();
+ res.headers.set('set-cookie', ['foo.1=bar', 'foo.2=baz'].join(', '));
+ const setter = new MiddlewareCookies();
+ setter.set('foo', 'qux');
+ setter.commit(res, 'foo');
+ expect(res.headers.get('set-cookie')).toEqual('foo=qux');
+ });
+
+ it('should clear cookies', async () => {
+ const [, res] = setup();
+ const setter = new MiddlewareCookies();
+ setter.clear('foo');
+ setter.commit(res);
+ expect(res.headers.get('set-cookie')).toEqual('foo=; Max-Age=0');
+ });
+});
diff --git a/typedoc.js b/typedoc.js
index b1fae47f2..64cf6d0ed 100644
--- a/typedoc.js
+++ b/typedoc.js
@@ -4,10 +4,8 @@ module.exports = {
exclude: [
'./src/auth0-session/**',
'./src/session/cache.ts',
- './src/frontend/use-config.tsx',
- './src/utils/!(errors.ts)',
- './src/index.ts',
- './src/index.browser.ts'
+ './src/client/use-config.tsx',
+ './src/utils/!(errors.ts)'
],
excludeExternals: true,
excludePrivate: true,
The error thrown by the user fetcher.
+The error thrown by the default {@link UserFetcher}.
The
-status
property contains the status code of the response. It is0
when the request fails, e.g. due to being - offline.This error is not thrown when the status code of the response is
+401
, because that means the user is not - authenticated.The
+status
property contains the status code of the response. It is0
when the request + fails, for example due to being offline.This error is not thrown when the status code of the response is
204
, because that means the + user is not authenticated.