├── .gitignore
├── .prettierignore
├── examples
└── react-typescript-app
│ ├── src
│ ├── react-app-env.d.ts
│ ├── index.css
│ ├── App.css
│ ├── App.tsx
│ ├── index.tsx
│ ├── logo.svg
│ └── serviceWorker.ts
│ ├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── package.json
│ └── README.md
├── .prettierrc
├── commitlint.config.js
├── src
├── index.ts
├── authReducer.ts
├── AuthProvider.tsx
├── __tests__
│ ├── authReducer.test.ts
│ └── useAuth.test.tsx
└── useAuth.ts
├── jest.config.js
├── tsconfig.json
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | yarn.lock
3 | examples
4 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "es5"
5 | }
6 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["@commitlint/config-conventional"]
3 | };
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { AuthProvider } from './AuthProvider';
2 | export { useAuth } from './useAuth';
3 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qruzz/react-auth-hook/HEAD/examples/react-typescript-app/public/favicon.ico
--------------------------------------------------------------------------------
/examples/react-typescript-app/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qruzz/react-auth-hook/HEAD/examples/react-typescript-app/public/logo192.png
--------------------------------------------------------------------------------
/examples/react-typescript-app/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qruzz/react-auth-hook/HEAD/examples/react-typescript-app/public/logo512.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'jsdom',
4 | testPathIgnorePatterns: ['/node_modules/', '/lib/', '/examples'],
5 | };
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "commonjs",
5 | "declaration": true,
6 | "outDir": "./lib",
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "jsx": "react"
10 | },
11 | "include": ["src"],
12 | "exclude": ["node_modules", "**/__tests__/*"]
13 | }
14 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react"
17 | },
18 | "include": ["src"]
19 | }
20 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | pointer-events: none;
9 | }
10 |
11 | .App-header {
12 | background-color: #282c34;
13 | min-height: 100vh;
14 | display: flex;
15 | flex-direction: column;
16 | align-items: center;
17 | justify-content: center;
18 | font-size: calc(10px + 2vmin);
19 | color: white;
20 | }
21 |
22 | .App-link {
23 | color: #61dafb;
24 | }
25 |
26 | @keyframes App-logo-spin {
27 | from {
28 | transform: rotate(0deg);
29 | }
30 | to {
31 | transform: rotate(360deg);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-typescript-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reach/router": "^1.3.4",
7 | "@types/jest": "26.0.13",
8 | "@types/node": "14.6.3",
9 | "@types/react": "16.9.49",
10 | "@types/react-dom": "16.9.8",
11 | "react": "^16.13.1",
12 | "react-dom": "^16.13.1",
13 | "react-scripts": "3.4.3",
14 | "typescript": "4.0.2"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "browserslist": {
23 | "production": [
24 | ">0.2%",
25 | "not dead",
26 | "not op_mini all"
27 | ],
28 | "development": [
29 | "last 1 chrome version",
30 | "last 1 firefox version",
31 | "last 1 safari version"
32 | ]
33 | },
34 | "devDependencies": {
35 | "@types/reach__router": "^1.3.5"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import logo from './logo.svg';
3 | import { useAuth } from 'react-auth-hook';
4 | import './App.css';
5 | import { RouteComponentProps } from '@reach/router';
6 |
7 | function App({ location }: RouteComponentProps) {
8 | const { login, logout, isAuthenticated, user } = useAuth();
9 |
10 | React.useEffect(() => {
11 | localStorage.setItem(
12 | 'ORIGIN',
13 | `${window.location.href.replace(window.location.origin, '')}`
14 | );
15 | }, []);
16 |
17 | return (
18 |
29 | );
30 | }
31 |
32 | export default App;
33 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { AuthProvider, useAuth } from 'react-auth-hook';
4 | import { navigate, Router, RouteComponentProps } from '@reach/router';
5 | import App from './App';
6 | import * as serviceWorker from './serviceWorker';
7 | import './index.css';
8 |
9 | function AuthCallback({ location }: RouteComponentProps) {
10 | const { handleAuth } = useAuth();
11 |
12 | React.useEffect(() => {
13 | const origin = localStorage.getItem('ORIGIN') || undefined;
14 |
15 | handleAuth(origin);
16 | }, [handleAuth]);
17 |
18 | return (
19 |
28 |
You have reached the callback page - you will now be redirected
29 |
30 | );
31 | }
32 |
33 | ReactDOM.render(
34 |
39 |
40 |
41 |
42 |
43 | ,
44 | document.getElementById('root')
45 | );
46 |
47 | // If you want your app to work offline and load faster, you can change
48 | // unregister() to register() below. Note this comes with some pitfalls.
49 | // Learn more about service workers: https://bit.ly/CRA-PWA
50 | serviceWorker.unregister();
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-auth-hook",
3 | "version": "1.2.0-beta.1",
4 | "description": "A small library for authenticating users in React using Auth0",
5 | "keywords": [
6 | "javascript",
7 | "typescript",
8 | "react",
9 | "auth0"
10 | ],
11 | "author": "Michael Nissen ",
12 | "license": "MIT",
13 | "main": "./lib/index.js",
14 | "types": "./lib/index.d.ts",
15 | "scripts": {
16 | "dev": "tsc --watch",
17 | "build": "tsc",
18 | "test": "jest"
19 | },
20 | "directories": {
21 | "src": "src",
22 | "test": "__tests__"
23 | },
24 | "files": [
25 | "lib/**/*",
26 | "README.md",
27 | "package.json"
28 | ],
29 | "publishConfig": {
30 | "access": "public"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/qruzz/react-auth-hook.git"
35 | },
36 | "bugs": {
37 | "url": "https://github.com/qruzz/react-auth-hook/issues"
38 | },
39 | "devDependencies": {
40 | "@commitlint/cli": "^9.1.2",
41 | "@commitlint/config-conventional": "^9.1.2",
42 | "@testing-library/react": "^11.0.1",
43 | "@types/jest": "^26.0.13",
44 | "@types/react": "^16.9.49",
45 | "husky": "^4.2.5",
46 | "jest": "^26.4.2",
47 | "lint-staged": "^10.3.0",
48 | "prettier": "^2.1.1",
49 | "react": "^16.13.1",
50 | "react-dom": "^16.13.1",
51 | "ts-jest": "^26.3.0",
52 | "typescript": "^4.0.2"
53 | },
54 | "husky": {
55 | "hooks": {
56 | "pre-commit": "lint-staged",
57 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
58 | }
59 | },
60 | "lint-staged": {
61 | "./**/*.{js,jsx,ts,tsx,json,yml,yaml,md,mdx,graphql}": [
62 | "prettier --write"
63 | ]
64 | },
65 | "dependencies": {
66 | "auth0-js": "^9.13.4",
67 | "immer": "^7.0.8",
68 | "use-immer": "^0.4.1",
69 | "@types/auth0-js": "^9.13.4"
70 | },
71 | "peerDependencies": {
72 | "react": "^16.13.1"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/authReducer.ts:
--------------------------------------------------------------------------------
1 | import { Draft } from 'immer';
2 | import { Auth0Error, Auth0UserProfile, Auth0DecodedHash } from 'auth0-js';
3 |
4 | export type Maybe = T | null;
5 |
6 | export type AuthState = {
7 | user: Maybe;
8 | authResult: Maybe;
9 | expiresOn: Maybe;
10 | errorType?: string;
11 | error?: Error | Auth0Error;
12 | };
13 |
14 | export type AuthAction =
15 | | {
16 | type: 'LOGIN_USER';
17 | user: Auth0UserProfile;
18 | authResult: Auth0DecodedHash;
19 | shouldStoreResult?: boolean;
20 | }
21 | | { type: 'LOGOUT_USER' }
22 | | { type: 'AUTH_ERROR'; errorType: string; error: Error | Auth0Error };
23 |
24 | export function authReducer(state: Draft, action: AuthAction) {
25 | switch (action.type) {
26 | case 'LOGIN_USER':
27 | const { authResult, user, shouldStoreResult = false } = action;
28 |
29 | // The time at which the user session expires
30 | const expiresOn = authResult.expiresIn
31 | ? authResult.expiresIn * 1000 + new Date().getTime()
32 | : null;
33 |
34 | if (localStorage) {
35 | localStorage.setItem(
36 | 'react-auth-hook:EXPIRES_ON',
37 | JSON.stringify(expiresOn)
38 | );
39 | localStorage.setItem(
40 | 'react-auth-hook:AUTH0_USER',
41 | JSON.stringify(user)
42 | );
43 | if (shouldStoreResult) {
44 | localStorage.setItem(
45 | 'react-auth-hook:AUTH0_RESULT',
46 | JSON.stringify(authResult)
47 | );
48 | }
49 | }
50 |
51 | return {
52 | user,
53 | expiresOn,
54 | authResult,
55 | };
56 | case 'LOGOUT_USER':
57 | if (localStorage) {
58 | localStorage.removeItem('react-auth-hook:EXPIRES_ON');
59 | localStorage.removeItem('react-auth-hook:AUTH0_USER');
60 | localStorage.removeItem('react-auth-hook:AUTH0_RESULT');
61 | }
62 |
63 | return {
64 | user: null,
65 | expiresOn: null,
66 | authResult: null,
67 | };
68 | case 'AUTH_ERROR':
69 | const { errorType, error } = action;
70 |
71 | return {
72 | user: null,
73 | expiresOn: null,
74 | authResult: null,
75 | errorType,
76 | error,
77 | };
78 | default:
79 | return state;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/AuthProvider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Auth0 from 'auth0-js';
3 | import { useImmerReducer } from 'use-immer';
4 | import { authReducer, AuthState, AuthAction } from './authReducer';
5 | import { handleAuthResult } from './useAuth';
6 |
7 | export interface AuthContext {
8 | state: AuthState;
9 | dispatch: React.Dispatch;
10 | auth0Client: Auth0.WebAuth;
11 | callbackDomain: string;
12 | navigate: any;
13 | }
14 |
15 | export const AuthContext = React.createContext({} as AuthContext);
16 |
17 | export interface AuthProvider {
18 | auth0Domain: string;
19 | auth0ClientId: string;
20 | auth0Params?: Omit<
21 | Auth0.AuthOptions,
22 | | 'domain'
23 | | 'clientId'
24 | | 'redirectUri'
25 | | 'audience'
26 | | 'responseType'
27 | | 'scope'
28 | >;
29 | navigate: any;
30 | shouldStoreResult?: boolean;
31 | children: React.ReactNode;
32 | }
33 |
34 | export function AuthProvider({
35 | auth0Domain,
36 | auth0ClientId,
37 | auth0Params,
38 | navigate,
39 | shouldStoreResult = false,
40 | children,
41 | }: AuthProvider) {
42 | // Holds the initial entry point URL to the page
43 | const callbackDomain = window
44 | ? `${window.location.protocol}//${window.location.host}`
45 | : 'http://localhost:3000';
46 |
47 | const auth0Client = new Auth0.WebAuth({
48 | domain: auth0Domain,
49 | clientID: auth0ClientId,
50 | redirectUri: `${callbackDomain}/auth_callback`,
51 | audience: `https://${auth0Domain}/api/v2/`,
52 | responseType: 'token id_token',
53 | scope: 'openid profile email',
54 | ...auth0Params,
55 | });
56 |
57 | // Reducer for containing the authentication state
58 | const [state, dispatch] = useImmerReducer(
59 | authReducer,
60 | {
61 | user: null,
62 | authResult: null,
63 | expiresOn: null,
64 | }
65 | );
66 |
67 | const [contextValue, setContextValue] = React.useState({
68 | state,
69 | dispatch,
70 | auth0Client,
71 | callbackDomain,
72 | navigate,
73 | });
74 |
75 | // Lift the context value into the parent's state to avoid triggering
76 | // unintentional renders in the consumers
77 | React.useEffect(() => {
78 | setContextValue({ ...contextValue, state });
79 | }, [state]);
80 |
81 | // Check the session to see if a user is authenticated on mount
82 | React.useEffect(() => {
83 | auth0Client.checkSession({}, (error, authResult) => {
84 | if (error) {
85 | dispatch({
86 | type: 'AUTH_ERROR',
87 | errorType: 'checkSession',
88 | error,
89 | });
90 | } else {
91 | handleAuthResult({
92 | dispatch,
93 | auth0Client,
94 | authResult,
95 | shouldStoreResult,
96 | });
97 | }
98 | });
99 | }, []);
100 |
101 | return (
102 | {children}
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/src/__tests__/authReducer.test.ts:
--------------------------------------------------------------------------------
1 | import { Auth0UserProfile } from 'auth0-js';
2 | import { Draft, castDraft } from 'immer';
3 | import { AuthAction, authReducer, AuthState } from '../authReducer';
4 |
5 | const testUser: Auth0UserProfile = {
6 | name: 'test user',
7 | user_id: 'user_id',
8 | nickname: 'test user',
9 | picture: 'picture_url',
10 | sub: 'sub',
11 | clientID: 'client_id',
12 | updated_at: 'updated_at',
13 | created_at: 'created_at',
14 | identities: [],
15 | };
16 |
17 | const EXPIRE_TIME = 500;
18 |
19 | describe('authReducer', () => {
20 | describe('handle login', () => {
21 | beforeEach(() => {
22 | localStorage.removeItem('react-auth-hook:EXPIRES_ON');
23 | localStorage.removeItem('react-auth-hook:AUTH0_USER');
24 | localStorage.removeItem('react-auth-hook:AUTH0_RESULT');
25 | });
26 |
27 | const now = new Date().getTime();
28 |
29 | const state: Draft = castDraft({
30 | user: null,
31 | expiresOn: null,
32 | authResult: null,
33 | });
34 |
35 | const action: AuthAction = {
36 | type: 'LOGIN_USER',
37 | user: testUser,
38 | authResult: { accessToken: 'login_access_token', expiresIn: EXPIRE_TIME },
39 | };
40 |
41 | it('sets the user in state', () => {
42 | expect(authReducer(state, action).user).toEqual(action.user);
43 | });
44 |
45 | it('sets the expiresOn in state', () => {
46 | expect(authReducer(state, action).expiresOn).toBeGreaterThanOrEqual(
47 | now + EXPIRE_TIME * 1000
48 | );
49 | });
50 |
51 | it('sets authResult in state', () => {
52 | expect(authReducer(state, action).authResult).toEqual(action.authResult);
53 | });
54 |
55 | it('stores user in local storage', () => {
56 | authReducer(state, action);
57 |
58 | expect(
59 | JSON.parse(localStorage.getItem('react-auth-hook:EXPIRES_ON')!)
60 | ).toBeGreaterThanOrEqual(now + EXPIRE_TIME * 1000);
61 |
62 | expect(localStorage.getItem('react-auth-hook:AUTH0_USER')).toEqual(
63 | JSON.stringify(action.user)
64 | );
65 | expect(localStorage.getItem('react-auth-hook:AUTH0_RESULT')).toBeNull();
66 | });
67 |
68 | it('stores authResult if shouldStoreResult is true', () => {
69 | const action: AuthAction = {
70 | type: 'LOGIN_USER',
71 | user: testUser,
72 | authResult: {
73 | accessToken: 'login_access_token',
74 | expiresIn: EXPIRE_TIME,
75 | },
76 | shouldStoreResult: true,
77 | };
78 |
79 | authReducer(state, action);
80 |
81 | expect(
82 | JSON.parse(localStorage.getItem('react-auth-hook:EXPIRES_ON')!)
83 | ).toBeGreaterThanOrEqual(now + EXPIRE_TIME * 1000);
84 |
85 | expect(localStorage.getItem('react-auth-hook:AUTH0_USER')).toEqual(
86 | JSON.stringify(action.user)
87 | );
88 | expect(localStorage.getItem('react-auth-hook:AUTH0_RESULT')).toEqual(
89 | JSON.stringify(action.authResult)
90 | );
91 | });
92 | });
93 |
94 | describe('handle logout', () => {
95 | const state: Draft = castDraft({
96 | user: testUser,
97 | expiresOn: new Date().getTime(),
98 | authResult: { accessToken: 'login_access_token', expiresIn: EXPIRE_TIME },
99 | });
100 |
101 | const action: AuthAction = { type: 'LOGOUT_USER' };
102 |
103 | it('clears user in state', () => {
104 | expect(authReducer(state, action).user).toBeNull();
105 | });
106 |
107 | it('clears expiresOn in state', () => {
108 | expect(authReducer(state, action).expiresOn).toBeNull();
109 | });
110 |
111 | it('clears authResult in state', () => {
112 | expect(authReducer(state, action).authResult).toBeNull();
113 | });
114 |
115 | it('removes all auth items from local storage', () => {
116 | localStorage.setItem(
117 | 'react-auth-hook:EXPIRES_ON',
118 | JSON.stringify(state.expiresOn)
119 | );
120 | localStorage.setItem(
121 | 'react-auth-hook:AUTH0_USER',
122 | JSON.stringify(state.user)
123 | );
124 | localStorage.setItem(
125 | 'react-auth-hook:AUTH0_RESULT',
126 | JSON.stringify(state.authResult)
127 | );
128 |
129 | authReducer(state, action);
130 |
131 | expect(
132 | JSON.parse(localStorage.getItem('react-auth-hook:EXPIRES_ON')!)
133 | ).toBeNull();
134 |
135 | expect(localStorage.getItem('react-auth-hook:AUTH0_USER')).toBeNull();
136 | expect(localStorage.getItem('react-auth-hook:AUTH0_RESULT')).toBeNull();
137 | });
138 | });
139 |
140 | describe('handles errors', () => {
141 | const state: Draft = castDraft({
142 | user: testUser,
143 | expiresOn: new Date().getTime(),
144 | authResult: { accessToken: 'login_access_token', expiresIn: EXPIRE_TIME },
145 | });
146 |
147 | const action: AuthAction = {
148 | type: 'AUTH_ERROR',
149 | errorType: 'test_error',
150 | error: new Error(),
151 | };
152 |
153 | it('sets the error type in state', () => {
154 | expect(authReducer(state, action).errorType).toBe('test_error');
155 | });
156 | it('sets the error type in state', () => {
157 | expect(authReducer(state, action).error).toBeDefined();
158 | });
159 | it('clears user in state', () => {
160 | expect(authReducer(state, action).user).toBeNull();
161 | });
162 |
163 | it('clears expiresOn in state', () => {
164 | expect(authReducer(state, action).expiresOn).toBeNull();
165 | });
166 |
167 | it('clears authResult in state', () => {
168 | expect(authReducer(state, action).authResult).toBeNull();
169 | });
170 | });
171 | });
172 |
--------------------------------------------------------------------------------
/examples/react-typescript-app/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl)
112 | .then(response => {
113 | // Ensure service worker exists, and that we really are getting a JS file.
114 | const contentType = response.headers.get('content-type');
115 | if (
116 | response.status === 404 ||
117 | (contentType != null && contentType.indexOf('javascript') === -1)
118 | ) {
119 | // No service worker found. Probably a different app. Reload the page.
120 | navigator.serviceWorker.ready.then(registration => {
121 | registration.unregister().then(() => {
122 | window.location.reload();
123 | });
124 | });
125 | } else {
126 | // Service worker found. Proceed as normal.
127 | registerValidSW(swUrl, config);
128 | }
129 | })
130 | .catch(() => {
131 | console.log(
132 | 'No internet connection found. App is running in offline mode.'
133 | );
134 | });
135 | }
136 |
137 | export function unregister() {
138 | if ('serviceWorker' in navigator) {
139 | navigator.serviceWorker.ready.then(registration => {
140 | registration.unregister();
141 | });
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/useAuth.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Auth0, {
3 | Auth0Error,
4 | Auth0DecodedHash,
5 | Auth0UserProfile,
6 | } from 'auth0-js';
7 | import { AuthAction, Maybe } from './authReducer';
8 | import { AuthContext } from './AuthProvider';
9 |
10 | export interface UseAuth {
11 | login: () => void;
12 | logout: () => void;
13 | handleAuth: (returnRoute?: string, shouldStoreResult?: boolean) => void;
14 | isAuthenticated: () => boolean;
15 | user: Maybe;
16 | authResult: Maybe;
17 | }
18 |
19 | export interface SetAuthSessionOptions extends HandleAuthTokenOptions {
20 | authResult: Auth0DecodedHash;
21 | }
22 |
23 | export interface HandleAuthTokenOptions {
24 | dispatch: React.Dispatch;
25 | error?: Maybe;
26 | auth0Client: Auth0.WebAuth;
27 | authResult: Maybe;
28 | shouldStoreResult?: boolean;
29 | }
30 |
31 | export type AuthResult = {
32 | accessToken: string;
33 | expiresIn: number;
34 | idToken: string;
35 | };
36 |
37 | export function useAuth(): UseAuth {
38 | const {
39 | state,
40 | dispatch,
41 | auth0Client,
42 | callbackDomain,
43 | navigate,
44 | } = React.useContext(AuthContext);
45 |
46 | /**
47 | * Use to redirect to the auth0 hosted login page (`/authorize`) in order to
48 | * initialize a new authN/authZ transaction
49 | *
50 | * @example
51 | * ```
52 | * import { useAuth } from 'react-auth-hook';
53 | *
54 | * const { login } = useAuth();
55 | *
56 | * return (
57 | * Log In
58 | * );
59 | * ```
60 | */
61 | function login() {
62 | auth0Client.authorize();
63 | }
64 |
65 | /**
66 | * Use to log out the user and remove the user and token expiration
67 | * time from localStorage
68 | *
69 | * @example
70 | * ```
71 | * import { logout } from 'react-auth-hook';
72 | *
73 | * const { logout } = useAuth();
74 | *
75 | * return (
76 | * Log Out
77 | * );
78 | * ```
79 | */
80 | function logout() {
81 | auth0Client.logout({ returnTo: callbackDomain });
82 | dispatch({ type: 'LOGOUT_USER' });
83 | navigate('/');
84 | }
85 |
86 | /**
87 | * Use to automatically verify that the returned ID Token's nonce claim is
88 | * the same as the option. It then logs in the user, setting the user in and
89 | * token expiration time in localStorage
90 | *
91 | * @param {string} returnRoute The route to navigate to after authentication
92 | *
93 | * @example
94 | * ```
95 | * import { useAuth } from 'react-auth-hook';
96 | *
97 | * function AuthCallback() {
98 | * const { handleAuth } = useAuth();
99 | *
100 | * React.useEffect(() => {
101 | * const returnRoute = '/some/nested?route';
102 | *
103 | * handleAuth(returnRoute);
104 | * }, [handleAuth]);
105 | *
106 | * return This is the callback page - redirects to returnRoute
107 | * }
108 | * ```
109 | */
110 | function handleAuth(
111 | returnRoute: string = '/',
112 | shouldStoreResult: boolean = false
113 | ) {
114 | if (typeof window !== 'undefined') {
115 | auth0Client.parseHash(async (error, authResult) => {
116 | await handleAuthResult({
117 | error,
118 | auth0Client,
119 | authResult,
120 | dispatch,
121 | shouldStoreResult,
122 | });
123 |
124 | navigate(returnRoute);
125 | });
126 | }
127 | }
128 |
129 | /**
130 | * Use to see if the the JWT token has expired, e.g. wether the user
131 | * is still authenticated
132 | *
133 | * @example
134 | * ```
135 | * import { useAuth } from 'react-auth-hook';
136 | *
137 | * const { isAuthenticated } = useAuth();
138 | *
139 | * return isAuthenticated() ? (
140 | *
Welcome logged in user
141 | * ) : (
142 | * Welcome anonymous user
143 | * )
144 | * ```
145 | */
146 | function isAuthenticated() {
147 | return state.expiresOn ? new Date().getTime() < state.expiresOn : false;
148 | }
149 |
150 | return {
151 | user: state.user,
152 | authResult: state.authResult,
153 | isAuthenticated,
154 | login,
155 | logout,
156 | handleAuth,
157 | };
158 | }
159 |
160 | export async function handleAuthResult({
161 | dispatch,
162 | auth0Client,
163 | error,
164 | authResult,
165 | shouldStoreResult = false,
166 | }: HandleAuthTokenOptions) {
167 | if (authResult && authResult.accessToken && authResult.idToken) {
168 | await setAuthSession({
169 | dispatch,
170 | auth0Client,
171 | authResult,
172 | shouldStoreResult,
173 | });
174 |
175 | return true;
176 | } else if (error) {
177 | dispatch({
178 | type: 'AUTH_ERROR',
179 | errorType: 'handleAuthResult',
180 | error,
181 | });
182 |
183 | return false;
184 | }
185 | }
186 |
187 | async function setAuthSession({
188 | dispatch,
189 | auth0Client,
190 | authResult,
191 | shouldStoreResult,
192 | }: SetAuthSessionOptions) {
193 | return new Promise((resolve: (user: Auth0UserProfile) => void, reject) => {
194 | if (authResult.accessToken) {
195 | auth0Client.client.userInfo(authResult.accessToken, (error, user) => {
196 | if (error) {
197 | dispatch({
198 | type: 'AUTH_ERROR',
199 | errorType: 'userInfo',
200 | error,
201 | });
202 | reject(error);
203 | } else {
204 | dispatch({
205 | type: 'LOGIN_USER',
206 | authResult,
207 | user,
208 | shouldStoreResult,
209 | });
210 | resolve(user);
211 | }
212 | });
213 | }
214 | });
215 | }
216 |
--------------------------------------------------------------------------------
/src/__tests__/useAuth.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Auth0, { Auth0UserProfile } from 'auth0-js';
3 | import { render, fireEvent, screen } from '@testing-library/react';
4 | import { AuthContext } from '../AuthProvider';
5 | import { handleAuthResult, useAuth } from '../useAuth';
6 |
7 | const auth0Client = new Auth0.WebAuth({
8 | domain: 'localhost',
9 | clientID: '12345',
10 | redirectUri: 'localhost/auth0_callback',
11 | audience: 'https://localhost/api/v2/',
12 | responseType: 'token id_token',
13 | scope: 'openid profile email',
14 | });
15 |
16 | auth0Client.authorize = jest.fn();
17 | auth0Client.logout = jest.fn();
18 |
19 | const testUser: Auth0UserProfile = {
20 | name: 'test user',
21 | user_id: 'user_id',
22 | nickname: 'test user',
23 | picture: 'picture_url',
24 | sub: 'sub',
25 | clientID: 'client_id',
26 | updated_at: 'updated_at',
27 | created_at: 'created_at',
28 | identities: [],
29 | };
30 |
31 | const context: AuthContext = {
32 | state: {
33 | user: testUser,
34 | expiresOn: null,
35 | authResult: null,
36 | },
37 | dispatch: jest.fn(),
38 | auth0Client,
39 | callbackDomain: 'localhost',
40 | navigate: jest.fn(),
41 | };
42 |
43 | function renderer(context: AuthContext, Mock: React.ElementType) {
44 | return (
45 |
46 |
47 |
48 | );
49 | }
50 |
51 | describe('useAuth', () => {
52 | describe('login', () => {
53 | const mock = () => {
54 | const { login } = useAuth();
55 |
56 | return log in ;
57 | };
58 |
59 | it('calls auth0.authorize when triggering login', () => {
60 | render(renderer(context, mock));
61 | fireEvent.click(screen.getByText('log in'));
62 | expect(auth0Client.authorize).toBeCalled();
63 | });
64 | });
65 |
66 | describe('logout', () => {
67 | const mock = () => {
68 | const { logout } = useAuth();
69 |
70 | return log out ;
71 | };
72 |
73 | it('calls auth0.logout when triggering logout', () => {
74 | render(renderer(context, mock));
75 | fireEvent.click(screen.getByText('log out'));
76 | expect(auth0Client.logout).toBeCalledWith({
77 | returnTo: context.callbackDomain,
78 | });
79 | });
80 |
81 | it('dispatches LOGOUT_USER action', () => {
82 | render(renderer(context, mock));
83 | fireEvent.click(screen.getByText('log out'));
84 | expect(context.dispatch).toHaveBeenCalledWith({
85 | type: 'LOGOUT_USER',
86 | });
87 | });
88 |
89 | it('navigates to root of callbackDomain', () => {
90 | render(renderer(context, mock));
91 | fireEvent.click(screen.getByText('log out'));
92 | expect(context.navigate).toHaveBeenCalledWith('/');
93 | });
94 | });
95 |
96 | describe('handleAuth', () => {
97 | const mock = (returnRoute?: string) => {
98 | const { handleAuth } = useAuth();
99 |
100 | React.useEffect(() => {
101 | handleAuth(returnRoute);
102 | }, [handleAuth]);
103 |
104 | return this is the callback page - redirects to returnRoute
;
105 | };
106 |
107 | it('navigates to / when no returnRoute is provided', () => {
108 | render(renderer(context, () => mock()));
109 | expect(context.navigate).toHaveBeenCalledWith('/');
110 | });
111 |
112 | // TODO: Fix this, as it seems to still call navigate with '/'
113 | // it('navigates to the returnRoute when provided', () => {
114 | // render(renderer(context, () => mock('/returnRoute')))
115 | // expect(context.navigate).toHaveBeenCalledWith('/returnRoute');
116 | // })
117 | });
118 |
119 | describe('isAuthenticated', () => {
120 | const falseMock = () => {
121 | const { isAuthenticated } = useAuth();
122 | expect(isAuthenticated()).toBe(false);
123 | return null;
124 | };
125 |
126 | const trueMock = () => {
127 | const { isAuthenticated } = useAuth();
128 | expect(isAuthenticated()).toBe(true);
129 | return null;
130 | };
131 |
132 | it('is false when expiresOn is not set', () => {
133 | context.state.expiresOn = null;
134 | render(renderer(context, falseMock));
135 | });
136 |
137 | it('is false when expiresOn is in the past', () => {
138 | context.state.expiresOn = new Date().getTime() - 3600 * 1000;
139 | render(renderer(context, falseMock));
140 | });
141 |
142 | it('is true expiresOn is in the future', () => {
143 | context.state.expiresOn = new Date().getTime() + 3600 * 1000;
144 | render(renderer(context, trueMock));
145 | });
146 | });
147 |
148 | describe('handleAuthResult', () => {
149 | const dispatch = jest.fn((_action: any) => null);
150 |
151 | const authResult = {
152 | accessToken: '12345',
153 | idToken: '12345',
154 | };
155 |
156 | beforeEach(() => {
157 | // mock auth0.client.userInfo for success
158 | auth0Client.client.userInfo = jest.fn((_accessToken, callback) =>
159 | callback(null, testUser)
160 | );
161 | });
162 |
163 | describe('on success', () => {
164 | it('dispatches LOGIN_USER action', async () => {
165 | await handleAuthResult({ dispatch, authResult, auth0Client });
166 |
167 | expect(dispatch).toHaveBeenCalledWith({
168 | type: 'LOGIN_USER',
169 | user: testUser,
170 | authResult,
171 | shouldStoreResult: false,
172 | });
173 | });
174 |
175 | it('returns true', async () => {
176 | expect(
177 | await handleAuthResult({ dispatch, authResult, auth0Client })
178 | ).toBe(true);
179 | });
180 | });
181 |
182 | describe('on error', () => {
183 | const error = (new Error() as unknown) as Auth0.Auth0Error;
184 |
185 | it('dispatches AUTH_ERROR action', async () => {
186 | await handleAuthResult({
187 | dispatch,
188 | authResult: null,
189 | auth0Client,
190 | error,
191 | });
192 |
193 | expect(dispatch).toHaveBeenCalledWith({
194 | type: 'AUTH_ERROR',
195 | errorType: 'handleAuthResult',
196 | error,
197 | });
198 | });
199 |
200 | it('returns false', async () => {
201 | expect(
202 | await handleAuthResult({
203 | dispatch,
204 | authResult: null,
205 | auth0Client,
206 | error,
207 | })
208 | ).toBe(false);
209 | });
210 | });
211 | });
212 | });
213 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
react-auth-hook
3 |
A small library for authenticating users in React using Auth0.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
If the library has has helped you, please consider giving it a ⭐️
13 |
14 |
15 | ## Table of Content
16 |
17 | - [Getting Started](#getting-started)
18 | - [Usage](#usage)
19 | - [Documentation](#documentation)
20 | - [`AuthProvider`](#authprovider)
21 | - [`useAuth`](#useauth)
22 | - [`login`](#login)
23 | - [`logout`](#logout)
24 | - [`handleAuth`](#handleauth)
25 | - [`isAuthenticated`](#isauthenticated)
26 | - [`user`](#user)
27 | - [`authResult`](#authresult)
28 | - [Issues](#issues)
29 |
30 | ## Getting Started
31 |
32 | This module is distributed with [npm](https://www.npmjs.com) which is bundled with [node](https://nodejs.org) and should be installed as one of your projects `dependencies`:
33 |
34 | ```shell
35 | npm install --save react-auth-hook
36 | ```
37 |
38 | or using [yarn](https://yarnpkg.com)
39 |
40 | ```shell
41 | yarn add react-auth-hook
42 | ```
43 |
44 | This library includes `auth0-js` as a `dependency` and requires `react` as a `peerDependency`.
45 |
46 |
47 |
48 | You can find a simple example of a react application with typescript in the [examples](https://github.com/qruzz/react-auth-hook/tree/master/examples) folder in this repository.
49 |
50 | ### Configuring Auth0
51 |
52 | `react-auth-hook` is designed to be quick to setup and easy to use. All it requires is a [Auth0](https://auth0.com/) account with a application set up.
53 |
54 | There are a few required configurations to be done in your Auth0 application to make `react-auth-hook` work properly.
55 |
56 | #### Allowed Callback URLs
57 |
58 | To route back a user after she is authenticated you need to supply a list of URLs that are considered valid. This means that you should add all the URLs which you be authenticating your users from.
59 |
60 | 
61 |
62 | #### Allowed Web Origins
63 |
64 | To allow origins for use with Cross-Origin Authentication you should supply a list of URLs that the authentication request will come from.
65 |
66 | 
67 |
68 | #### Allowed Logout URLs
69 |
70 | After logging out your users will need to be redirected back from Auth0. Provide a list of valid URLs that Auth- can redirect them to with the `returnTo` query parameter.
71 |
72 | 
73 |
74 | ## Usage
75 |
76 | To use this library and the `useAuth` hook, you first need to wrap your application in an `AuthProvider` component to configure the Auth0 client and share state between components.
77 |
78 | ### 1. Configure `AuthProvider`
79 |
80 | In your application, wrap the parts you want to be "hidden" behind your authentication layer in the `AuthProvider` component. I recommend adding it around your root component in the `index.js` file (in React).
81 |
82 | ```js
83 | // src/index.tsx
84 |
85 | import React from 'react';
86 | import ReactDOM from 'react-dom';
87 | import { navigate } from '@reach/router';
88 | import { AuthProvider } from 'react-auth-hook';
89 |
90 | ReactDOM.render(
91 |
96 |
97 | ,
98 | document.getElementById('root')
99 | );
100 | ```
101 |
102 | The `AuthProvider` create the context, sets up an immutable state reducer, and instantiates the Auth0 client.
103 |
104 | ### 2. Handle the Callback
105 |
106 | Auth0 use [OAuth](https://oauth.net/2/) which required you to redirect your users to their login form. After the user has then been authenticated, the provider will redirect the user back to your application.
107 |
108 | The simplest way to handle the callback is to create a page for it:
109 |
110 | ```js
111 | // src/pages/AuthCallback
112 |
113 | import React from 'react';
114 | import { RouteComponentProps } from '@reach/router';
115 | import { useAuth } from 'react-auth-hook';
116 |
117 | export function AuthCallback(props: RouteComponentProps) {
118 | const { handleAuth } = useAuth();
119 |
120 | React.useEffect(() => {
121 | handleAuth();
122 | }, []);
123 |
124 | return (
125 | <>
126 | You have reached the callback page
127 | you will now be redirected
128 | >
129 | );
130 | }
131 | ```
132 |
133 | The purpose of this page is to show some "loading" state and then run the `handleAuth` method from `useAuth` on page load. The function will automatically redirect the user to the root route (`/`).
134 |
135 | ### 3. Authenticating Users
136 |
137 | Now you are done with the hard part that is configuring the Auth0 and the library. Now all that is left to do, is to authenticate your users in your application:
138 |
139 | ```ts
140 | // src/pages/Home
141 |
142 | import React from 'react';
143 | import { useAuth } from 'react-auth-hook';
144 |
145 | export function Home() {
146 | const { isAuthenticated, login, logout } = useAuth();
147 | return isAuthenticated() ? (
148 | log in
149 | ) : (
150 | log out
151 | );
152 | }
153 | ```
154 |
155 | For a full example, check out the [examples](https://github.com/qruzz/react-auth-hook/tree/master/examples) folder in this repository.
156 |
157 | ## Documentation
158 |
159 | ### `AuthProvider`
160 |
161 | The `AuthProvider` component implements the `AuthProvider` interface and takes a number of props to initialise the Auth0 client and more.
162 |
163 | ```ts
164 | interface AuthProvider {
165 | auth0Domain: string;
166 | auth0ClientId: string;
167 | auth0Params?: Omit<
168 | Auth0.AuthOptions,
169 | | 'domain'
170 | | 'clientId'
171 | | 'redirectUri'
172 | | 'audience'
173 | | 'responseType'
174 | | 'scope'
175 | >;
176 | navigate: any;
177 | children: React.ReactNode;
178 | }
179 | ```
180 |
181 | As can be seen from the type interface, the `AuthProvider` API takes a couple of configuration options:
182 |
183 | - `auth0Domain` _the auth domain from your Auth0 application_
184 | - `auth0ClientId` _the client id from your Auth0 application_
185 | - `auth0Params` _additional parameters to pass to `Auth0.WebAuth`_
186 | - `navigate` _your routers navigation function used for redirects_
187 |
188 | #### Default Auth0 Configuration
189 |
190 | `react-auth-hook` infers and sets a few defaults for the configuration parameters required by `auth0-js`:
191 |
192 | ```ts
193 | // AuthProvider.tsx
194 |
195 | const callbackDomain = window
196 | ? `${window.location.protocol}//${window.location.host}`
197 | : 'http://localhost:3000';
198 |
199 | const auth0Client = new Auth0.WebAuth({
200 | domain: auth0Domain,
201 | clientID: auth0ClientId,
202 | redirectUri: `${callbackDomain}/auth_callback`,
203 | audience: `https://${auth0Domain}/api/v2/`,
204 | responseType: 'token id_token',
205 | scope: 'openid profile email',
206 | });
207 | ```
208 |
209 | The `domain` and `clientID` comes from the `AuthProvider` props.
210 |
211 | The `redirectUri` is configured to use the `/auth_callback` page on the current domain which is inferred automatically as can be seen above. Auth0 redirect your users to this page after login so you can set initialise the user session. `useAuth` handles all this for you.
212 |
213 | The `audience` is used for requesting API access and is set to `v2` of the Auth0 API by default.
214 |
215 | The `responseType` specifies which response we want back from the Auth0 API, here being the `token` and `id_token`.
216 |
217 | The `scope` is set here is the default in `auth0-js` as of version 9. It specifies what user resources you will gain access to on successful authentication.
218 |
219 | ### `useAuth`
220 |
221 | The `useAuth` hook implements the `UseAuth` interface and exposes a number of functions and data objects.
222 |
223 | ```ts
224 | interface UseAuth {
225 | login: () => void;
226 | logout: () => void;
227 | handleAuth: (returnRoute?: string) => void;
228 | isAuthenticated: () => boolean;
229 | user: Auth0.Auth0UserProfile | null;
230 | authResult: Auth0.Auth0DecodedHash | null;
231 | }
232 | ```
233 |
234 | #### `login`
235 |
236 | The `login` function calls the `authorize` function from Auth0 and redirects the user to the Auth0 hosted login page (`/authorize`) in order to initialize a new authN/authZ transaction using the [Universal Login]().
237 |
238 | #### `logout`
239 |
240 | The `logout` function calls the similarly named function from Auth0. After a successful logout, the users will be routed to the some-domain URLs that you whitelisted in the Auth0 configuration step.
241 |
242 | #### `handleAuth`
243 |
244 | The `handleAuth` function takes care of - as the name suggests - handling the authentication. The method will create a cookie in local storage with your user's information and redirect back to the homepage (`/`) by default.
245 |
246 | If your users have navigated directly to a nested route within your site, you are probably going to want to redirect them back to that route. Ro redirect to a route other than the homepage, supply the `returnRoute` argument with the associated route. For example, to dynamically redirect to a nested route after authentication, call `handleAuth` like so:
247 |
248 | ```js
249 | handleAuth(window.location.href.replace(window.location.origin, ''));
250 | ```
251 |
252 | #### `isAuthenticated`
253 |
254 | The `isAuthenticated` function returns a `boolean` depending on wether the users is authenticated or not. It utilises the expiration time for the auth token provided by `authResult` returned by a successful login. The `useAuth` reducer sets and read this token in `localStorage`.
255 |
256 | #### `user`
257 |
258 | The `user` object contains the Auth0 user profile returned when the users is successfully authenticated. It implements the `Auth0UserProfile` interface. A detailed description of the interface can be found in the [`auth0-js` types](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/auth0-js/index.d.ts?ts=3#L644).
259 |
260 | #### `authResult`
261 |
262 | The `authResult` object contains the decoded Auth0 hash which is the object returned by the [`parseHash`]() function. It implements the `Auth0DecodedHash` interface which you can see here:
263 |
264 | ```ts
265 | interface Auth0DecodedHash {
266 | accessToken?: string;
267 | idToken?: string;
268 | idTokenPayload?: any;
269 | appState?: any;
270 | refreshToken?: string;
271 | state?: string;
272 | expiresIn?: number;
273 | tokenType?: string;
274 | scope?: string;
275 | }
276 | ```
277 |
278 | ## Issues
279 |
280 | If any issues occur using this library, please fill our a detailed bug report on [GitHub](https://github.com/qruzz/react-auth-hook/issues).
281 |
282 |
283 |
--------------------------------------------------------------------------------