Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplified configuration of custom URLs [SDK-2264] #253

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Examples

- [Basic Setup](#basic-setup)
- [Customise handlers behaviour](#customise-handlers-behaviour)
- [Customize handlers behaviour](#customize-handlers-behaviour)
- [Use custom auth urls](#use-custom-auth-urls)
- [Protecting a Server Side Rendered (SSR) Page](#protecting-a-server-side-rendered-ssr-page)
- [Protecting a Client Side Rendered (CSR) Page](#protecting-a-client-side-rendered-csr-page)
Expand Down Expand Up @@ -78,7 +78,7 @@ export default () => {

Have a look at the `basic-example` app [./examples/basic-example](./examples/basic-example).

## Customise handlers behaviour
## Customize handlers behaviour

Pass custom parameters to the auth handlers or add your own logging and error handling.

Expand Down Expand Up @@ -132,7 +132,7 @@ export default async function login(req, res) {
export default () => <a href="/api/custom-login">Login</a>;
```

> Note: you will need to specify this custom login URL when calling `withPageAuthRequired` both the [front end version](https://auth0.github.io/nextjs-auth0/interfaces/frontend_with_page_auth_required.withpageauthrequiredoptions.html#loginurl) and [server side version](https://auth0.github.io/nextjs-auth0/modules/helpers_with_page_auth_required.html#withpageauthrequiredoptions)
> Note: If you customize the login URL you will need to set the environment variable `NEXT_PUBLIC_AUTH0_LOGIN` to this custom value for `withPageAuthRequired` to work correctly. And if you customize the profile URL, you will need to set the `NEXT_PUBLIC_AUTH0_PROFILE` environment variable to this custom value for the `useUser` hook to work properly.

## Protecting a Server Side Rendered (SSR) Page

Expand Down
2 changes: 1 addition & 1 deletion src/auth0-session/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export const get = (params: ConfigParameters = {}): Config => {
...params
};

const { value, error, warning } = paramsSchema.validate(config);
const { value, error, warning } = paramsSchema.validate(config, { allowUnknown: true });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise the validation would fail because of the new extra fields.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with doing this is that we will disable a lot of the runtime checking of config, a user might now pass config.cookie.samesite (rather than sameSite) and get no feedback about whether they spelled the property correctly.

For security sensitive properties, this would be an issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with that approach is that you can't really have a configuration object that is any different from the one the underlying session library uses. That's not flexible and is not really an option, as it's a hard coupling with that package. I don't think that being able to catch misspellings is worth trading off for that flexibility.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll come up with a solution for this and #253 (comment) and share it with you

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've put a branch here https://github.com/auth0/nextjs-auth0/tree/split-config

It should make it easier for you to add config that isn't used by the base auth0-session layer https://github.com/auth0/nextjs-auth0/blob/split-config/src/config.ts#L460 - I'll add a few more tests to the config module and raise a PR

if (error) {
throw new TypeError(error.details[0].message);
}
Expand Down
181 changes: 21 additions & 160 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IncomingMessage } from 'http';
import { AuthorizationParameters as OidcAuthorizationParameters } from 'openid-client';
import { LoginOptions, DeepPartial } from './auth0-session';

import { DeepPartial, Config as SessionLayerConfig } from './auth0-session';

/**
* ## Configuration properties.
Expand Down Expand Up @@ -39,6 +39,8 @@ import { LoginOptions, DeepPartial } from './auth0-session';
* - `AUTH0_IDP_LOGOUT`: See {@link idpLogout}
* - `AUTH0_ID_TOKEN_SIGNING_ALG`: See {@link idTokenSigningAlg}
* - `AUTH0_LEGACY_SAME_SITE_COOKIE`: See {@link legacySameSiteCookie}
* - `NEXT_PUBLIC_AUTH0_LOGIN`: See {@link Config.routes}
* - `NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT`: See {@link Config.routes}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not adding NEXT_PUBLIC_AUTH0_PROFILE here as it's not used server-side.

* - `AUTH0_POST_LOGOUT_REDIRECT`: See {@link Config.routes}
* - `AUTH0_CALLBACK`: See {@link Config.routes}
* - `AUTH0_AUDIENCE`: See {@link Config.authorizationParams}
Expand Down Expand Up @@ -82,180 +84,35 @@ import { LoginOptions, DeepPartial } from './auth0-session';
*
* @category Server
*/
export interface Config {
/**
* The secret(s) used to derive an encryption key for the user identity in a session cookie and
* to sign the transient cookies used by the login callback.
* Use a single string key or array of keys for an encrypted session cookie.
* Can use env key SECRET instead.
*/
secret: string | Array<string>;

/**
* Object defining application session cookie attributes.
*/
session: SessionConfig;

/**
* Boolean value to enable Auth0's logout feature.
*/
auth0Logout: boolean;

/**
* URL parameters used when redirecting users to the authorization server to log in.
*
* If this property is not provided by your application, its default values will be:
*
* ```js
* {
* response_type: 'code',
Copy link
Contributor

@adamjmcgrath adamjmcgrath Jan 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to keep these docs for most of these properties because the defaults are different.

If you generate the docs now, it'll say the default response_type is id_token when it should be code https://github.com/auth0/nextjs-auth0/blob/beta/src/auth0-session/config.ts#L40

I also wanted to add the corresponding env vars to each config option, eg

    /**	
   * To opt-out of sending the library and node version to your authorization server	
   * via the `Auth0-Client` header. Default is `true	
   * You can also use the `AUTH0_ENABLE_TELEMETRY` env var
   */	
  enableTelemetry: boolean;

Also baseURL has some Vercel specific docs

* scope: 'openid profile email'
* }
* ```
*
* New values can be passed in to change what is returned from the authorization server
* depending on your specific scenario. Additional custom parameters can be added as well.
*
* **Note:** You must provide the required parameters if this object is set.
*
* ```js
* {
* response_type: 'code',
* scope: 'openid profile email',
*
* // Additional parameters
* acr_value: "tenant:test-tenant",
* custom_param: "custom-value"
* };
* ```
*/
authorizationParams: AuthorizationParameters;

/**
* The root URL for the application router, eg https://localhost
* Can use env key BASE_URL instead.
* If you provide a domain, we will prefix it with `https://` - This can be useful when assigning it to
* `VERCEL_URL` for preview deploys
*/
baseURL: string;

/**
* The Client ID for your application.
* Can be read from CLIENT_ID instead.
*/
clientID: string;

/**
* The Client Secret for your application.
* Required when requesting access tokens.
* Can be read from CLIENT_SECRET instead.
*/
clientSecret?: string;

/**
* Integer value for the system clock's tolerance (leeway) in seconds for ID token verification.`
* Default is 60
*/
clockTolerance: number;

/**
* Integer value for the http timeout in ms for authentication requests.
* Default is 5000
*/
httpTimeout: number;

/**
* To opt-out of sending the library and node version to your authorization server
* via the `Auth0-Client` header. Default is `true
*/
enableTelemetry: boolean;

/**
* @ignore
*/
errorOnRequiredAuth: boolean;

/**
* @ignore
*/
attemptSilentLogin: boolean;

/**
* Function that returns an object with URL-safe state values for `res.oidc.login()`.
* Used for passing custom state parameters to your authorization server.
* Can also be passed in to {@link HandleLogin}
*
* ```js
* {
* ...
* getLoginState(req, options) {
* return {
* returnTo: options.returnTo || req.originalUrl,
* customState: 'foo'
* };
* }
* }
* ``
*/
getLoginState: (req: IncomingMessage, options: LoginOptions) => Record<string, any>;

/**
* Array value of claims to remove from the ID token before storing the cookie session.
* Default is `['aud', 'iss', 'iat', 'exp', 'nbf', 'nonce', 'azp', 'auth_time', 's_hash', 'at_hash', 'c_hash' ]`
*/
identityClaimFilter: string[];

/**
* Boolean value to log the user out from the identity provider on application logout. Default is `true`
*/
idpLogout: boolean;

/**
* String value for the expected ID token algorithm. Default is 'RS256'
*/
idTokenSigningAlg: string;

/**
* REQUIRED. The root URL for the token issuer with no trailing slash.
* This is `https://` plus your Auth0 domain
* Can use env key ISSUER_BASE_URL instead.
*/
issuerBaseURL: string;

/**
* Set a fallback cookie with no SameSite attribute when response_mode is form_post.
* Default is true
*/
legacySameSiteCookie: boolean;

/**
* @ignore
*/
authRequired: boolean;

export interface Config extends SessionLayerConfig {
/**
* Boolean value to automatically install the login and logout routes.
* Configuration parameters to override the default authentication URLs.
*/
routes: {
/**
* @ignore
* Relative path to the login handler.
*/
login: string | false;
login: string;

/**
* Either a relative path to the application or a valid URI to an external domain.
* The user will be redirected to this after a login has been performed.
*/
postLoginRedirect: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to create a postLoginRedirect option, then it should work the same as postLogoutRedirect - in that it is the default in auth0-session#logout if returnTo is not set https://github.com/auth0/nextjs-auth0/blob/beta/src/auth0-session/handlers/logout.ts#L17

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I'll add it.


/**
* @ignore
*/
logout: string | false;
logout: string;

/**
* Either a relative path to the application or a valid URI to an external domain.
* This value must be registered on the authorization server.
* The user will be redirected to this after a logout has been performed.
*/
postLogoutRedirect: string;

/**
* Relative path to the application callback to process the response from the authorization server.
* Relative path to the callback handler.
*/
callback: string;
};
Expand Down Expand Up @@ -398,6 +255,8 @@ export const getParams = (params?: ConfigParameters): ConfigParameters => {
AUTH0_IDP_LOGOUT,
AUTH0_ID_TOKEN_SIGNING_ALG,
AUTH0_LEGACY_SAME_SITE_COOKIE,
NEXT_PUBLIC_AUTH0_LOGIN,
NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT,
AUTH0_POST_LOGOUT_REDIRECT,
AUTH0_CALLBACK,
AUTH0_AUDIENCE,
Expand Down Expand Up @@ -457,8 +316,10 @@ export const getParams = (params?: ConfigParameters): ConfigParameters => {
}
},
routes: {
callback: AUTH0_CALLBACK || '/api/auth/callback',
login: NEXT_PUBLIC_AUTH0_LOGIN || '/api/auth/login',
postLoginRedirect: NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT,
postLogoutRedirect: AUTH0_POST_LOGOUT_REDIRECT,
callback: AUTH0_CALLBACK || '/api/auth/callback',
...params?.routes
}
};
Expand Down
1 change: 1 addition & 0 deletions src/frontend/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as ConfigProvider, ConfigProviderProps, useConfig } from './use-config';
export { default as UserProvider, UserProviderProps, UserProfile, UserContext, useUser } from './use-user';
export { default as withPageAuthRequired, WithPageAuthRequired } from './with-page-auth-required';
22 changes: 22 additions & 0 deletions src/frontend/use-config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { ReactElement, useContext, createContext } from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you exclude this file from the API Docs https://github.com/auth0/nextjs-auth0/blob/beta/typedoc.js#L4

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure 👍🏼

import { useRouter } from 'next/router';

export type ConfigContext = {
loginUrl?: string;
returnTo?: string;
};

const Config = createContext<ConfigContext>({});

export type ConfigProviderProps = React.PropsWithChildren<ConfigContext>;
export type UseConfig = () => ConfigContext;
export const useConfig: UseConfig = () => useContext<ConfigContext>(Config);

export default ({
Widcket marked this conversation as resolved.
Show resolved Hide resolved
children,
loginUrl = process.env.NEXT_PUBLIC_AUTH0_LOGIN || '/api/auth/login',
returnTo = process.env.NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT
}: ConfigProviderProps): ReactElement<ConfigContext> => {
const router = useRouter();
return <Config.Provider value={{ loginUrl, returnTo: returnTo || router.asPath }}>{children}</Config.Provider>;
};
47 changes: 32 additions & 15 deletions src/frontend/use-user.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { ReactElement, useState, useEffect, useCallback, useContext, createContext } from 'react';

import ConfigProvider, { ConfigContext } from './use-config';

/**
* The user claims returned from the {@link useUser} hook.
*
Expand All @@ -21,12 +23,12 @@ export interface UserProfile {
*
* @category Client
*/
export interface UserContext {
export type UserContext = {
user?: UserProfile;
error?: Error;
isLoading: boolean;
checkSession: () => Promise<void>;
}
};

/**
* Configure the {@link UserProvider} component.
Expand Down Expand Up @@ -59,7 +61,7 @@ export interface UserContext {
*
* @category Client
*/
export type UserProviderProps = React.PropsWithChildren<{ user?: UserProfile; profileUrl?: string }>;
export type UserProviderProps = React.PropsWithChildren<{ user?: UserProfile; profileUrl?: string } & ConfigContext>;

/**
* @ignore
Expand Down Expand Up @@ -119,33 +121,48 @@ export const useUser: UseUser = () => useContext<UserContext>(User);
*/
export type UserProvider = (props: UserProviderProps) => ReactElement<UserContext>;

/**
* @ignore
*/
type UserProviderState = {
user?: UserProfile;
error?: Error;
isLoading: boolean;
};

export default ({
children,
user: initialUser,
profileUrl = '/api/auth/me'
profileUrl = process.env.NEXT_PUBLIC_AUTH0_PROFILE || '/api/auth/me',
loginUrl,
returnTo
}: UserProviderProps): ReactElement<UserContext> => {
const [user, setUser] = useState<UserProfile | undefined>(() => initialUser);
const [error, setError] = useState<Error | undefined>();
const [isLoading, setIsLoading] = useState<boolean>(() => !initialUser);
const [state, setState] = useState<UserProviderState>({ user: initialUser, isLoading: !initialUser });

const checkSession = useCallback(async (): Promise<void> => {
try {
const response = await fetch(profileUrl);
setUser(response.ok ? await response.json() : undefined);
setError(undefined);
const user = response.ok ? await response.json() : undefined;
setState((previous) => ({ ...previous, user, error: undefined }));
} catch (_e) {
setUser(undefined);
setError(new Error(`The request to ${profileUrl} failed`));
const error = new Error(`The request to ${profileUrl} failed`);
setState((previous) => ({ ...previous, user: undefined, error }));
}
}, [profileUrl]);

useEffect((): void => {
if (user) return;
if (state.user) return;
(async (): Promise<void> => {
await checkSession();
setIsLoading(false);
setState((previous) => ({ ...previous, isLoading: false }));
})();
}, [user]);
}, [state.user]);

const { user, error, isLoading } = state;

return <User.Provider value={{ user, error, isLoading, checkSession }}>{children}</User.Provider>;
return (
<ConfigProvider loginUrl={loginUrl} returnTo={returnTo}>
<User.Provider value={{ user, error, isLoading, checkSession }}>{children}</User.Provider>
</ConfigProvider>
);
};
Loading