├── .gitignore ├── tsup.config.ts ├── src ├── context.ts ├── hook.ts ├── index.ts ├── types.ts ├── state.ts └── provider.tsx ├── tsconfig.json ├── .github └── workflows │ ├── ci.yml │ ├── coana-analysis.yml │ ├── release.yml │ └── coana-guardrail.yml ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | .idea 4 | .DS_Store 5 | *.log 6 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | format: ["cjs", "esm"], // Build for commonJS and ESmodules 6 | dts: true, // Generate declaration file (.d.ts) 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | }); 11 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Client } from "./types"; 5 | import { State, initialState } from "./state"; 6 | 7 | export interface ContextValue extends Client, State {} 8 | 9 | export const Context = React.createContext( 10 | initialState as ContextValue, 11 | ); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "lib": ["DOM", "ESNext", "DOM.Iterable"], 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "outDir": "./dist", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/hook.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Context } from "./context"; 3 | import { initialState } from "./state"; 4 | 5 | export function useAuth() { 6 | const context = React.useContext(Context); 7 | 8 | if (context === initialState) { 9 | throw new Error("useAuth must be used within an AuthKitProvider"); 10 | } 11 | 12 | return context; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { useAuth } from "./hook"; 2 | export { AuthKitProvider } from "./provider"; 3 | export { 4 | getClaims, 5 | AuthKitError, 6 | LoginRequiredError, 7 | } from "@workos-inc/authkit-js"; 8 | export type { 9 | User, 10 | AuthenticationResponse, 11 | JWTPayload, 12 | OnRefreshResponse, 13 | } from "@workos-inc/authkit-js"; 14 | export type { Impersonator } from "./state"; 15 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@workos-inc/authkit-js"; 2 | 3 | export type Client = Pick< 4 | Awaited>, 5 | | "signIn" 6 | | "signUp" 7 | | "getUser" 8 | | "getAccessToken" 9 | | "signOut" 10 | | "switchToOrganization" 11 | | "getSignInUrl" 12 | | "getSignUpUrl" 13 | >; 14 | 15 | export type CreateClientOptions = NonNullable< 16 | Parameters[1] 17 | >; 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: {} 8 | 9 | jobs: 10 | checks: 11 | name: Pre-merge Checks 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | 19 | - name: Setup 20 | run: npm ci 21 | 22 | - name: Check formatting 23 | run: npm run format:check 24 | 25 | - name: Test build 26 | run: npm run build 27 | -------------------------------------------------------------------------------- /src/state.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@workos-inc/authkit-js"; 2 | 3 | export interface Impersonator { 4 | email: string; 5 | reason: string | null; 6 | } 7 | 8 | export interface State { 9 | isLoading: boolean; 10 | user: User | null; 11 | role: string | null; 12 | roles: string[] | null; 13 | organizationId: string | null; 14 | permissions: string[]; 15 | featureFlags: string[]; 16 | impersonator: Impersonator | null; 17 | } 18 | 19 | export const initialState: State = { 20 | isLoading: true, 21 | user: null, 22 | role: null, 23 | roles: null, 24 | organizationId: null, 25 | permissions: [], 26 | featureFlags: [], 27 | impersonator: null, 28 | }; 29 | -------------------------------------------------------------------------------- /.github/workflows/coana-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Coana Vulnerability Analysis 2 | 3 | on: 4 | schedule: 5 | - cron: "0 3 * * *" # every day at 3 AM 6 | workflow_dispatch: 7 | inputs: 8 | tags: 9 | description: "Manually run vulnerability analysis" 10 | # Required by the return-dispatch action 11 | distinct_id: 12 | 13 | jobs: 14 | coana-vulnerability-analysis: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Run Coana CLI 22 | id: coana-cli 23 | uses: docker://coana/coana:latest 24 | with: 25 | args: | 26 | coana run . \ 27 | --api-key ${{ secrets.COANA_API_KEY }} \ 28 | --repo-url https://github.com/${{github.repository}} 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 WorkOS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@workos-inc/authkit-react", 3 | "version": "0.14.0", 4 | "description": "AuthKit React SDK", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "files": [ 9 | "dist", 10 | "src", 11 | "LICENSE", 12 | "README.md" 13 | ], 14 | "scripts": { 15 | "build": "tsup", 16 | "format": "prettier --write .", 17 | "format:check": "prettier --check .", 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/workos/authkit-react.git" 23 | }, 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/workos/authkit-react/issues" 27 | }, 28 | "homepage": "https://github.com/workos/authkit-react#readme", 29 | "devDependencies": { 30 | "@types/react": "18.3.3", 31 | "prettier": "^3.4.1", 32 | "ts-node": "^10.9.2", 33 | "tsup": "^8.3.5", 34 | "typescript": "5.5.3" 35 | }, 36 | "peerDependencies": { 37 | "react": ">=17" 38 | }, 39 | "dependencies": { 40 | "@workos-inc/authkit-js": "0.14.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | # Support manually pushing a new release 5 | workflow_dispatch: {} 6 | # Trigger when a release is published 7 | release: 8 | types: [published] 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | jobs: 15 | test: 16 | name: Publish to NPM 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | id-token: write 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: 24 26 | registry-url: "https://registry.npmjs.org" 27 | 28 | - name: Install Dependencies 29 | run: | 30 | npm install 31 | 32 | - name: Build project 33 | run: | 34 | npm run build 35 | 36 | - name: Push Release 37 | if: ${{ !github.event.release.prerelease }} 38 | run: | 39 | npm publish --tag latest --access=public --provenance 40 | 41 | - name: Push Pre-Release 42 | if: ${{ github.event.release.prerelease }} 43 | run: | 44 | npm publish --tag next --access=public --provenance 45 | -------------------------------------------------------------------------------- /.github/workflows/coana-guardrail.yml: -------------------------------------------------------------------------------- 1 | name: Coana Guardrail 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | guardrail: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout the ${{github.base_ref}} branch 11 | uses: actions/checkout@v4 12 | with: 13 | ref: ${{github.base_ref}} # checkout the base branch (usually master/main). 14 | 15 | - name: Fetch the PR branch 16 | run: | 17 | git fetch ${{ github.event.pull_request.head.repo.clone_url }} ${{ github.head_ref }}:${{ github.head_ref }} --depth=1 18 | 19 | - name: Get list of changed files relative to the main/master branch 20 | id: changed-files 21 | run: | 22 | echo "all_changed_files=$(git diff --name-only ${{ github.base_ref }} ${{ github.head_ref }} | tr '\n' ' ')" >> $GITHUB_OUTPUT 23 | 24 | - name: Use Node.js 20.x 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 20.x 28 | 29 | - name: Run Coana on the ${{github.base_ref}} branch 30 | run: | 31 | npx @coana-tech/cli run . \ 32 | --guardrail-mode \ 33 | --api-key ${{ secrets.COANA_API_KEY || 'api-key-unavailable' }} \ 34 | -o /tmp/main-branch \ 35 | --changed-files ${{ steps.changed-files.outputs.all_changed_files }} \ 36 | --lightweight-reachability \ 37 | 38 | # Reset file permissions. 39 | # This is necessary because the Coana CLI may add 40 | # new files with root ownership since it's using docker. 41 | # These files will not be deleted by the clean step in checkout 42 | # if the permissions are not reset. 43 | - name: Reset file permissions 44 | run: sudo chown -R $USER:$USER . 45 | 46 | - name: Checkout the current branch 47 | uses: actions/checkout@v4 48 | with: 49 | clean: true 50 | 51 | - name: Run Coana on the current branch 52 | run: | 53 | npx @coana-tech/cli run . \ 54 | --guardrail-mode \ 55 | --api-key ${{ secrets.COANA_API_KEY || 'api-key-unavailable' }} \ 56 | -o /tmp/current-branch \ 57 | --changed-files ${{ steps.changed-files.outputs.all_changed_files }} \ 58 | --lightweight-reachability \ 59 | 60 | - name: Run Report Comparison 61 | run: | 62 | npx @coana-tech/cli compare-reports \ 63 | --api-key ${{ secrets.COANA_API_KEY || 'api-key-unavailable' }} \ 64 | /tmp/main-branch/coana-report.json \ 65 | /tmp/current-branch/coana-report.json 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | -------------------------------------------------------------------------------- /src/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { 5 | createClient, 6 | getClaims, 7 | LoginRequiredError, 8 | OnRefreshResponse, 9 | } from "@workos-inc/authkit-js"; 10 | import { Context } from "./context"; 11 | import { Client, CreateClientOptions } from "./types"; 12 | import { initialState } from "./state"; 13 | 14 | interface AuthKitProviderProps extends CreateClientOptions { 15 | clientId: string; 16 | children: React.ReactNode; 17 | } 18 | 19 | export function AuthKitProvider(props: AuthKitProviderProps) { 20 | const { 21 | clientId, 22 | devMode, 23 | apiHostname, 24 | https, 25 | port, 26 | redirectUri, 27 | children, 28 | onRefresh, 29 | onRefreshFailure, 30 | onRedirectCallback, 31 | refreshBufferInterval, 32 | } = props; 33 | const [client, setClient] = React.useState(NOOP_CLIENT); 34 | const [state, setState] = React.useState(initialState); 35 | 36 | const handleRefresh = React.useCallback( 37 | (response: OnRefreshResponse) => { 38 | const { 39 | user, 40 | accessToken, 41 | organizationId = null, 42 | impersonator = null, 43 | } = response; 44 | const { 45 | role = null, 46 | roles = null, 47 | permissions = [], 48 | feature_flags: featureFlags = [], 49 | } = getClaims(accessToken); 50 | setState((prev) => { 51 | const next = { 52 | ...prev, 53 | user, 54 | organizationId, 55 | role, 56 | roles, 57 | permissions, 58 | featureFlags, 59 | impersonator, 60 | }; 61 | return isEquivalentWorkOSSession(prev, next) ? prev : next; 62 | }); 63 | onRefresh?.(response); 64 | }, 65 | [client], 66 | ); 67 | 68 | React.useEffect(() => { 69 | function initialize() { 70 | const timeoutId = setTimeout(() => { 71 | createClient(clientId, { 72 | apiHostname, 73 | port, 74 | https, 75 | redirectUri, 76 | devMode, 77 | onRedirectCallback, 78 | onRefresh: handleRefresh, 79 | onRefreshFailure, 80 | refreshBufferInterval, 81 | }).then(async (client) => { 82 | const user = client.getUser(); 83 | setClient({ 84 | getAccessToken: client.getAccessToken.bind(client), 85 | getUser: client.getUser.bind(client), 86 | signIn: client.signIn.bind(client), 87 | signUp: client.signUp.bind(client), 88 | signOut: client.signOut.bind(client), 89 | switchToOrganization: client.switchToOrganization.bind(client), 90 | getSignInUrl: client.getSignInUrl.bind(client), 91 | getSignUpUrl: client.getSignUpUrl.bind(client), 92 | }); 93 | setState((prev) => ({ ...prev, isLoading: false, user })); 94 | }); 95 | }); 96 | 97 | return () => { 98 | clearTimeout(timeoutId); 99 | }; 100 | } 101 | 102 | setClient(NOOP_CLIENT); 103 | setState(initialState); 104 | 105 | return initialize(); 106 | }, [clientId, apiHostname, https, port, redirectUri, refreshBufferInterval]); 107 | 108 | return ( 109 | 110 | {children} 111 | 112 | ); 113 | } 114 | 115 | // poor-man's "deep equality" check 116 | function isEquivalentWorkOSSession( 117 | a: typeof initialState, 118 | b: typeof initialState, 119 | ) { 120 | return ( 121 | a.user?.updatedAt === b.user?.updatedAt && 122 | a.organizationId === b.organizationId && 123 | a.role === b.role && 124 | a.roles === b.roles && 125 | a.permissions.length === b.permissions.length && 126 | a.permissions.every((perm, i) => perm === b.permissions[i]) && 127 | a.featureFlags.length === b.featureFlags.length && 128 | a.featureFlags.every((flag, i) => flag === b.featureFlags[i]) && 129 | a.impersonator?.email === b.impersonator?.email && 130 | a.impersonator?.reason === b.impersonator?.reason 131 | ); 132 | } 133 | 134 | const NOOP_CLIENT: Client = { 135 | signIn: async () => {}, 136 | signUp: async () => {}, 137 | getUser: () => null, 138 | getAccessToken: () => Promise.reject(new LoginRequiredError()), 139 | switchToOrganization: () => Promise.resolve(), 140 | signOut: async () => {}, 141 | getSignInUrl: async () => "", 142 | getSignUpUrl: async () => "", 143 | }; 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AuthKit React Library 2 | 3 | ## Installation 4 | 5 | ```bash 6 | npm install @workos-inc/authkit-react 7 | ``` 8 | 9 | or 10 | 11 | ```bash 12 | yarn add @workos-inc/authkit-react 13 | ``` 14 | 15 | ## Setup 16 | 17 | Add your site's URL to the list of allowed origins in the WorkOS dashboard by 18 | clicking on the "Configure CORS" button of the "Authentication" page. 19 | 20 | ## Usage 21 | 22 | ```jsx 23 | import { useAuth, AuthKitProvider } from "@workos-inc/authkit-react"; 24 | 25 | function Root() { 26 | return ( 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | function App() { 34 | const { user, getAccessToken, isLoading, signIn, signUp, signOut } = 35 | useAuth(); 36 | 37 | // This `/login` endpoint should be registered on the "Redirects" page of the 38 | // WorkOS Dashboard. 39 | // In a real app, this code would live in a route instead 40 | // of in the main component 41 | React.useEffect(() => { 42 | if (window.location.pathname === "/login") { 43 | const searchParams = new URLSearchParams(window.location.search); 44 | const context = searchParams.get("context") ?? undefined; 45 | signIn({ context }); 46 | } 47 | }, [window.location, signIn]); 48 | 49 | if (isLoading) { 50 | return "Loading..."; 51 | } 52 | 53 | const performMutation = async () => { 54 | const accessToken = await getAccessToken(); 55 | alert(`API request with accessToken: ${accessToken}`); 56 | }; 57 | 58 | if (user) { 59 | return ( 60 |
61 | Hello, {user.email} 62 |

