authkit-react-native 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2026 Joshua Baker
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.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # AuthKit for React Native
2
+
3
+ WorkOS AuthKit integration for React Native. Handles the full OAuth 2.0 PKCE flow, token storage, automatic session restoration, and token refresh — built on Zustand and Expo modules.
4
+
5
+ For server-side auth proxy examples (Hono, Next.js), see the [`examples/`](examples/) directory.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npx expo install authkit-react-native zustand expo-auth-session expo-secure-store expo-web-browser @react-native-async-storage/async-storage
11
+ ```
12
+
13
+ ## Client Usage
14
+
15
+ ### Create the hook
16
+
17
+ Create a `useAuth` hook that your app imports everywhere. The store is created at module scope so the session begins restoring immediately on app launch.
18
+
19
+ ```typescript
20
+ // hooks/useAuth.ts
21
+ import { createAuthStore } from "authkit-react-native";
22
+ import { useStore } from "zustand";
23
+
24
+ const authStore = createAuthStore({
25
+ authorizationEndpoint: `${process.env.EXPO_PUBLIC_API_URL}/v1/auth/authorize`,
26
+ tokenEndpoint: `${process.env.EXPO_PUBLIC_API_URL}/v1/auth/token`,
27
+ revocationEndpoint: `${process.env.EXPO_PUBLIC_API_URL}/v1/auth/revoke`,
28
+ });
29
+
30
+ export function useAuth() {
31
+ return useStore(authStore);
32
+ }
33
+ ```
34
+
35
+ ### Protected routes
36
+
37
+ Because the store lives outside React, there's no provider to wrap your app in. Just call `useAuth()` in a layout to gate routes while the session is loading:
38
+
39
+ ```tsx
40
+ // app/(auth)/_layout.tsx
41
+ import { useAuth } from "@/hooks/useAuth";
42
+ import { Redirect, Stack } from "expo-router";
43
+
44
+ export default function AuthLayout() {
45
+ const { user, isLoading } = useAuth();
46
+
47
+ if (isLoading) return <Loading />;
48
+ if (!user) return <Redirect href="/sign-in" />;
49
+
50
+ return <Stack />;
51
+ }
52
+ ```
53
+
54
+ ### Sign in
55
+
56
+ Call `signIn()` to open the WorkOS AuthKit sign-in page. Returns `true` on success, `false` if the user cancelled.
57
+
58
+ ```tsx
59
+ import { useAuth } from "@/hooks/useAuth";
60
+
61
+ function SignInScreen() {
62
+ const { signIn } = useAuth();
63
+
64
+ return (
65
+ <>
66
+ <Button title="Sign in" onPress={() => signIn()} />
67
+ <Button
68
+ title="Sign up"
69
+ onPress={() => signIn({ screenHint: "sign-up" })}
70
+ />
71
+ </>
72
+ );
73
+ }
74
+ ```
75
+
76
+ ### Sign out
77
+
78
+ `signOut()` revokes the token and clears storage. It does not show any confirmation UI — add that in your app:
79
+
80
+ ```tsx
81
+ import { useAuth } from "@/hooks/useAuth";
82
+ import { Alert } from "react-native";
83
+
84
+ function SignOutButton() {
85
+ const { user, signOut } = useAuth();
86
+
87
+ const handleSignOut = () => {
88
+ Alert.alert(
89
+ user?.email
90
+ ? `Are you sure you want to sign out as ${user.email}?`
91
+ : "Are you sure you want to sign out?",
92
+ undefined,
93
+ [
94
+ { text: "Cancel", style: "cancel" },
95
+ { text: "Sign out", style: "destructive", onPress: () => signOut() },
96
+ ],
97
+ );
98
+ };
99
+
100
+ return <Button title="Sign out" onPress={handleSignOut} />;
101
+ }
102
+ ```
103
+
104
+ ## Bare React Native (without Expo)
105
+
106
+ This package is built around Expo modules but works in any React Native app. Expo packages can be used in bare React Native projects by adding the Expo modules runtime:
107
+
108
+ 1. Install the `expo` package:
109
+ ```bash
110
+ npm install expo
111
+ npx install-expo-modules
112
+ ```
113
+ 2. Install the peer dependencies using `npx expo install` (handles native linking):
114
+ ```bash
115
+ npx expo install expo-auth-session expo-secure-store expo-web-browser @react-native-async-storage/async-storage zustand
116
+ ```
117
+ 3. Ensure your app's URL scheme is configured for the OAuth redirect. In `app.json` or your native project's `Info.plist` / `AndroidManifest.xml`, register a deep link scheme (e.g., `myapp://`).
118
+
119
+ After that, usage is identical to the examples above.
120
+
121
+ ## Configuration
122
+
123
+ | Option | Required | Default | Description |
124
+ | ----------------------- | -------- | ------------------- | ------------------------------------------------------------------------- |
125
+ | `authorizationEndpoint` | \* | WorkOS default | OAuth authorize URL. Required unless `clientId` is provided. |
126
+ | `clientId` | \* | — | WorkOS client ID. Required when using the default authorization endpoint. |
127
+ | `tokenEndpoint` | Yes | — | Token exchange URL. |
128
+ | `revocationEndpoint` | Yes | — | Token revocation URL. |
129
+ | `redirectUri` | No | `makeRedirectUri()` | OAuth redirect URI. |
130
+ | `storageKeyPrefix` | No | `"workos"` | Prefix for SecureStore/AsyncStorage keys. |
131
+ | `devMode` | No | `false` | Logs errors to the console when enabled. |
@@ -0,0 +1,56 @@
1
+ import * as zustand from 'zustand';
2
+
3
+ type ClientIdOrAuthorizationEndpoint = {
4
+ clientId?: string;
5
+ authorizationEndpoint: string;
6
+ } | {
7
+ clientId: string;
8
+ authorizationEndpoint?: string;
9
+ };
10
+ type AuthConfig = ClientIdOrAuthorizationEndpoint & {
11
+ tokenEndpoint: string;
12
+ revocationEndpoint: string;
13
+ redirectUri?: string;
14
+ storageKeyPrefix?: string;
15
+ devMode?: boolean;
16
+ };
17
+ type User = {
18
+ id: string;
19
+ email: string;
20
+ emailVerified: boolean;
21
+ profilePictureUrl: string | null;
22
+ firstName: string | null;
23
+ lastName: string | null;
24
+ lastSignInAt: string | null;
25
+ locale: string | null;
26
+ createdAt: string;
27
+ updatedAt: string;
28
+ externalId: string | null;
29
+ metadata: Record<string, string>;
30
+ };
31
+ type Impersonator = {
32
+ email: string;
33
+ reason: string | null;
34
+ };
35
+ type AuthenticationMethod = "SSO" | "Password" | "Passkey" | "AppleOAuth" | "BitbucketOAuth" | "GitHubOAuth" | "GitLabOAuth" | "GoogleOAuth" | "LinkedInOAuth" | "MicrosoftOAuth" | "SalesforceOAuth" | "VercelOAuth" | "MagicAuth" | "CrossAppAuth" | "Impersonation";
36
+ type AuthSessionInfo = {
37
+ user: User;
38
+ organizationId?: string;
39
+ impersonator?: Impersonator;
40
+ authenticationMethod?: AuthenticationMethod;
41
+ };
42
+ type AuthState = {
43
+ isLoading: boolean;
44
+ } & Partial<AuthSessionInfo> & {
45
+ signIn: (options?: {
46
+ screenHint?: "sign-up" | "sign-in";
47
+ }) => Promise<boolean>;
48
+ signOut: () => Promise<void>;
49
+ getAccessToken: (options?: {
50
+ forceRefresh?: boolean;
51
+ }) => Promise<string | null>;
52
+ };
53
+
54
+ declare function createAuthStore(config: AuthConfig): zustand.StoreApi<AuthState>;
55
+
56
+ export { type AuthConfig, type AuthSessionInfo, type AuthState, type AuthenticationMethod, type Impersonator, type User, createAuthStore };
package/dist/index.js ADDED
@@ -0,0 +1,184 @@
1
+ // src/store.ts
2
+ import {
3
+ AccessTokenRequest,
4
+ AuthRequest,
5
+ makeRedirectUri,
6
+ RefreshTokenRequest,
7
+ RevokeTokenRequest,
8
+ TokenResponse
9
+ } from "expo-auth-session";
10
+ import * as WebBrowser from "expo-web-browser";
11
+ import { createStore } from "zustand";
12
+
13
+ // src/storage.ts
14
+ import AsyncStorage from "@react-native-async-storage/async-storage";
15
+ import * as SecureStore from "expo-secure-store";
16
+ function createStorage(prefix) {
17
+ const tokensKey = `${prefix}.tokens`;
18
+ const metaKey = `${prefix}.meta`;
19
+ return {
20
+ async readTokens() {
21
+ return SecureStore.getItemAsync(tokensKey);
22
+ },
23
+ async writeTokens(value) {
24
+ await SecureStore.setItemAsync(tokensKey, value);
25
+ },
26
+ async readMeta() {
27
+ return AsyncStorage.getItem(metaKey);
28
+ },
29
+ async writeMeta(value) {
30
+ await AsyncStorage.setItem(metaKey, value);
31
+ },
32
+ async clear() {
33
+ await Promise.all([
34
+ SecureStore.deleteItemAsync(tokensKey),
35
+ AsyncStorage.removeItem(metaKey)
36
+ ]);
37
+ }
38
+ };
39
+ }
40
+
41
+ // src/store.ts
42
+ var defaultAuthorizationEndpoint = "https://api.workos.com/user_management/authorize";
43
+ WebBrowser.maybeCompleteAuthSession();
44
+ var emptySession = {
45
+ user: void 0,
46
+ organizationId: void 0,
47
+ impersonator: void 0,
48
+ authenticationMethod: void 0
49
+ };
50
+ function assertAuthSessionInfo(response) {
51
+ if (!response || typeof response !== "object" || !("user" in response)) {
52
+ throw new Error("Invalid authentication response");
53
+ }
54
+ }
55
+ function createAuthStore(config) {
56
+ const {
57
+ clientId = "",
58
+ redirectUri = makeRedirectUri(),
59
+ authorizationEndpoint = defaultAuthorizationEndpoint,
60
+ tokenEndpoint,
61
+ revocationEndpoint,
62
+ storageKeyPrefix = "workos",
63
+ devMode = false
64
+ } = config;
65
+ if (authorizationEndpoint === defaultAuthorizationEndpoint && !clientId) {
66
+ throw new Error(
67
+ "clientId is required when using default authorizationEndpoint"
68
+ );
69
+ }
70
+ const storage = createStorage(storageKeyPrefix);
71
+ WebBrowser.warmUpAsync();
72
+ async function readTokenResponse() {
73
+ const stored = await storage.readTokens();
74
+ if (!stored) return null;
75
+ const requestConfig = JSON.parse(stored);
76
+ return new TokenResponse(requestConfig);
77
+ }
78
+ async function storeTokenResponse(tokenResponse) {
79
+ assertAuthSessionInfo(tokenResponse.rawResponse);
80
+ const requestConfig = tokenResponse.getRequestConfig();
81
+ const { user, organizationId, impersonator, authenticationMethod } = tokenResponse.rawResponse;
82
+ const meta = { user, organizationId, impersonator, authenticationMethod };
83
+ await Promise.all([
84
+ storage.writeTokens(JSON.stringify(requestConfig)),
85
+ storage.writeMeta(JSON.stringify(meta))
86
+ ]);
87
+ return meta;
88
+ }
89
+ const store = createStore()((set) => ({
90
+ isLoading: true,
91
+ signIn: async ({ screenHint = "sign-in" } = {}) => {
92
+ const authSessionRequest = new AuthRequest({
93
+ clientId,
94
+ redirectUri,
95
+ extraParams: { provider: "authkit", screen_hint: screenHint }
96
+ });
97
+ const authSessionResult = await authSessionRequest.promptAsync({
98
+ authorizationEndpoint
99
+ });
100
+ if (authSessionResult.type === "error") {
101
+ throw new Error(
102
+ authSessionResult.error?.description ?? "Unknown error"
103
+ );
104
+ }
105
+ if (authSessionResult.type !== "success") {
106
+ return false;
107
+ }
108
+ if (authSessionRequest.state !== authSessionResult.params.state) {
109
+ throw new Error("State mismatch");
110
+ }
111
+ if (!authSessionRequest.codeVerifier) {
112
+ throw new Error("Code verifier missing");
113
+ }
114
+ const tokenRequest = new AccessTokenRequest({
115
+ code: authSessionResult.params.code,
116
+ clientId,
117
+ redirectUri,
118
+ extraParams: {
119
+ code_verifier: authSessionRequest.codeVerifier
120
+ }
121
+ });
122
+ const tokenResponse = await tokenRequest.performAsync({ tokenEndpoint });
123
+ const meta = await storeTokenResponse(tokenResponse);
124
+ set(meta);
125
+ return true;
126
+ },
127
+ signOut: async () => {
128
+ const tokenResponse = await readTokenResponse();
129
+ if (tokenResponse?.accessToken) {
130
+ const revokeTokenRequest = new RevokeTokenRequest({
131
+ clientId,
132
+ token: tokenResponse.accessToken
133
+ });
134
+ await revokeTokenRequest.performAsync({ revocationEndpoint });
135
+ }
136
+ await storage.clear();
137
+ set(emptySession);
138
+ },
139
+ getAccessToken: async (options) => {
140
+ const forceRefresh = options?.forceRefresh ?? false;
141
+ try {
142
+ let tokenResponse = await readTokenResponse();
143
+ if (!tokenResponse) {
144
+ await storage.clear();
145
+ set(emptySession);
146
+ return null;
147
+ }
148
+ if (forceRefresh || tokenResponse.shouldRefresh()) {
149
+ const refreshTokenRequest = new RefreshTokenRequest({
150
+ refreshToken: tokenResponse.refreshToken,
151
+ clientId
152
+ });
153
+ tokenResponse = await refreshTokenRequest.performAsync({
154
+ tokenEndpoint
155
+ });
156
+ const meta = await storeTokenResponse(tokenResponse);
157
+ set(meta);
158
+ }
159
+ return tokenResponse.accessToken;
160
+ } catch (error) {
161
+ if (devMode) console.error(error);
162
+ await storage.clear();
163
+ set(emptySession);
164
+ return null;
165
+ }
166
+ }
167
+ }));
168
+ (async () => {
169
+ try {
170
+ const storedMeta = await storage.readMeta();
171
+ if (storedMeta !== null) {
172
+ const meta = JSON.parse(storedMeta);
173
+ store.setState(meta);
174
+ }
175
+ await store.getState().getAccessToken({ forceRefresh: true });
176
+ } finally {
177
+ store.setState({ isLoading: false });
178
+ }
179
+ })();
180
+ return store;
181
+ }
182
+ export {
183
+ createAuthStore
184
+ };
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "authkit-react-native",
3
+ "version": "0.1.0",
4
+ "description": "WorkOS AuthKit integration for React Native — OAuth 2.0 PKCE auth with token storage, session restoration, and automatic refresh",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "main": "./dist/index.js",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE.md"
20
+ ],
21
+ "publishConfig": {
22
+ "provenance": true
23
+ },
24
+ "simple-git-hooks": {
25
+ "pre-commit": "pnpm lint-staged"
26
+ },
27
+ "lint-staged": {
28
+ "*": "prettier --write --ignore-unknown"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/joshuabaker/authkit-react-native.git"
33
+ },
34
+ "homepage": "https://github.com/joshuabaker/authkit-react-native#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/joshuabaker/authkit-react-native/issues"
37
+ },
38
+ "keywords": [
39
+ "workos",
40
+ "authkit",
41
+ "react-native",
42
+ "expo",
43
+ "oauth",
44
+ "pkce",
45
+ "authentication",
46
+ "zustand"
47
+ ],
48
+ "peerDependencies": {
49
+ "@react-native-async-storage/async-storage": ">=2",
50
+ "expo-auth-session": ">=5",
51
+ "expo-secure-store": ">=13",
52
+ "expo-web-browser": ">=13",
53
+ "react": ">=18",
54
+ "zustand": ">=4"
55
+ },
56
+ "devDependencies": {
57
+ "@changesets/changelog-github": "^0.6.0",
58
+ "@changesets/cli": "^2.30.0",
59
+ "@react-native-async-storage/async-storage": "^2.2.0",
60
+ "@vitest/coverage-v8": "^4.1.0",
61
+ "expo-auth-session": "^55.0.7",
62
+ "expo-secure-store": "^55.0.8",
63
+ "expo-web-browser": "^55.0.9",
64
+ "lint-staged": "^16.1.0",
65
+ "prettier": "^3.8.1",
66
+ "react": "^19.2.0",
67
+ "simple-git-hooks": "^2.13.0",
68
+ "tsup": "^8.4.0",
69
+ "typescript": "^5.0.0",
70
+ "vitest": "^4.1.0",
71
+ "zustand": "^5.0.12"
72
+ },
73
+ "scripts": {
74
+ "build": "tsup",
75
+ "format": "prettier --write .",
76
+ "format:check": "prettier --check .",
77
+ "test": "vitest run",
78
+ "test:watch": "vitest",
79
+ "test:coverage": "vitest run --coverage",
80
+ "type-check": "tsc --noEmit",
81
+ "changeset": "changeset",
82
+ "version": "changeset version",
83
+ "release": "changeset publish"
84
+ }
85
+ }