Skip to content

Commit

Permalink
Add option to disable redirect in useAuth hook when a user is unaut…
Browse files Browse the repository at this point in the history
…henticated (#531)

* Add option to disable redirect in `useAuth` hook

Introduced `UseAuthOptions` that allows a user to specify the `shouldRedirect` property. This property defines if the `useAuth` hook should redirect to the appropriate url when unauthenticated.

* `UseAuthOptions` docs

Describe the new `UseAuthOptions` properties and provide examples

* `UseAuthOptions` changeset

* Changeset for `Content-Type` header fix

* Fix middleware tests

Added `setHeader` to res mock

* Use lodash defaults for `UseAuthOptions`
  • Loading branch information
blakewilson committed Oct 4, 2021
1 parent 647020b commit 5c7f662
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 27 deletions.
27 changes: 27 additions & 0 deletions .changeset/six-dingos-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@faustjs/next': minor
---

Introduced an argument to the `useAuth` hook, `UseAuthOptions`, to provide users the ability to disable automatic redirect from the `useAuth` hook upon an unauthenticated user.

```tsx
import { client } from 'client';

export default function Page() {
const { isLoading, isAuthenticated, authResult } = client.auth.useAuth({
shouldRedirect: false,
});

if (isLoading) {
return <p>Loading...</p>;
}

if (!isAuthenticated) {
return (
<p>You need to be authenticated to see this content. Please login.</p>
);
}

return <p>Authenticated content</p>;
}
```
5 changes: 5 additions & 0 deletions .changeset/weak-sheep-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@faustjs/core': patch
---

Added the appropriate `Content-Type` response header to the `authorizeHandler` middleware
4 changes: 2 additions & 2 deletions docs/next/guides/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Redirect based authentication is the default strategy in Faust.js. This strategy

This strategy is great for use cases where your authenticated users are admins/editors/etc. and do not necessarily need a "white label" login/register experience. Typically, you would use the redirect strategy if your primary reason for authentication is previews.

Since Redirect based authentication is the default authentication method, there is no configuration needed on your end to use it. It comes out of the box, and you'll see it in action when using previews or the `useAuth` hook.
Since Redirect based authentication is the default authentication method, there is no configuration needed on your end to use it. It comes out of the box, and you'll see it in action when using previews or the [`useAuth`](/docs/next/reference/custom-hooks#useauth) hook.

### Local Based Authentication

Expand Down Expand Up @@ -194,4 +194,4 @@ export default function Page() {
}
```

**Note:** The `useAuth` hook fetches the applicable tokens and ensures that the user is authenticated. Therefore, you should check for `isAuthenticated` prior to making authenticated requests, as doing so too early will result in a request without a valid access token.
**Note:** The [`useAuth`](/docs/next/reference/custom-hooks#useauth) hook fetches the applicable tokens and ensures that the user is authenticated. Therefore, you should check for `isAuthenticated` prior to making authenticated requests, as doing so too early will result in a request without a valid access token.
59 changes: 56 additions & 3 deletions docs/next/reference/custom-hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,22 @@ export default function Preview() {

### `useAuth`

The `useAuth` hook provides a way to guarantee a page's content is only rendered if the user is authenticated. If the user is not authenticated, the page will redirect to the WordPress backend if the `authType` is `redirect`, and to the `loginPagePath` if `authType` is `local`.
The `useAuth` hook provides a way to guarantee a users' authentication state, thus allowing you to control how a page's content is rendered.

The following example shows how to use the `useAuth` hook to render a page only if the user is authenticated:
The `useAuth` hook accepts 1 argument of `UseAuthOptions`, which is an object that contains the following properties:

```ts
const options = {
// Specify if the useAuth hook should facilitate the redirect to the appropriate url.
shouldRedirect: true;
}
```

By default, if the user is not authenticated, the page will redirect to the WordPress backend if the `authType` is `redirect`, and to the `loginPagePath` if `authType` is `local`.

However, if the `shouldRedirect` option is `false`, the `useAuth` hook will **not** facilitate the redirect.

The example below shows how to use the `useAuth` hook to render a page that requires authentication. If a user is authenticated, they will be shown the content. Otherwise, they will be redirected to the appropriate URL to authenticate:

```tsx title=src/pages/gated-content.tsx {5}
import { client } from 'client';
Expand All @@ -230,8 +243,27 @@ export default function Gated() {
return <div>Loading...</div>;
}

return <div>Authenticated content</div>;
}
```

Additionally, the example below shows how to use `useAuth` with the `shouldRedirect` option set to `false`. This will disable the automatic redirect to the appropriate URL to authenticate, and allows you to control how the page's content is rendered in an unauthenticated state:

```tsx title=src/pages/gated-content.tsx {5-7,13-15}
import { client } from 'client';