63 | 70 |

71 |

72 | 73 |

74 |
75 | ); 76 | } 77 | 78 | return ( 79 | <> 80 | {" "} 81 | 82 | 83 | ); 84 | } 85 | ``` 86 | 87 | ## Reference 88 | 89 | ### `` 90 | 91 | Your app should be wrapped in the `AuthKitProvider` component. This component 92 | takes the following props: 93 | 94 | - `clientId` (required): Your `WORKOS_CLIENT_ID` 95 | - `apiHostname`: Defaults to `api.workos.com`. This should be set to your custom Authentication API domain in production. 96 | - `redirectUri`: The url that WorkOS will redirect to upon successful authentication. (Used when constructing sign-in/sign-up URLs). 97 | - `devMode`: Defaults to `true` if window.location is "localhost" or "127.0.0.1". Tokens will be stored in localStorage when this prop is true. 98 | - `onRedirectCallback`: Called after exchanging the 99 | `authorization_code`. Can be used for things like redirecting to a "return 100 | to" path in the OAuth state. 101 | 102 | ### `useAuth` 103 | 104 | The `useAuth` hook returns user information and helper functions: 105 | 106 | - `isLoading`: true while user information is being obtained from fetch during initial load. 107 | - `user`: The WorkOS `User` object for this session. 108 | - `getAccessToken`: Returns an access token. Will fetch a fresh access token if necessary. 109 | - `signIn`: Redirects the user to the Hosted AuthKit sign-in page. Takes an optional `state` argument. 110 | - `signUp`: Redirects the user to the Hosted AuthKit sign-up page. Takes an optional `state` argument. 111 | - `signOut`: Ends the session. 112 | - `switchToOrganization`: Switches to the given organization. Redirects to the hosted login page if switch is unsuccessful. 113 | 114 | The following claims may be populated if the user is part of an organization: 115 | 116 | - `organizationId`: The currently-selected organization. 117 | - `role`: The `role` of the user for the current organization. 118 | - `permissions`: Permissions corresponding to this role. 119 | - `featureFlags`: Enabled feature flags for the current organization. 120 | 121 | ## Passing Data Through Authentication Flows 122 | 123 | When building authentication flows, you often need to maintain state across redirects. For example, you might want to return users to the page they were viewing before login or preserve other application state. AuthKit provides a way to pass and retrieve data through the authentication process. 124 | 125 | ### Using `state` 126 | 127 | `state` is used to pass data that you need to retrieve after authentication completes 128 | 129 | ```tsx 130 | // When signing in, pass your data using the state parameter 131 | function LoginButton() { 132 | return ( 133 | 140 | ); 141 | } 142 | 143 | // Then retrieve your data in the onRedirectCallback 144 | function App() { 145 | return ( 146 | { 150 | // Access your data here 151 | if (state?.returnTo) { 152 | window.location.href = state.returnTo; 153 | } 154 | }} 155 | > 156 | 157 | 158 | ); 159 | } 160 | ``` 161 | 162 | This pattern works with both `signIn` and `signUp` functions. 163 | --------------------------------------------------------------------------------