├── .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 |
19 |
20 | logo 21 |

Hey {isAuthenticated() ? user && user.name : 'you!'}

22 | {isAuthenticated() ? ( 23 | 24 | ) : ( 25 | 26 | )} 27 |
28 |
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 | 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 | * 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 | * 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 ; 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 ; 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 | version 8 | bundle-size 9 | licence 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 | ![](https://i.imgur.com/0k1rWSB.png) 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 | ![](https://i.imgur.com/elsFn7V.png) 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 | ![](https://i.imgur.com/3qk7PzU.png) 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 | 149 | ) : ( 150 | 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 | --------------------------------------------------------------------------------