export default function Gated() {
const { useAuth } = client.auth;
const { isLoading, isAuthenticated, authResult } = useAuth({
shouldRedirect: false,
});

if (isLoading) {
return <div>Loading...</div>;
}

if (!isAuthenticated) {
return <div>You are not authenticated!</div>;
return <div>You are not authenticated! Please login.</div>;
}

return <div>Authenticated content</div>;
Expand All @@ -242,6 +274,27 @@ export default function Gated() {

- `isLoading`: A boolean that indicates whether the `useAuth` function is currently checking if a user is authenticated.
- `isAuthenticated`: A boolean that indicates whether the user is authenticated.
- `authResult`: The result from checking if there is an authenticated user.

If there is an authenticated user, the `authResult` will be `true`. Otherwise, the `authResult` will be an object with the following properties:

```js title="The authResult object when there is no authenticated user"
{
/**
* An absolute URL to the WordPress backend that the user should be redirected to in order to authenticate.
* This property is used for the "redirect" based authentication strategy
*/
redirect: 'xxxx';

/*
* A relative URL path to the local login page as specified in the `loginPagePath` option.
* This property is used for the "local" based authentication strategy
*/
login: 'xxxx';
}
```

The `authResult` can be helpful if you want to handle the redirection yourself, instead of the `useAuth` hook.

### `useLogin`

Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/auth/server/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export async function authorizeHandler(

if (!refreshToken && !code) {
res.statusCode = 401;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Unauthorized' }));

return;
Expand All @@ -66,12 +67,14 @@ export async function authorizeHandler(
oauth.setRefreshToken(undefined);
}

res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(result.result));
}
} catch (e) {
log(e);

res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Internal Server Error' }));
}
}
Expand Down
8 changes: 7 additions & 1 deletion packages/core/test/auth/server/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
describe('auth/middleware', () => {
test('redirect will write a 302', () => {
const res: ServerResponse = {
setHeader() {},
writeHead() {},
end() {},
} as any;
Expand Down Expand Up @@ -37,6 +38,7 @@ describe('auth/middleware', () => {
} as any;

const res: ServerResponse = {
setHeader() {},
writeHead() {},
end() {},
} as any;
Expand Down Expand Up @@ -64,14 +66,17 @@ describe('auth/middleware', () => {
} as any;

const res: ServerResponse = {
setHeader() {},
writeHead() {},
end() {},
} as any;

try {
await authorizeHandler(req, res);
} catch (e) {
expect((e as Error).message).toContain('The apiClientSecret must be specified');
expect((e as Error).message).toContain(
'The apiClientSecret must be specified',
);
console.log(e);
}
});
Expand All @@ -90,6 +95,7 @@ describe('auth/middleware', () => {
} as any;

const res: ServerResponse = {
setHeader() {},
writeHead() {},
end() {},
} as any;
Expand Down
15 changes: 4 additions & 11 deletions packages/next/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { RequiredSchema } from '@faustjs/react';
import { ReactClient, UseMutationOptions } from '@gqty/react';
import { ReactClient } from '@gqty/react';
import type { GQtyError } from 'gqty';
import type { NextClient } from '../client';

import { create as createAuthHook } from './useAuth';
import { create as createAuthHook, UseAuthOptions } from './useAuth';
import { create as createLazyQueryHook } from './useLazyQuery';
import { create as createMutationHook } from './useMutation';
import { create as createPaginatedQueryHook } from './usePaginatedQuery';
Expand All @@ -16,7 +16,7 @@ import { create as createPostsHook } from './usePosts';
import { create as createPostHook } from './usePost';
import { create as createPageHook } from './usePage';
import { create as createPreviewHook, UsePreviewResponse } from './usePreview';
import { create as createLoginHook } from './useLogin';
import { create as createLoginHook, UseLoginOptions } from './useLogin';

export type UseClient<
Schema extends RequiredSchema,
Expand All @@ -30,13 +30,6 @@ export type UseClient<
| NextClient<Schema, ObjectTypesNames, ObjectTypes>['useClient']
| NextClient<Schema, ObjectTypesNames, ObjectTypes>['auth']['useClient'];

export interface UseLoginOptions {
useMutationOptions?: UseMutationOptions<{
code?: string | null | undefined;
error?: string | null | undefined;
}>;
}

interface WithAuthHooks<Schema extends RequiredSchema> {
/**
* Faust.js hook to get preview data for a page or post.
Expand All @@ -50,7 +43,7 @@ interface WithAuthHooks<Schema extends RequiredSchema> {
*
* @see https://faustjs.org/docs/next/reference/custom-hooks#useauth
*/
useAuth(): {
useAuth(options?: UseAuthOptions): {
isLoading: boolean;
isAuthenticated: boolean | undefined;
authResult:
Expand Down
30 changes: 26 additions & 4 deletions packages/next/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { ensureAuthorization, headlessConfig } from '@faustjs/core';
import { useEffect, useState } from 'react';
import type { RequiredSchema } from '@faustjs/react';
import defaults from 'lodash/defaults';
import isObject from 'lodash/isObject';
import isUndefined from 'lodash/isUndefined';
import trim from 'lodash/trim';
import noop from 'lodash/noop';
import trim from 'lodash/trim';
import { useEffect, useState } from 'react';
import type { NextClient } from '../client';

export interface UseAuthOptions {
/**
* Specify if the useAuth hook should facilitate the redirect to the appropriate url.
*
* @default true
* @type {boolean}
* @memberof UseAuthOptions
*/
shouldRedirect?: boolean;
}

export function create<
Schema extends RequiredSchema,
ObjectTypesNames extends string = never,
Expand All @@ -16,7 +28,12 @@ export function create<
};
} = never,
>(): NextClient<Schema, ObjectTypesNames, ObjectTypes>['auth']['useAuth'] {
return () => {
return (useAuthOptions?: UseAuthOptions) => {
const options = defaults({}, useAuthOptions, {
shouldRedirect: true,
});

const { shouldRedirect } = options;
const { authType, loginPagePath } = headlessConfig();
const [{ isAuthenticated, isLoading, authResult }, setState] = useState<
ReturnType<
Expand Down Expand Up @@ -67,6 +84,11 @@ export function create<

// Redirect the user to the login page if they are not authenticated
useEffect(() => {
// Do not redirect if specified in UseAuthOptions.
if (!shouldRedirect) {
return noop;
}

if (typeof window === 'undefined') {
return noop;
}
Expand All @@ -93,7 +115,7 @@ export function create<
return () => {
clearTimeout(timeout);
};
}, [isAuthenticated, authResult, authType]);
}, [shouldRedirect, isAuthenticated, authResult, authType]);

return { isAuthenticated, isLoading, authResult };
};
Expand Down
14 changes: 8 additions & 6 deletions packages/next/src/hooks/useLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ import noop from 'lodash/noop';
import { useEffect } from 'react';
import type { NextClientHooks, NextClientHooksWithAuth } from '.';

export interface UseLoginOptions {
useMutationOptions?: UseMutationOptions<{
code?: string | null | undefined;
error?: string | null | undefined;
}>;
}

export function create<Schema extends RequiredSchema>(
useMutation: NextClientHooks<Schema>['useMutation'],
): NextClientHooksWithAuth<Schema>['useLogin'] {
return (options?: {
useMutationOptions?: UseMutationOptions<{
code?: string | null | undefined;
error?: string | null | undefined;
}>;
}) => {
return (options?: UseLoginOptions) => {
const { useMutationOptions } = options || {};

const [loginMutation, { isLoading, data, error }] = useMutation(
Expand Down

0 comments on commit 5c7f662

Please sign in to comment.