@witsauth/react-native-client 0.0.0-stage → 1.0.1
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/README.md +194 -2
- package/dist/axios/interceptor.d.ts +13 -0
- package/dist/core/AuthClient.d.ts +48 -0
- package/dist/core/browser.d.ts +29 -0
- package/dist/core/pkce.d.ts +17 -0
- package/dist/core/storage.d.ts +51 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.es.js +711 -0
- package/dist/index.umd.js +1 -0
- package/dist/models/types.d.ts +131 -0
- package/dist/react/AuthContext.d.ts +30 -0
- package/package.json +61 -3
package/README.md
CHANGED
|
@@ -1,3 +1,195 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @witsauth/react-native-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official React Native SDK for **WitsAuth** OAuth 2.0 + PKCE authentication, token lifecycle management, Axios interceptor integration, and hosted Account Management on **Android** and **iOS** (supporting both **Expo** and **Bare React Native CLI**).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **RFC 8252 System Browser OAuth 2.0 + PKCE (`S256`)**: Uses `ASWebAuthenticationSession` on iOS and Chrome Custom Tabs on Android.
|
|
10
|
+
- **Hermes-Ready Pure-TS SHA-256 PKCE**: Works out-of-the-box on React Native Hermes without requiring native Web Crypto polyfills.
|
|
11
|
+
- **Hardware-Backed Secure Token Storage**: Built-in adapters for `expo-secure-store` (Expo) and `react-native-keychain` (Bare React Native CLI), backed by iOS Keychain and Android Keystore.
|
|
12
|
+
- **Automatic Token Refresh**: Proactively refreshes expiring tokens and refreshes automatically when the app returns to the foreground (`AppState`).
|
|
13
|
+
- **Hosted Account Management (`/auth/account`)**: Allows users to change their password, update their profile, and verify their mobile phone number via OTP using a single-use handoff ticket (`navigateToAccountManagement()`).
|
|
14
|
+
- **Axios Interceptor**: Automatically attaches `Authorization: Bearer <token>` and queues/retries concurrent `401 Unauthorized` requests.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. WitsAuth Console Configuration
|
|
19
|
+
|
|
20
|
+
1. Open the **WitsAuth Console** and create or edit a **Regular Web / Mobile Application**.
|
|
21
|
+
2. Set **Framework** to **React Native (Mobile)**.
|
|
22
|
+
3. Add your mobile deep-link callback URI under **Allowed Callback URLs**, for example:
|
|
23
|
+
```text
|
|
24
|
+
myapp://auth/callback
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Installation & Setup
|
|
30
|
+
|
|
31
|
+
### Option A: Expo (Managed or Prebuild Workflow)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx expo install @witsauth/react-native-client expo-web-browser expo-secure-store
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Configure your custom scheme in `app.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"expo": {
|
|
42
|
+
"name": "My App",
|
|
43
|
+
"slug": "my-app",
|
|
44
|
+
"scheme": "myapp"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Initialize `AuthProvider` in your app root:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import React from 'react';
|
|
53
|
+
import { AppState } from 'react-native';
|
|
54
|
+
import * as WebBrowser from 'expo-web-browser';
|
|
55
|
+
import * as SecureStore from 'expo-secure-store';
|
|
56
|
+
import {
|
|
57
|
+
AuthProvider,
|
|
58
|
+
createExpoSecureStoreAdapter,
|
|
59
|
+
createExpoWebBrowserLauncher,
|
|
60
|
+
OAuth2NativeConfig
|
|
61
|
+
} from '@witsauth/react-native-client';
|
|
62
|
+
|
|
63
|
+
WebBrowser.maybeCompleteAuthSession();
|
|
64
|
+
|
|
65
|
+
const authConfig: OAuth2NativeConfig = {
|
|
66
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
67
|
+
authorizationEndpoint: 'https://your-tenant.witsauth.com/auth/authorize',
|
|
68
|
+
tokenEndpoint: 'https://your-tenant.witsauth.com/oauth/token',
|
|
69
|
+
revokeEndpoint: 'https://your-tenant.witsauth.com/oauth/revoke',
|
|
70
|
+
redirectUri: 'myapp://auth/callback',
|
|
71
|
+
scope: 'openid profile email offline_access',
|
|
72
|
+
storage: createExpoSecureStoreAdapter(SecureStore),
|
|
73
|
+
openAuthSession: createExpoWebBrowserLauncher(WebBrowser)
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default function App() {
|
|
77
|
+
return (
|
|
78
|
+
<AuthProvider
|
|
79
|
+
config={authConfig}
|
|
80
|
+
subscribeAppState={(listener) => {
|
|
81
|
+
const sub = AppState.addEventListener('change', listener);
|
|
82
|
+
return () => sub.remove();
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{/* Your App Navigation */}
|
|
86
|
+
</AuthProvider>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### Option B: Bare React Native CLI (Android & iOS)
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm install @witsauth/react-native-client react-native-inappbrowser-reborn react-native-keychain
|
|
97
|
+
cd ios && pod install
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Android Deep-Link Configuration (`android/app/src/main/AndroidManifest.xml`)
|
|
101
|
+
|
|
102
|
+
Add an `<intent-filter>` inside your main `<activity>`:
|
|
103
|
+
|
|
104
|
+
```xml
|
|
105
|
+
<intent-filter>
|
|
106
|
+
<action android:name="android.intent.action.VIEW" />
|
|
107
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
108
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
109
|
+
<data android:scheme="myapp" android:host="auth" android:pathPrefix="/callback" />
|
|
110
|
+
</intent-filter>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### iOS Deep-Link Configuration (`ios/<YourApp>/Info.plist`)
|
|
114
|
+
|
|
115
|
+
Register `CFBundleURLTypes`:
|
|
116
|
+
|
|
117
|
+
```xml
|
|
118
|
+
<key>CFBundleURLTypes</key>
|
|
119
|
+
<array>
|
|
120
|
+
<dict>
|
|
121
|
+
<key>CFBundleTypeRole</key>
|
|
122
|
+
<string>Editor</string>
|
|
123
|
+
<key>CFBundleURLName</key>
|
|
124
|
+
<string>com.yourcompany.myapp</string>
|
|
125
|
+
<key>CFBundleURLSchemes</key>
|
|
126
|
+
<array>
|
|
127
|
+
<string>myapp</string>
|
|
128
|
+
</array>
|
|
129
|
+
</dict>
|
|
130
|
+
</array>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Initialize `AuthProvider` with `react-native-keychain` and `react-native-inappbrowser-reborn`:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import React from 'react';
|
|
137
|
+
import { AppState } from 'react-native';
|
|
138
|
+
import InAppBrowser from 'react-native-inappbrowser-reborn';
|
|
139
|
+
import * as Keychain from 'react-native-keychain';
|
|
140
|
+
import {
|
|
141
|
+
AuthProvider,
|
|
142
|
+
createKeychainStorageAdapter,
|
|
143
|
+
createInAppBrowserLauncher,
|
|
144
|
+
OAuth2NativeConfig
|
|
145
|
+
} from '@witsauth/react-native-client';
|
|
146
|
+
|
|
147
|
+
const authConfig: OAuth2NativeConfig = {
|
|
148
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
149
|
+
authorizationEndpoint: 'https://your-tenant.witsauth.com/auth/authorize',
|
|
150
|
+
tokenEndpoint: 'https://your-tenant.witsauth.com/oauth/token',
|
|
151
|
+
revokeEndpoint: 'https://your-tenant.witsauth.com/oauth/revoke',
|
|
152
|
+
redirectUri: 'myapp://auth/callback',
|
|
153
|
+
scope: 'openid profile email offline_access',
|
|
154
|
+
storage: createKeychainStorageAdapter(Keychain),
|
|
155
|
+
openAuthSession: createInAppBrowserLauncher(InAppBrowser)
|
|
156
|
+
};
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 3. Using `useAuth()` for Login, Logout & Account Management
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import React from 'react';
|
|
165
|
+
import { View, Button, Text } from 'react-native';
|
|
166
|
+
import { useAuth } from '@witsauth/react-native-client';
|
|
167
|
+
|
|
168
|
+
export function ProfileScreen() {
|
|
169
|
+
const {
|
|
170
|
+
isAuthenticated,
|
|
171
|
+
isLoading,
|
|
172
|
+
login,
|
|
173
|
+
logout,
|
|
174
|
+
navigateToAccountManagement
|
|
175
|
+
} = useAuth();
|
|
176
|
+
|
|
177
|
+
if (isLoading) {
|
|
178
|
+
return <Text>Loading...</Text>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!isAuthenticated) {
|
|
182
|
+
return <Button title="Sign In with WitsAuth" onPress={login} />;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<View>
|
|
187
|
+
<Button
|
|
188
|
+
title="Manage Account (Password & Phone)"
|
|
189
|
+
onPress={navigateToAccountManagement}
|
|
190
|
+
/>
|
|
191
|
+
<Button title="Sign Out" onPress={logout} />
|
|
192
|
+
</View>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AxiosInstance } from 'axios';
|
|
2
|
+
import { AuthClient } from '../core/AuthClient';
|
|
3
|
+
export interface InterceptorOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Optional callback triggered when token refresh fails completely (e.g., refresh token expired).
|
|
6
|
+
*/
|
|
7
|
+
onUnauthorized?: () => void;
|
|
8
|
+
/**
|
|
9
|
+
* Specify URLs that should NOT have the Authorization header injected.
|
|
10
|
+
*/
|
|
11
|
+
excludeUrls?: (string | RegExp)[];
|
|
12
|
+
}
|
|
13
|
+
export declare function setupAxiosInterceptors(axiosInstance: AxiosInstance, authClient: AuthClient, options?: InterceptorOptions): () => void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AuthorizeOptions, LogoutOptions, OAuth2NativeConfig, TokenInfo, UserProfile } from '../models/types';
|
|
2
|
+
import { NativeTokenStorage } from './storage';
|
|
3
|
+
export declare class AuthClient {
|
|
4
|
+
private config;
|
|
5
|
+
private storage;
|
|
6
|
+
private refreshPromise;
|
|
7
|
+
constructor(config: OAuth2NativeConfig, storage?: NativeTokenStorage);
|
|
8
|
+
/**
|
|
9
|
+
* Hydrates the in-memory token cache from the underlying async secure storage.
|
|
10
|
+
* Called automatically by `AuthProvider` on mount.
|
|
11
|
+
*/
|
|
12
|
+
initialize(): Promise<void>;
|
|
13
|
+
log(level: 'error' | 'warn' | 'info' | 'debug', ...args: unknown[]): void;
|
|
14
|
+
/**
|
|
15
|
+
* Builds the OAuth 2.0 + PKCE authorization URL and persists `code_verifier`, `state`, and `nonce`.
|
|
16
|
+
*/
|
|
17
|
+
buildAuthorizationUrl(options?: AuthorizeOptions): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Launches the system authentication browser (`ASWebAuthenticationSession` on iOS / Chrome Custom Tabs on Android),
|
|
20
|
+
* intercepts the deep-link callback (`redirectUri`), validates `state`, and exchanges `code` + `code_verifier` for tokens.
|
|
21
|
+
*/
|
|
22
|
+
authorize(options?: AuthorizeOptions): Promise<TokenInfo>;
|
|
23
|
+
/**
|
|
24
|
+
* Parses a redirect callback URL (`myapp://auth/callback?code=...&state=...`) and exchanges the code for tokens.
|
|
25
|
+
*/
|
|
26
|
+
handleCallbackUrl(callbackUrl: string): Promise<TokenInfo>;
|
|
27
|
+
exchangeCodeForTokens(code: string, state: string): Promise<TokenInfo>;
|
|
28
|
+
refreshAccessToken(): Promise<TokenInfo>;
|
|
29
|
+
private saveTokenInfo;
|
|
30
|
+
getAccessToken(): string | null;
|
|
31
|
+
getIdToken(): string | null;
|
|
32
|
+
getUserProfile<T extends UserProfile = UserProfile>(): T | null;
|
|
33
|
+
getRefreshToken(): string | null;
|
|
34
|
+
getExpiresAt(): number | null;
|
|
35
|
+
isAuthenticated(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Opens the hosted WitsAuth Account Management portal (`/auth/account`) in the native secure browser
|
|
38
|
+
* using a short-lived single-use ticket (`POST /auth/account/ticket`), allowing the user to change
|
|
39
|
+
* their password, update their profile, or verify their mobile phone number via OTP.
|
|
40
|
+
*/
|
|
41
|
+
navigateToAccountManagement(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Clears tokens from secure storage and revokes both refresh_token and access_token at `revokeEndpoint`.
|
|
44
|
+
* If `options.clearBrowserSession` is true and `logoutEndpoint` + `openAuthSession` are configured,
|
|
45
|
+
* launches the system browser to terminate the WitsAuth web session cookie.
|
|
46
|
+
*/
|
|
47
|
+
logout(options?: LogoutOptions): Promise<void>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BrowserSessionLauncher } from '../models/types';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a `BrowserSessionLauncher` using Expo's `expo-web-browser` module.
|
|
4
|
+
*/
|
|
5
|
+
export declare function createExpoWebBrowserLauncher(webBrowserModule: {
|
|
6
|
+
openAuthSessionAsync: (url: string, redirectUrl?: string) => Promise<{
|
|
7
|
+
type: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
}>;
|
|
10
|
+
}): BrowserSessionLauncher;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a `BrowserSessionLauncher` using `react-native-inappbrowser-reborn` for Bare React Native CLI apps.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createInAppBrowserLauncher(inAppBrowserModule: {
|
|
15
|
+
isAvailable: () => Promise<boolean>;
|
|
16
|
+
openAuth: (url: string, redirectUrl: string, options?: Record<string, unknown>) => Promise<{
|
|
17
|
+
type: string;
|
|
18
|
+
url?: string;
|
|
19
|
+
}>;
|
|
20
|
+
}): BrowserSessionLauncher;
|
|
21
|
+
/**
|
|
22
|
+
* Parses `code`, `state`, `error`, and `error_description` from a custom-scheme or HTTPS callback URL.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseCallbackUrl(callbackUrl: string): {
|
|
25
|
+
code: string | null;
|
|
26
|
+
state: string | null;
|
|
27
|
+
error: string | null;
|
|
28
|
+
errorDescription: string | null;
|
|
29
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermes-compatible PKCE (RFC 7636) utilities.
|
|
3
|
+
* Uses Web Crypto API when available, and falls back to a pure-TypeScript
|
|
4
|
+
* FIPS 180-4 SHA-256 implementation so S256 works out-of-the-box on React Native
|
|
5
|
+
* Hermes without requiring native crypto polyfills.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Pure-TypeScript FIPS 180-4 SHA-256 digest.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sha256Bytes(asciiOrUtf8: string): Uint8Array;
|
|
11
|
+
export declare function base64UrlEncode(bytes: Uint8Array): string;
|
|
12
|
+
export declare function getRandomBytes(length: number): Uint8Array;
|
|
13
|
+
export declare function decodeJwtPayload<T = Record<string, unknown>>(token: string | null | undefined): T | null;
|
|
14
|
+
export declare function generateRandomString(length?: number): string;
|
|
15
|
+
export declare function generateCodeVerifier(): string;
|
|
16
|
+
export declare function generateState(): string;
|
|
17
|
+
export declare function createCodeChallenge(verifier: string): Promise<string>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { OAuth2AsyncStorage } from '../models/types';
|
|
2
|
+
/**
|
|
3
|
+
* Wraps an asynchronous native secure store (e.g., Expo SecureStore or React Native Keychain)
|
|
4
|
+
* with a synchronous in-memory read cache. Once `hydrate()` resolves on startup,
|
|
5
|
+
* `getAccessToken()` and `isAuthenticated()` are synchronous while all mutations
|
|
6
|
+
* persist asynchronously to the native keychain/keystore.
|
|
7
|
+
*/
|
|
8
|
+
export declare class NativeTokenStorage {
|
|
9
|
+
private backend;
|
|
10
|
+
private cache;
|
|
11
|
+
private hydrated;
|
|
12
|
+
private hydratePromise;
|
|
13
|
+
constructor(customStorage?: OAuth2AsyncStorage);
|
|
14
|
+
hydrate(): Promise<void>;
|
|
15
|
+
getItemSync(key: string): string | null;
|
|
16
|
+
getItem(key: string): Promise<string | null>;
|
|
17
|
+
setItem(key: string, value: string): Promise<void>;
|
|
18
|
+
removeItem(key: string): Promise<void>;
|
|
19
|
+
clearTemporary(): Promise<void>;
|
|
20
|
+
clearTokens(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export declare function createMemoryStorageAdapter(): OAuth2AsyncStorage;
|
|
23
|
+
/**
|
|
24
|
+
* Helper to create a storage adapter backed by `expo-secure-store`.
|
|
25
|
+
* Automatically splits values exceeding 2000 bytes across indexed chunk keys
|
|
26
|
+
* to prevent Expo SecureStore's 2048-byte limit errors on large JWTs.
|
|
27
|
+
* Pass the imported `SecureStore` module from `expo-secure-store`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createExpoSecureStoreAdapter(secureStoreModule: {
|
|
30
|
+
getItemAsync: (key: string) => Promise<string | null>;
|
|
31
|
+
setItemAsync: (key: string, value: string) => Promise<void>;
|
|
32
|
+
deleteItemAsync: (key: string) => Promise<void>;
|
|
33
|
+
}): OAuth2AsyncStorage;
|
|
34
|
+
/**
|
|
35
|
+
* Helper to create a storage adapter backed by `react-native-keychain` for Bare React Native CLI apps.
|
|
36
|
+
* Pass the imported `Keychain` module from `react-native-keychain`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createKeychainStorageAdapter(keychainModule: {
|
|
39
|
+
getGenericPassword: (options?: {
|
|
40
|
+
service?: string;
|
|
41
|
+
}) => Promise<false | {
|
|
42
|
+
username: string;
|
|
43
|
+
password: string;
|
|
44
|
+
}>;
|
|
45
|
+
setGenericPassword: (username: string, password: string, options?: {
|
|
46
|
+
service?: string;
|
|
47
|
+
}) => Promise<unknown>;
|
|
48
|
+
resetGenericPassword: (options?: {
|
|
49
|
+
service?: string;
|
|
50
|
+
}) => Promise<unknown>;
|
|
51
|
+
}, servicePrefix?: string): OAuth2AsyncStorage;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './models/types';
|
|
2
|
+
export { AuthClient } from './core/AuthClient';
|
|
3
|
+
export { NativeTokenStorage, createMemoryStorageAdapter, createExpoSecureStoreAdapter, createKeychainStorageAdapter } from './core/storage';
|
|
4
|
+
export { createExpoWebBrowserLauncher, createInAppBrowserLauncher, parseCallbackUrl } from './core/browser';
|
|
5
|
+
export * from './core/pkce';
|
|
6
|
+
export { AuthProvider, useAuth } from './react/AuthContext';
|
|
7
|
+
export type { AuthContextType, AuthProviderProps } from './react/AuthContext';
|
|
8
|
+
export { setupAxiosInterceptors } from './axios/interceptor';
|
|
9
|
+
export type { InterceptorOptions } from './axios/interceptor';
|
package/dist/index.es.js
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import { createContext as e, useCallback as t, useContext as n, useEffect as r, useState as i } from "react";
|
|
2
|
+
import { jsx as a } from "react/jsx-runtime";
|
|
3
|
+
// src/models/types.ts
|
|
4
|
+
var o = {
|
|
5
|
+
ACCESS_TOKEN: "witsauth_access_token",
|
|
6
|
+
REFRESH_TOKEN: "witsauth_refresh_token",
|
|
7
|
+
TOKEN_EXPIRES_AT: "witsauth_expires_at",
|
|
8
|
+
TOKEN_TYPE: "witsauth_token_type",
|
|
9
|
+
SCOPE: "witsauth_scope",
|
|
10
|
+
ID_TOKEN: "witsauth_id_token",
|
|
11
|
+
CODE_VERIFIER: "witsauth_code_verifier",
|
|
12
|
+
STATE: "witsauth_state",
|
|
13
|
+
NONCE: "witsauth_nonce"
|
|
14
|
+
}, s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", c = new Uint32Array([
|
|
15
|
+
1116352408,
|
|
16
|
+
1899447441,
|
|
17
|
+
3049323471,
|
|
18
|
+
3921009573,
|
|
19
|
+
961987163,
|
|
20
|
+
1508970993,
|
|
21
|
+
2453635748,
|
|
22
|
+
2870763221,
|
|
23
|
+
3624381080,
|
|
24
|
+
310598401,
|
|
25
|
+
607225278,
|
|
26
|
+
1426881987,
|
|
27
|
+
1925078388,
|
|
28
|
+
2162078206,
|
|
29
|
+
2614888103,
|
|
30
|
+
3248222580,
|
|
31
|
+
3835390401,
|
|
32
|
+
4022224774,
|
|
33
|
+
264347078,
|
|
34
|
+
604807628,
|
|
35
|
+
770255983,
|
|
36
|
+
1249150122,
|
|
37
|
+
1555081692,
|
|
38
|
+
1996064986,
|
|
39
|
+
2554220882,
|
|
40
|
+
2821834349,
|
|
41
|
+
2952996808,
|
|
42
|
+
3210313671,
|
|
43
|
+
3336571891,
|
|
44
|
+
3584528711,
|
|
45
|
+
113926993,
|
|
46
|
+
338241895,
|
|
47
|
+
666307205,
|
|
48
|
+
773529912,
|
|
49
|
+
1294757372,
|
|
50
|
+
1396182291,
|
|
51
|
+
1695183700,
|
|
52
|
+
1986661051,
|
|
53
|
+
2177026350,
|
|
54
|
+
2456956037,
|
|
55
|
+
2730485921,
|
|
56
|
+
2820302411,
|
|
57
|
+
3259730800,
|
|
58
|
+
3345764771,
|
|
59
|
+
3516065817,
|
|
60
|
+
3600352804,
|
|
61
|
+
4094571909,
|
|
62
|
+
275423344,
|
|
63
|
+
430227734,
|
|
64
|
+
506948616,
|
|
65
|
+
659060556,
|
|
66
|
+
883997877,
|
|
67
|
+
958139571,
|
|
68
|
+
1322822218,
|
|
69
|
+
1537002063,
|
|
70
|
+
1747873779,
|
|
71
|
+
1955562222,
|
|
72
|
+
2024104815,
|
|
73
|
+
2227730452,
|
|
74
|
+
2361852424,
|
|
75
|
+
2428436474,
|
|
76
|
+
2756734187,
|
|
77
|
+
3204031479,
|
|
78
|
+
3329325298
|
|
79
|
+
]);
|
|
80
|
+
function l(e, t) {
|
|
81
|
+
return t >>> e | t << 32 - e;
|
|
82
|
+
}
|
|
83
|
+
function u(e) {
|
|
84
|
+
if (typeof TextEncoder < "u") return new TextEncoder().encode(e);
|
|
85
|
+
let t = [];
|
|
86
|
+
for (let n = 0; n < e.length; n++) {
|
|
87
|
+
let r = e.charCodeAt(n);
|
|
88
|
+
r < 128 ? t.push(r) : r < 2048 ? t.push(192 | r >> 6, 128 | r & 63) : r < 55296 || r >= 57344 ? t.push(224 | r >> 12, 128 | r >> 6 & 63, 128 | r & 63) : (n++, r = 65536 + ((r & 1023) << 10 | e.charCodeAt(n) & 1023), t.push(240 | r >> 18, 128 | r >> 12 & 63, 128 | r >> 6 & 63, 128 | r & 63));
|
|
89
|
+
}
|
|
90
|
+
return new Uint8Array(t);
|
|
91
|
+
}
|
|
92
|
+
function d(e) {
|
|
93
|
+
let t = u(e), n = t.length * 8, r = t.length + 9 + 63 >> 6 << 6, i = new Uint8Array(r);
|
|
94
|
+
i.set(t), i[t.length] = 128;
|
|
95
|
+
let a = new DataView(i.buffer);
|
|
96
|
+
a.setUint32(r - 8, Math.floor(n / 4294967296), !1), a.setUint32(r - 4, n >>> 0, !1);
|
|
97
|
+
let o = 1779033703, s = 3144134277, d = 1013904242, f = 2773480762, p = 1359893119, m = 2600822924, h = 528734635, g = 1541459225, _ = /* @__PURE__ */ new Uint32Array(64);
|
|
98
|
+
for (let e = 0; e < r; e += 64) {
|
|
99
|
+
for (let t = 0; t < 16; t++) _[t] = a.getUint32(e + t * 4, !1);
|
|
100
|
+
for (let e = 16; e < 64; e++) {
|
|
101
|
+
let t = l(7, _[e - 15]) ^ l(18, _[e - 15]) ^ _[e - 15] >>> 3, n = l(17, _[e - 2]) ^ l(19, _[e - 2]) ^ _[e - 2] >>> 10;
|
|
102
|
+
_[e] = _[e - 16] + t + _[e - 7] + n >>> 0;
|
|
103
|
+
}
|
|
104
|
+
let t = o, n = s, r = d, i = f, u = p, v = m, y = h, b = g;
|
|
105
|
+
for (let e = 0; e < 64; e++) {
|
|
106
|
+
let a = l(6, u) ^ l(11, u) ^ l(25, u), o = u & v ^ ~u & y, s = b + a + o + c[e] + _[e] >>> 0, d = (l(2, t) ^ l(13, t) ^ l(22, t)) + (t & n ^ t & r ^ n & r) >>> 0;
|
|
107
|
+
b = y, y = v, v = u, u = i + s >>> 0, i = r, r = n, n = t, t = s + d >>> 0;
|
|
108
|
+
}
|
|
109
|
+
o = o + t >>> 0, s = s + n >>> 0, d = d + r >>> 0, f = f + i >>> 0, p = p + u >>> 0, m = m + v >>> 0, h = h + y >>> 0, g = g + b >>> 0;
|
|
110
|
+
}
|
|
111
|
+
let v = /* @__PURE__ */ new Uint8Array(32), y = new DataView(v.buffer);
|
|
112
|
+
return y.setUint32(0, o, !1), y.setUint32(4, s, !1), y.setUint32(8, d, !1), y.setUint32(12, f, !1), y.setUint32(16, p, !1), y.setUint32(20, m, !1), y.setUint32(24, h, !1), y.setUint32(28, g, !1), v;
|
|
113
|
+
}
|
|
114
|
+
function f(e) {
|
|
115
|
+
let t = "", n = e.length;
|
|
116
|
+
for (let r = 0; r < n; r += 3) {
|
|
117
|
+
let i = e[r], a = r + 1 < n ? e[r + 1] : 0, o = r + 2 < n ? e[r + 2] : 0;
|
|
118
|
+
t += s[i >> 2], t += s[(i & 3) << 4 | a >> 4], r + 1 < n && (t += s[(a & 15) << 2 | o >> 6]), r + 2 < n && (t += s[o & 63]);
|
|
119
|
+
}
|
|
120
|
+
return t;
|
|
121
|
+
}
|
|
122
|
+
function p(e) {
|
|
123
|
+
let t = new Uint8Array(e), n = typeof globalThis < "u" ? globalThis : void 0;
|
|
124
|
+
if (n?.crypto && typeof n.crypto.getRandomValues == "function") return n.crypto.getRandomValues(t), t;
|
|
125
|
+
try {
|
|
126
|
+
let r = typeof n?.require == "function" ? n.require : void 0;
|
|
127
|
+
if (r) {
|
|
128
|
+
let n = r("expo-crypto");
|
|
129
|
+
if (n && typeof n.getRandomValues == "function") return n.getRandomValues(t), t;
|
|
130
|
+
if (n && typeof n.getRandomBytes == "function") {
|
|
131
|
+
let r = n.getRandomBytes(e);
|
|
132
|
+
return t.set(r.subarray(0, e)), t;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch {}
|
|
136
|
+
throw Error("Cryptographically secure random number generator (CSPRNG) is unavailable. Please install and import \"react-native-get-random-values\" or \"expo-crypto\".");
|
|
137
|
+
}
|
|
138
|
+
function m(e) {
|
|
139
|
+
let t = e.replace(/-/g, "+").replace(/_/g, "/"), n = t + "=".repeat((4 - t.length % 4) % 4);
|
|
140
|
+
if (typeof atob == "function") {
|
|
141
|
+
let e = atob(n), t = new Uint8Array(e.length);
|
|
142
|
+
for (let n = 0; n < e.length; n++) t[n] = e.charCodeAt(n);
|
|
143
|
+
if (typeof TextDecoder < "u") return new TextDecoder().decode(t);
|
|
144
|
+
let r = "";
|
|
145
|
+
for (let e = 0; e < t.length; e++) r += String.fromCharCode(t[e]);
|
|
146
|
+
return decodeURIComponent(escape(r));
|
|
147
|
+
}
|
|
148
|
+
let r = typeof globalThis < "u" ? globalThis : void 0;
|
|
149
|
+
if (r?.Buffer) return r.Buffer.from(n, "base64").toString("utf8");
|
|
150
|
+
throw Error("Unable to decode base64url string.");
|
|
151
|
+
}
|
|
152
|
+
function h(e) {
|
|
153
|
+
if (!e || typeof e != "string") return null;
|
|
154
|
+
let t = e.split(".");
|
|
155
|
+
if (t.length < 2) return null;
|
|
156
|
+
try {
|
|
157
|
+
let e = m(t[1]);
|
|
158
|
+
return JSON.parse(e);
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function g(e = 64) {
|
|
164
|
+
let t = p(e), n = "";
|
|
165
|
+
for (let r = 0; r < e; r++) n += "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"[t[r] % 66];
|
|
166
|
+
return n;
|
|
167
|
+
}
|
|
168
|
+
function _() {
|
|
169
|
+
return g(64);
|
|
170
|
+
}
|
|
171
|
+
function v() {
|
|
172
|
+
return g(32);
|
|
173
|
+
}
|
|
174
|
+
async function y(e) {
|
|
175
|
+
let t = typeof globalThis < "u" ? globalThis : void 0;
|
|
176
|
+
if (t?.crypto?.subtle && typeof t.crypto.subtle.digest == "function") try {
|
|
177
|
+
let n = u(e), r = await t.crypto.subtle.digest("SHA-256", n);
|
|
178
|
+
return f(new Uint8Array(r));
|
|
179
|
+
} catch {}
|
|
180
|
+
return f(d(e));
|
|
181
|
+
}
|
|
182
|
+
// src/core/storage.ts
|
|
183
|
+
var b = Object.values(o), x = class {
|
|
184
|
+
backend;
|
|
185
|
+
cache = /* @__PURE__ */ new Map();
|
|
186
|
+
hydrated = !1;
|
|
187
|
+
hydratePromise = null;
|
|
188
|
+
constructor(e) {
|
|
189
|
+
this.backend = e || S();
|
|
190
|
+
}
|
|
191
|
+
async hydrate() {
|
|
192
|
+
if (!this.hydrated) return this.hydratePromise ||= (async () => {
|
|
193
|
+
await Promise.all(b.map(async (e) => {
|
|
194
|
+
try {
|
|
195
|
+
let t = await this.backend.getItem(e);
|
|
196
|
+
t != null && this.cache.set(e, t);
|
|
197
|
+
} catch {}
|
|
198
|
+
})), this.hydrated = !0;
|
|
199
|
+
})().finally(() => {
|
|
200
|
+
this.hydratePromise = null;
|
|
201
|
+
}), this.hydratePromise;
|
|
202
|
+
}
|
|
203
|
+
getItemSync(e) {
|
|
204
|
+
return this.cache.get(e) ?? null;
|
|
205
|
+
}
|
|
206
|
+
async getItem(e) {
|
|
207
|
+
if (this.cache.has(e)) return this.cache.get(e);
|
|
208
|
+
let t = await this.backend.getItem(e);
|
|
209
|
+
return t == null ? null : (this.cache.set(e, t), t);
|
|
210
|
+
}
|
|
211
|
+
async setItem(e, t) {
|
|
212
|
+
this.cache.set(e, t), await this.backend.setItem(e, t);
|
|
213
|
+
}
|
|
214
|
+
async removeItem(e) {
|
|
215
|
+
this.cache.delete(e), await this.backend.removeItem(e);
|
|
216
|
+
}
|
|
217
|
+
async clearTemporary() {
|
|
218
|
+
await Promise.all([
|
|
219
|
+
this.removeItem(o.CODE_VERIFIER),
|
|
220
|
+
this.removeItem(o.STATE),
|
|
221
|
+
this.removeItem(o.NONCE)
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
async clearTokens() {
|
|
225
|
+
await Promise.all([
|
|
226
|
+
this.removeItem(o.ACCESS_TOKEN),
|
|
227
|
+
this.removeItem(o.REFRESH_TOKEN),
|
|
228
|
+
this.removeItem(o.TOKEN_EXPIRES_AT),
|
|
229
|
+
this.removeItem(o.TOKEN_TYPE),
|
|
230
|
+
this.removeItem(o.SCOPE),
|
|
231
|
+
this.removeItem(o.ID_TOKEN)
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
function S() {
|
|
236
|
+
let e = /* @__PURE__ */ new Map();
|
|
237
|
+
return {
|
|
238
|
+
getItem: async (t) => e.get(t) ?? null,
|
|
239
|
+
setItem: async (t, n) => {
|
|
240
|
+
e.set(t, n);
|
|
241
|
+
},
|
|
242
|
+
removeItem: async (t) => {
|
|
243
|
+
e.delete(t);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
var C = 2e3, w = "__witsauth_chunked__:";
|
|
248
|
+
function T(e) {
|
|
249
|
+
let t = async (t) => {
|
|
250
|
+
try {
|
|
251
|
+
let n = await e.getItemAsync(t);
|
|
252
|
+
if (n && n.startsWith(w)) {
|
|
253
|
+
let r = parseInt(n.slice(21), 10);
|
|
254
|
+
if (Number.isFinite(r) && r > 0) {
|
|
255
|
+
let n = [];
|
|
256
|
+
for (let i = 0; i < r; i++) n.push(e.deleteItemAsync(`${t}__chunk_${i}`));
|
|
257
|
+
await Promise.all(n);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch {}
|
|
261
|
+
};
|
|
262
|
+
return {
|
|
263
|
+
getItem: async (t) => {
|
|
264
|
+
let n = await e.getItemAsync(t);
|
|
265
|
+
if (!n) return null;
|
|
266
|
+
if (n.startsWith(w)) {
|
|
267
|
+
let r = parseInt(n.slice(21), 10);
|
|
268
|
+
if (!Number.isFinite(r) || r <= 0) return null;
|
|
269
|
+
let i = [];
|
|
270
|
+
for (let n = 0; n < r; n++) i.push(e.getItemAsync(`${t}__chunk_${n}`));
|
|
271
|
+
let a = await Promise.all(i);
|
|
272
|
+
return a.some((e) => e == null) ? null : a.join("");
|
|
273
|
+
}
|
|
274
|
+
return n;
|
|
275
|
+
},
|
|
276
|
+
setItem: async (n, r) => {
|
|
277
|
+
if (r.length <= C) {
|
|
278
|
+
await t(n), await e.setItemAsync(n, r);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
await t(n);
|
|
282
|
+
let i = [];
|
|
283
|
+
for (let e = 0; e < r.length; e += C) i.push(r.slice(e, e + C));
|
|
284
|
+
await Promise.all(i.map((t, r) => e.setItemAsync(`${n}__chunk_${r}`, t))), await e.setItemAsync(n, `${w}${i.length}`);
|
|
285
|
+
},
|
|
286
|
+
removeItem: async (n) => {
|
|
287
|
+
await t(n), await e.deleteItemAsync(n);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function E(e, t = "com.witsauth.tokens") {
|
|
292
|
+
return {
|
|
293
|
+
getItem: async (n) => {
|
|
294
|
+
let r = await e.getGenericPassword({ service: `${t}.${n}` });
|
|
295
|
+
return r ? r.password : null;
|
|
296
|
+
},
|
|
297
|
+
setItem: async (n, r) => {
|
|
298
|
+
await e.setGenericPassword(n, r, { service: `${t}.${n}` });
|
|
299
|
+
},
|
|
300
|
+
removeItem: async (n) => {
|
|
301
|
+
await e.resetGenericPassword({ service: `${t}.${n}` });
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// src/core/browser.ts
|
|
306
|
+
function D(e) {
|
|
307
|
+
return async (t, n) => {
|
|
308
|
+
let r = await e.openAuthSessionAsync(t, n);
|
|
309
|
+
return r.type === "success" && r.url ? {
|
|
310
|
+
type: "success",
|
|
311
|
+
url: r.url
|
|
312
|
+
} : r.type === "cancel" ? { type: "cancel" } : { type: "dismiss" };
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function O(e) {
|
|
316
|
+
return async (t, n) => {
|
|
317
|
+
if (!await e.isAvailable()) throw Error("InAppBrowser is not available on this device.");
|
|
318
|
+
let r = await e.openAuth(t, n, {
|
|
319
|
+
ephemeralWebSession: !1,
|
|
320
|
+
showTitle: !1,
|
|
321
|
+
enableUrlBarHiding: !0,
|
|
322
|
+
enableDefaultShare: !1
|
|
323
|
+
});
|
|
324
|
+
return r.type === "success" && r.url ? {
|
|
325
|
+
type: "success",
|
|
326
|
+
url: r.url
|
|
327
|
+
} : r.type === "cancel" ? { type: "cancel" } : { type: "dismiss" };
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function k(e) {
|
|
331
|
+
let t = e.indexOf("?");
|
|
332
|
+
if (t === -1) return {
|
|
333
|
+
code: null,
|
|
334
|
+
state: null,
|
|
335
|
+
error: null,
|
|
336
|
+
errorDescription: null
|
|
337
|
+
};
|
|
338
|
+
let n = e.indexOf("#", t), r = n === -1 ? e.slice(t + 1) : e.slice(t + 1, n), i = new URLSearchParams(r);
|
|
339
|
+
return {
|
|
340
|
+
code: i.get("code"),
|
|
341
|
+
state: i.get("state"),
|
|
342
|
+
error: i.get("error"),
|
|
343
|
+
errorDescription: i.get("error_description")
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// src/core/AuthClient.ts
|
|
347
|
+
var A = class {
|
|
348
|
+
config;
|
|
349
|
+
storage;
|
|
350
|
+
refreshPromise = null;
|
|
351
|
+
constructor(e, t) {
|
|
352
|
+
this.config = e, this.storage = t || new x(e.storage);
|
|
353
|
+
}
|
|
354
|
+
async initialize() {
|
|
355
|
+
await this.storage.hydrate();
|
|
356
|
+
}
|
|
357
|
+
log(e, ...t) {
|
|
358
|
+
let n = {
|
|
359
|
+
none: 0,
|
|
360
|
+
error: 1,
|
|
361
|
+
warn: 2,
|
|
362
|
+
info: 3,
|
|
363
|
+
debug: 4
|
|
364
|
+
}, r = n[this.config.logLevel || "warn"] || 0, i = n[e] || 0;
|
|
365
|
+
i > 0 && r >= i && console[e](...t);
|
|
366
|
+
}
|
|
367
|
+
async buildAuthorizationUrl(e) {
|
|
368
|
+
await this.storage.hydrate();
|
|
369
|
+
let t = _(), n = await y(t), r = v(), i = this.config.nonce || v();
|
|
370
|
+
await this.storage.setItem(o.CODE_VERIFIER, t), await this.storage.setItem(o.STATE, r), await this.storage.setItem(o.NONCE, i);
|
|
371
|
+
let a = new URLSearchParams({
|
|
372
|
+
client_id: this.config.clientId,
|
|
373
|
+
redirect_uri: this.config.redirectUri,
|
|
374
|
+
response_type: this.config.responseType || "code",
|
|
375
|
+
code_challenge_method: this.config.codeChallengeMethod || "S256",
|
|
376
|
+
code_challenge: n,
|
|
377
|
+
state: r,
|
|
378
|
+
nonce: i
|
|
379
|
+
});
|
|
380
|
+
if (this.config.theme) {
|
|
381
|
+
let e = typeof this.config.theme == "function" ? this.config.theme() : this.config.theme;
|
|
382
|
+
e && a.append("theme", e);
|
|
383
|
+
}
|
|
384
|
+
let s = e?.scope ?? this.config.scope, c = e?.audience ?? this.config.audience;
|
|
385
|
+
return s && a.append("scope", s), c && a.append("audience", c), e?.prompt && a.append("prompt", e.prompt), `${this.config.authorizationEndpoint}?${a.toString()}`;
|
|
386
|
+
}
|
|
387
|
+
async authorize(e) {
|
|
388
|
+
if (!this.config.openAuthSession) throw Error("No openAuthSession launcher configured. Pass createExpoWebBrowserLauncher(WebBrowser) or createInAppBrowserLauncher(InAppBrowser) in OAuth2NativeConfig.");
|
|
389
|
+
let t = await this.buildAuthorizationUrl(e), n = await this.config.openAuthSession(t, this.config.redirectUri);
|
|
390
|
+
if (n.type !== "success" || !n.url) throw await this.storage.clearTemporary(), Error(`Authentication ${n.type}`);
|
|
391
|
+
return this.handleCallbackUrl(n.url);
|
|
392
|
+
}
|
|
393
|
+
async handleCallbackUrl(e) {
|
|
394
|
+
let { code: t, state: n, error: r, errorDescription: i } = k(e);
|
|
395
|
+
if (r) throw await this.storage.clearTemporary(), Error(i ? `${r}: ${i}` : r);
|
|
396
|
+
if (!t || !n) throw await this.storage.clearTemporary(), Error("Callback URL is missing required code or state parameters.");
|
|
397
|
+
return this.exchangeCodeForTokens(t, n);
|
|
398
|
+
}
|
|
399
|
+
async exchangeCodeForTokens(e, t) {
|
|
400
|
+
await this.storage.hydrate();
|
|
401
|
+
let n = await this.storage.getItem(o.STATE), r = await this.storage.getItem(o.CODE_VERIFIER);
|
|
402
|
+
if (!n || t !== n) throw await this.storage.clearTemporary(), Error("Invalid state parameter. Possible CSRF attack.");
|
|
403
|
+
if (!r) throw await this.storage.clearTemporary(), Error("Code verifier missing from storage.");
|
|
404
|
+
let i = new URLSearchParams({
|
|
405
|
+
grant_type: "authorization_code",
|
|
406
|
+
client_id: this.config.clientId,
|
|
407
|
+
redirect_uri: this.config.redirectUri,
|
|
408
|
+
code: e,
|
|
409
|
+
code_verifier: r
|
|
410
|
+
}), a = await fetch(this.config.tokenEndpoint, {
|
|
411
|
+
method: "POST",
|
|
412
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
413
|
+
body: i.toString()
|
|
414
|
+
});
|
|
415
|
+
if (!a.ok) throw Error("Failed to exchange code for tokens");
|
|
416
|
+
let s = await a.json(), c = {
|
|
417
|
+
accessToken: s.access_token,
|
|
418
|
+
refreshToken: s.refresh_token,
|
|
419
|
+
tokenType: s.token_type || "Bearer",
|
|
420
|
+
expiresAt: Date.now() + s.expires_in * 1e3,
|
|
421
|
+
scope: s.scope,
|
|
422
|
+
idToken: s.id_token
|
|
423
|
+
};
|
|
424
|
+
return await this.saveTokenInfo(c), await this.storage.clearTemporary(), c;
|
|
425
|
+
}
|
|
426
|
+
async refreshAccessToken() {
|
|
427
|
+
return this.refreshPromise ||= (async () => {
|
|
428
|
+
await this.storage.hydrate();
|
|
429
|
+
let e = await this.storage.getItem(o.REFRESH_TOKEN);
|
|
430
|
+
if (!e) throw Error("No refresh token available");
|
|
431
|
+
let t = new URLSearchParams({
|
|
432
|
+
grant_type: "refresh_token",
|
|
433
|
+
client_id: this.config.clientId,
|
|
434
|
+
refresh_token: e
|
|
435
|
+
}), n;
|
|
436
|
+
try {
|
|
437
|
+
n = await fetch(this.config.tokenEndpoint, {
|
|
438
|
+
method: "POST",
|
|
439
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
440
|
+
body: t.toString()
|
|
441
|
+
});
|
|
442
|
+
} catch (e) {
|
|
443
|
+
throw this.log("error", "Token refresh network request failed:", e), e;
|
|
444
|
+
}
|
|
445
|
+
if (!n.ok) {
|
|
446
|
+
let e = {};
|
|
447
|
+
try {
|
|
448
|
+
e = await n.json();
|
|
449
|
+
} catch {
|
|
450
|
+
try {
|
|
451
|
+
e = { error: await n.text() };
|
|
452
|
+
} catch {
|
|
453
|
+
e = {};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
this.log("error", "Token refresh failed with status:", n.status, e), (n.status === 400 || n.status === 401) && await this.storage.clearTokens();
|
|
457
|
+
let t = e.error_description || e.error || n.statusText || "Failed to refresh token";
|
|
458
|
+
throw Error(`Failed to refresh token (${n.status}): ${t}`);
|
|
459
|
+
}
|
|
460
|
+
let r = await n.json(), i = {
|
|
461
|
+
accessToken: r.access_token,
|
|
462
|
+
refreshToken: r.refresh_token || e,
|
|
463
|
+
tokenType: r.token_type || "Bearer",
|
|
464
|
+
expiresAt: Date.now() + r.expires_in * 1e3,
|
|
465
|
+
scope: r.scope,
|
|
466
|
+
idToken: r.id_token
|
|
467
|
+
};
|
|
468
|
+
return await this.saveTokenInfo(i), i;
|
|
469
|
+
})().finally(() => {
|
|
470
|
+
this.refreshPromise = null;
|
|
471
|
+
}), this.refreshPromise;
|
|
472
|
+
}
|
|
473
|
+
async saveTokenInfo(e) {
|
|
474
|
+
await this.storage.setItem(o.ACCESS_TOKEN, e.accessToken), await this.storage.setItem(o.TOKEN_EXPIRES_AT, e.expiresAt.toString()), await this.storage.setItem(o.TOKEN_TYPE, e.tokenType), e.refreshToken && await this.storage.setItem(o.REFRESH_TOKEN, e.refreshToken), e.scope && await this.storage.setItem(o.SCOPE, e.scope), e.idToken && await this.storage.setItem(o.ID_TOKEN, e.idToken);
|
|
475
|
+
}
|
|
476
|
+
getAccessToken() {
|
|
477
|
+
return this.storage.getItemSync(o.ACCESS_TOKEN);
|
|
478
|
+
}
|
|
479
|
+
getIdToken() {
|
|
480
|
+
return this.storage.getItemSync(o.ID_TOKEN);
|
|
481
|
+
}
|
|
482
|
+
getUserProfile() {
|
|
483
|
+
return h(this.getIdToken() || this.getAccessToken());
|
|
484
|
+
}
|
|
485
|
+
getRefreshToken() {
|
|
486
|
+
return this.storage.getItemSync(o.REFRESH_TOKEN);
|
|
487
|
+
}
|
|
488
|
+
getExpiresAt() {
|
|
489
|
+
let e = this.storage.getItemSync(o.TOKEN_EXPIRES_AT);
|
|
490
|
+
if (!e) return null;
|
|
491
|
+
let t = Number(e);
|
|
492
|
+
return Number.isFinite(t) ? t : null;
|
|
493
|
+
}
|
|
494
|
+
isAuthenticated() {
|
|
495
|
+
let e = this.getExpiresAt();
|
|
496
|
+
return e !== null && Date.now() < e - 3e4 && !!this.getAccessToken();
|
|
497
|
+
}
|
|
498
|
+
async navigateToAccountManagement() {
|
|
499
|
+
await this.storage.hydrate();
|
|
500
|
+
let e = await this.storage.getItem(o.REFRESH_TOKEN);
|
|
501
|
+
if (!e) throw Error("No refresh token available to access account management.");
|
|
502
|
+
if (!this.config.openAuthSession) throw Error("No openAuthSession launcher configured. Pass createExpoWebBrowserLauncher(WebBrowser) or createInAppBrowserLauncher(InAppBrowser) in OAuth2NativeConfig.");
|
|
503
|
+
let t = this.config.authorizationEndpoint.replace(/\/authorize\/?$/, ""), n = await fetch(`${t}/account/ticket`, {
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers: { "Content-Type": "application/json" },
|
|
506
|
+
body: JSON.stringify({
|
|
507
|
+
refreshToken: e,
|
|
508
|
+
clientId: this.config.clientId,
|
|
509
|
+
redirectUri: this.config.redirectUri
|
|
510
|
+
})
|
|
511
|
+
});
|
|
512
|
+
if (!n.ok) throw Error(`Failed to request account management ticket (${n.status})`);
|
|
513
|
+
let { ticket: r } = await n.json();
|
|
514
|
+
if (!r) throw Error("Account management ticket missing from server response.");
|
|
515
|
+
let i = `${t}/account?ticket=${encodeURIComponent(r)}&return_to=${encodeURIComponent(this.config.redirectUri)}`;
|
|
516
|
+
await this.config.openAuthSession(i, this.config.redirectUri);
|
|
517
|
+
}
|
|
518
|
+
async logout(e) {
|
|
519
|
+
await this.storage.hydrate();
|
|
520
|
+
let t = this.getAccessToken(), n = await this.storage.getItem(o.REFRESH_TOKEN);
|
|
521
|
+
if (await this.storage.clearTokens(), await this.storage.clearTemporary(), this.config.revokeEndpoint) {
|
|
522
|
+
let e = [...n ? [{
|
|
523
|
+
token: n,
|
|
524
|
+
token_type_hint: "refresh_token"
|
|
525
|
+
}] : [], ...t ? [{
|
|
526
|
+
token: t,
|
|
527
|
+
token_type_hint: "access_token"
|
|
528
|
+
}] : []];
|
|
529
|
+
for (let t of e) try {
|
|
530
|
+
await fetch(this.config.revokeEndpoint, {
|
|
531
|
+
method: "POST",
|
|
532
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
533
|
+
body: new URLSearchParams({
|
|
534
|
+
token: t.token,
|
|
535
|
+
token_type_hint: t.token_type_hint,
|
|
536
|
+
client_id: this.config.clientId
|
|
537
|
+
}).toString()
|
|
538
|
+
});
|
|
539
|
+
} catch (e) {
|
|
540
|
+
this.log("error", "Failed to revoke token during logout", e);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (e?.clearBrowserSession && this.config.logoutEndpoint && this.config.openAuthSession) {
|
|
544
|
+
let e = new URLSearchParams({
|
|
545
|
+
client_id: this.config.clientId,
|
|
546
|
+
redirect_uri: this.config.redirectUri
|
|
547
|
+
}), t = this.config.logoutEndpoint.includes("?") ? "&" : "?", n = `${this.config.logoutEndpoint}${t}${e.toString()}`;
|
|
548
|
+
try {
|
|
549
|
+
await this.config.openAuthSession(n, this.config.redirectUri);
|
|
550
|
+
} catch (e) {
|
|
551
|
+
this.log("warn", "Browser logout session dismissed or failed", e);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}, j = e(null), M = ({ config: e, client: n, children: o, onLoginSuccess: s, onLoginError: c, subscribeAppState: l }) => {
|
|
556
|
+
let [u] = i(() => {
|
|
557
|
+
if (n) return n;
|
|
558
|
+
if (e) return new A(e);
|
|
559
|
+
throw Error("AuthProvider requires either a `client` or `config` prop.");
|
|
560
|
+
}), [d, f] = i(!1), [p, m] = i(null), [h, g] = i(null), [_, v] = i(null), [y, b] = i(!0), [x, S] = i(null), C = t(async () => {
|
|
561
|
+
if (await u.initialize(), u.isAuthenticated()) f(!0), m(u.getAccessToken()), g(u.getIdToken()), v(u.getUserProfile());
|
|
562
|
+
else {
|
|
563
|
+
if (u.getRefreshToken()) try {
|
|
564
|
+
let e = await u.refreshAccessToken();
|
|
565
|
+
f(!0), m(e.accessToken), g(u.getIdToken()), v(u.getUserProfile());
|
|
566
|
+
return;
|
|
567
|
+
} catch {
|
|
568
|
+
f(!1), m(null), g(null), v(null);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
f(!1), m(null), g(null), v(null);
|
|
572
|
+
}
|
|
573
|
+
}, [u]);
|
|
574
|
+
r(() => {
|
|
575
|
+
let e = !0;
|
|
576
|
+
return (async () => {
|
|
577
|
+
try {
|
|
578
|
+
await C();
|
|
579
|
+
} finally {
|
|
580
|
+
e && b(!1);
|
|
581
|
+
}
|
|
582
|
+
})(), () => {
|
|
583
|
+
e = !1;
|
|
584
|
+
};
|
|
585
|
+
}, [C]), r(() => {
|
|
586
|
+
if (!d) return;
|
|
587
|
+
let e, t = () => {
|
|
588
|
+
let n = u.getExpiresAt();
|
|
589
|
+
if (!n) return;
|
|
590
|
+
let r = n - Date.now() - 6e4;
|
|
591
|
+
e = setTimeout(async () => {
|
|
592
|
+
try {
|
|
593
|
+
let e = await u.refreshAccessToken();
|
|
594
|
+
m(e.accessToken), g(u.getIdToken()), v(u.getUserProfile()), f(!0), t();
|
|
595
|
+
} catch (n) {
|
|
596
|
+
u.log("error", "Background token refresh failed:", n), u.getAccessToken() ? e = setTimeout(t, 3e4) : (f(!1), m(null), g(null), v(null));
|
|
597
|
+
}
|
|
598
|
+
}, Math.max(0, r));
|
|
599
|
+
};
|
|
600
|
+
t();
|
|
601
|
+
let n = l ? l((e) => {
|
|
602
|
+
e === "active" && C().catch(() => {});
|
|
603
|
+
}) : void 0;
|
|
604
|
+
return () => {
|
|
605
|
+
e && clearTimeout(e), n && n();
|
|
606
|
+
};
|
|
607
|
+
}, [
|
|
608
|
+
d,
|
|
609
|
+
u,
|
|
610
|
+
l,
|
|
611
|
+
C
|
|
612
|
+
]);
|
|
613
|
+
let w = t(async (e) => {
|
|
614
|
+
b(!0), S(null);
|
|
615
|
+
try {
|
|
616
|
+
let t = await u.authorize(e);
|
|
617
|
+
m(t.accessToken), g(u.getIdToken()), v(u.getUserProfile()), f(!0), s?.();
|
|
618
|
+
} catch (e) {
|
|
619
|
+
let t = e instanceof Error ? e : /* @__PURE__ */ Error("Authentication failed");
|
|
620
|
+
S(t), c?.(t);
|
|
621
|
+
} finally {
|
|
622
|
+
b(!1);
|
|
623
|
+
}
|
|
624
|
+
}, [
|
|
625
|
+
u,
|
|
626
|
+
s,
|
|
627
|
+
c
|
|
628
|
+
]), T = t(async (e) => {
|
|
629
|
+
b(!0);
|
|
630
|
+
try {
|
|
631
|
+
await u.logout(e), f(!1), m(null), g(null), v(null);
|
|
632
|
+
} catch (e) {
|
|
633
|
+
S(e instanceof Error ? e : /* @__PURE__ */ Error("Logout failed"));
|
|
634
|
+
} finally {
|
|
635
|
+
b(!1);
|
|
636
|
+
}
|
|
637
|
+
}, [u]), E = t(async () => {
|
|
638
|
+
if (await u.initialize(), u.isAuthenticated()) return u.getAccessToken();
|
|
639
|
+
try {
|
|
640
|
+
let e = await u.refreshAccessToken();
|
|
641
|
+
return m(e.accessToken), g(u.getIdToken()), v(u.getUserProfile()), f(!0), e.accessToken;
|
|
642
|
+
} catch {
|
|
643
|
+
return f(!1), m(null), g(null), v(null), null;
|
|
644
|
+
}
|
|
645
|
+
}, [u]), D = t(async () => {
|
|
646
|
+
await u.navigateToAccountManagement(), await C();
|
|
647
|
+
}, [u, C]);
|
|
648
|
+
return /* @__PURE__ */ a(j.Provider, {
|
|
649
|
+
value: {
|
|
650
|
+
isAuthenticated: d,
|
|
651
|
+
isLoading: y,
|
|
652
|
+
accessToken: p,
|
|
653
|
+
idToken: h,
|
|
654
|
+
user: _,
|
|
655
|
+
error: x,
|
|
656
|
+
login: w,
|
|
657
|
+
logout: T,
|
|
658
|
+
getToken: E,
|
|
659
|
+
navigateToAccountManagement: D,
|
|
660
|
+
authClient: u
|
|
661
|
+
},
|
|
662
|
+
children: o
|
|
663
|
+
});
|
|
664
|
+
}, N = () => {
|
|
665
|
+
let e = n(j);
|
|
666
|
+
if (!e) throw Error("useAuth must be used within an AuthProvider");
|
|
667
|
+
return e;
|
|
668
|
+
};
|
|
669
|
+
// src/axios/interceptor.ts
|
|
670
|
+
function P(e, t, n = {}) {
|
|
671
|
+
let r = !1, i = [], a = (e, t = null) => {
|
|
672
|
+
i.forEach((n) => {
|
|
673
|
+
e ? n.reject(e) : n.resolve(t);
|
|
674
|
+
}), i = [];
|
|
675
|
+
}, o = (e) => !e || !n.excludeUrls ? !1 : n.excludeUrls.some((t) => typeof t == "string" ? e.includes(t) : t.test(e)), s = e.interceptors.request.use(async (e) => {
|
|
676
|
+
if (o(e.url)) return e;
|
|
677
|
+
await t.initialize();
|
|
678
|
+
let n = t.getAccessToken();
|
|
679
|
+
if (!t.isAuthenticated() && n) try {
|
|
680
|
+
n = (await t.refreshAccessToken()).accessToken;
|
|
681
|
+
} catch {
|
|
682
|
+
n = null;
|
|
683
|
+
}
|
|
684
|
+
return n && e.headers.set("Authorization", `Bearer ${n}`), e;
|
|
685
|
+
}, (e) => Promise.reject(e)), c = e.interceptors.response.use((e) => e, async (s) => {
|
|
686
|
+
let c = s.config;
|
|
687
|
+
if (!c || o(c.url)) return Promise.reject(s);
|
|
688
|
+
if (s.response?.status === 401 && !c._retry) {
|
|
689
|
+
if (r) return new Promise((e, t) => {
|
|
690
|
+
i.push({
|
|
691
|
+
resolve: e,
|
|
692
|
+
reject: t
|
|
693
|
+
});
|
|
694
|
+
}).then((t) => (t && c.headers.set("Authorization", `Bearer ${t}`), e(c))).catch((e) => Promise.reject(e));
|
|
695
|
+
c._retry = !0, r = !0;
|
|
696
|
+
try {
|
|
697
|
+
let n = (await t.refreshAccessToken()).accessToken;
|
|
698
|
+
return a(null, n), c.headers.set("Authorization", `Bearer ${n}`), e(c);
|
|
699
|
+
} catch (e) {
|
|
700
|
+
return a(e, null), await t.logout(), n.onUnauthorized && n.onUnauthorized(), Promise.reject(e);
|
|
701
|
+
} finally {
|
|
702
|
+
r = !1;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return Promise.reject(s);
|
|
706
|
+
});
|
|
707
|
+
return () => {
|
|
708
|
+
e.interceptors.request.eject(s), e.interceptors.response.eject(c);
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
export { A as AuthClient, M as AuthProvider, x as NativeTokenStorage, o as STORAGE_KEYS, f as base64UrlEncode, y as createCodeChallenge, T as createExpoSecureStoreAdapter, D as createExpoWebBrowserLauncher, O as createInAppBrowserLauncher, E as createKeychainStorageAdapter, S as createMemoryStorageAdapter, h as decodeJwtPayload, _ as generateCodeVerifier, g as generateRandomString, v as generateState, p as getRandomBytes, k as parseCallbackUrl, P as setupAxiosInterceptors, d as sha256Bytes, N as useAuth };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("react"),require("react/jsx-runtime")):typeof define==`function`&&define.amd?define([`exports`,`react`,`react/jsx-runtime`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.WitsAuthReactNativeClient={},e.React,e.jsxRuntime))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=Object.create,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,l=(e,t,n,r)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var s=o(t),l=0,u=s.length,d;l<u;l++)d=s[l],!c.call(e,d)&&d!==n&&i(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(r=a(t,d))||r.enumerable});return e};t=((e,t,n)=>(n=e==null?{}:r(s(e)),l(i(n,`default`,{value:e,enumerable:!0}),e)))(t,1);var u={ACCESS_TOKEN:`witsauth_access_token`,REFRESH_TOKEN:`witsauth_refresh_token`,TOKEN_EXPIRES_AT:`witsauth_expires_at`,TOKEN_TYPE:`witsauth_token_type`,SCOPE:`witsauth_scope`,ID_TOKEN:`witsauth_id_token`,CODE_VERIFIER:`witsauth_code_verifier`,STATE:`witsauth_state`,NONCE:`witsauth_nonce`},d=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_`,f=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]);function p(e,t){return t>>>e|t<<32-e}function m(e){if(typeof TextEncoder<`u`)return new TextEncoder().encode(e);let t=[];for(let n=0;n<e.length;n++){let r=e.charCodeAt(n);r<128?t.push(r):r<2048?t.push(192|r>>6,128|r&63):r<55296||r>=57344?t.push(224|r>>12,128|r>>6&63,128|r&63):(n++,r=65536+((r&1023)<<10|e.charCodeAt(n)&1023),t.push(240|r>>18,128|r>>12&63,128|r>>6&63,128|r&63))}return new Uint8Array(t)}function h(e){let t=m(e),n=t.length*8,r=t.length+9+63>>6<<6,i=new Uint8Array(r);i.set(t),i[t.length]=128;let a=new DataView(i.buffer);a.setUint32(r-8,Math.floor(n/4294967296),!1),a.setUint32(r-4,n>>>0,!1);let o=1779033703,s=3144134277,c=1013904242,l=2773480762,u=1359893119,d=2600822924,h=528734635,g=1541459225,_=new Uint32Array(64);for(let e=0;e<r;e+=64){for(let t=0;t<16;t++)_[t]=a.getUint32(e+t*4,!1);for(let e=16;e<64;e++){let t=p(7,_[e-15])^p(18,_[e-15])^_[e-15]>>>3,n=p(17,_[e-2])^p(19,_[e-2])^_[e-2]>>>10;_[e]=_[e-16]+t+_[e-7]+n>>>0}let t=o,n=s,r=c,i=l,m=u,v=d,y=h,b=g;for(let e=0;e<64;e++){let a=p(6,m)^p(11,m)^p(25,m),o=m&v^~m&y,s=b+a+o+f[e]+_[e]>>>0,c=(p(2,t)^p(13,t)^p(22,t))+(t&n^t&r^n&r)>>>0;b=y,y=v,v=m,m=i+s>>>0,i=r,r=n,n=t,t=s+c>>>0}o=o+t>>>0,s=s+n>>>0,c=c+r>>>0,l=l+i>>>0,u=u+m>>>0,d=d+v>>>0,h=h+y>>>0,g=g+b>>>0}let v=new Uint8Array(32),y=new DataView(v.buffer);return y.setUint32(0,o,!1),y.setUint32(4,s,!1),y.setUint32(8,c,!1),y.setUint32(12,l,!1),y.setUint32(16,u,!1),y.setUint32(20,d,!1),y.setUint32(24,h,!1),y.setUint32(28,g,!1),v}function g(e){let t=``,n=e.length;for(let r=0;r<n;r+=3){let i=e[r],a=r+1<n?e[r+1]:0,o=r+2<n?e[r+2]:0;t+=d[i>>2],t+=d[(i&3)<<4|a>>4],r+1<n&&(t+=d[(a&15)<<2|o>>6]),r+2<n&&(t+=d[o&63])}return t}function _(e){let t=new Uint8Array(e),n=typeof globalThis<`u`?globalThis:void 0;if(n?.crypto&&typeof n.crypto.getRandomValues==`function`)return n.crypto.getRandomValues(t),t;try{let r=typeof n?.require==`function`?n.require:void 0;if(r){let n=r(`expo-crypto`);if(n&&typeof n.getRandomValues==`function`)return n.getRandomValues(t),t;if(n&&typeof n.getRandomBytes==`function`){let r=n.getRandomBytes(e);return t.set(r.subarray(0,e)),t}}}catch{}throw Error(`Cryptographically secure random number generator (CSPRNG) is unavailable. Please install and import "react-native-get-random-values" or "expo-crypto".`)}function v(e){let t=e.replace(/-/g,`+`).replace(/_/g,`/`),n=t+`=`.repeat((4-t.length%4)%4);if(typeof atob==`function`){let e=atob(n),t=new Uint8Array(e.length);for(let n=0;n<e.length;n++)t[n]=e.charCodeAt(n);if(typeof TextDecoder<`u`)return new TextDecoder().decode(t);let r=``;for(let e=0;e<t.length;e++)r+=String.fromCharCode(t[e]);return decodeURIComponent(escape(r))}let r=typeof globalThis<`u`?globalThis:void 0;if(r?.Buffer)return r.Buffer.from(n,`base64`).toString(`utf8`);throw Error(`Unable to decode base64url string.`)}function y(e){if(!e||typeof e!=`string`)return null;let t=e.split(`.`);if(t.length<2)return null;try{let e=v(t[1]);return JSON.parse(e)}catch{return null}}function b(e=64){let t=_(e),n=``;for(let r=0;r<e;r++)n+=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~`[t[r]%66];return n}function x(){return b(64)}function S(){return b(32)}async function C(e){let t=typeof globalThis<`u`?globalThis:void 0;if(t?.crypto?.subtle&&typeof t.crypto.subtle.digest==`function`)try{let n=m(e),r=await t.crypto.subtle.digest(`SHA-256`,n);return g(new Uint8Array(r))}catch{}return g(h(e))}var w=Object.values(u),T=class{backend;cache=new Map;hydrated=!1;hydratePromise=null;constructor(e){this.backend=e||E()}async hydrate(){if(!this.hydrated)return this.hydratePromise||=(async()=>{await Promise.all(w.map(async e=>{try{let t=await this.backend.getItem(e);t!=null&&this.cache.set(e,t)}catch{}})),this.hydrated=!0})().finally(()=>{this.hydratePromise=null}),this.hydratePromise}getItemSync(e){return this.cache.get(e)??null}async getItem(e){if(this.cache.has(e))return this.cache.get(e);let t=await this.backend.getItem(e);return t==null?null:(this.cache.set(e,t),t)}async setItem(e,t){this.cache.set(e,t),await this.backend.setItem(e,t)}async removeItem(e){this.cache.delete(e),await this.backend.removeItem(e)}async clearTemporary(){await Promise.all([this.removeItem(u.CODE_VERIFIER),this.removeItem(u.STATE),this.removeItem(u.NONCE)])}async clearTokens(){await Promise.all([this.removeItem(u.ACCESS_TOKEN),this.removeItem(u.REFRESH_TOKEN),this.removeItem(u.TOKEN_EXPIRES_AT),this.removeItem(u.TOKEN_TYPE),this.removeItem(u.SCOPE),this.removeItem(u.ID_TOKEN)])}};function E(){let e=new Map;return{getItem:async t=>e.get(t)??null,setItem:async(t,n)=>{e.set(t,n)},removeItem:async t=>{e.delete(t)}}}var D=2e3,O=`__witsauth_chunked__:`;function k(e){let t=async t=>{try{let n=await e.getItemAsync(t);if(n&&n.startsWith(O)){let r=parseInt(n.slice(21),10);if(Number.isFinite(r)&&r>0){let n=[];for(let i=0;i<r;i++)n.push(e.deleteItemAsync(`${t}__chunk_${i}`));await Promise.all(n)}}}catch{}};return{getItem:async t=>{let n=await e.getItemAsync(t);if(!n)return null;if(n.startsWith(O)){let r=parseInt(n.slice(21),10);if(!Number.isFinite(r)||r<=0)return null;let i=[];for(let n=0;n<r;n++)i.push(e.getItemAsync(`${t}__chunk_${n}`));let a=await Promise.all(i);return a.some(e=>e==null)?null:a.join(``)}return n},setItem:async(n,r)=>{if(r.length<=D){await t(n),await e.setItemAsync(n,r);return}await t(n);let i=[];for(let e=0;e<r.length;e+=D)i.push(r.slice(e,e+D));await Promise.all(i.map((t,r)=>e.setItemAsync(`${n}__chunk_${r}`,t))),await e.setItemAsync(n,`${O}${i.length}`)},removeItem:async n=>{await t(n),await e.deleteItemAsync(n)}}}function A(e,t=`com.witsauth.tokens`){return{getItem:async n=>{let r=await e.getGenericPassword({service:`${t}.${n}`});return r?r.password:null},setItem:async(n,r)=>{await e.setGenericPassword(n,r,{service:`${t}.${n}`})},removeItem:async n=>{await e.resetGenericPassword({service:`${t}.${n}`})}}}function j(e){return async(t,n)=>{let r=await e.openAuthSessionAsync(t,n);return r.type===`success`&&r.url?{type:`success`,url:r.url}:r.type===`cancel`?{type:`cancel`}:{type:`dismiss`}}}function M(e){return async(t,n)=>{if(!await e.isAvailable())throw Error(`InAppBrowser is not available on this device.`);let r=await e.openAuth(t,n,{ephemeralWebSession:!1,showTitle:!1,enableUrlBarHiding:!0,enableDefaultShare:!1});return r.type===`success`&&r.url?{type:`success`,url:r.url}:r.type===`cancel`?{type:`cancel`}:{type:`dismiss`}}}function N(e){let t=e.indexOf(`?`);if(t===-1)return{code:null,state:null,error:null,errorDescription:null};let n=e.indexOf(`#`,t),r=n===-1?e.slice(t+1):e.slice(t+1,n),i=new URLSearchParams(r);return{code:i.get(`code`),state:i.get(`state`),error:i.get(`error`),errorDescription:i.get(`error_description`)}}var P=class{config;storage;refreshPromise=null;constructor(e,t){this.config=e,this.storage=t||new T(e.storage)}async initialize(){await this.storage.hydrate()}log(e,...t){let n={none:0,error:1,warn:2,info:3,debug:4},r=n[this.config.logLevel||`warn`]||0,i=n[e]||0;i>0&&r>=i&&console[e](...t)}async buildAuthorizationUrl(e){await this.storage.hydrate();let t=x(),n=await C(t),r=S(),i=this.config.nonce||S();await this.storage.setItem(u.CODE_VERIFIER,t),await this.storage.setItem(u.STATE,r),await this.storage.setItem(u.NONCE,i);let a=new URLSearchParams({client_id:this.config.clientId,redirect_uri:this.config.redirectUri,response_type:this.config.responseType||`code`,code_challenge_method:this.config.codeChallengeMethod||`S256`,code_challenge:n,state:r,nonce:i});if(this.config.theme){let e=typeof this.config.theme==`function`?this.config.theme():this.config.theme;e&&a.append(`theme`,e)}let o=e?.scope??this.config.scope,s=e?.audience??this.config.audience;return o&&a.append(`scope`,o),s&&a.append(`audience`,s),e?.prompt&&a.append(`prompt`,e.prompt),`${this.config.authorizationEndpoint}?${a.toString()}`}async authorize(e){if(!this.config.openAuthSession)throw Error(`No openAuthSession launcher configured. Pass createExpoWebBrowserLauncher(WebBrowser) or createInAppBrowserLauncher(InAppBrowser) in OAuth2NativeConfig.`);let t=await this.buildAuthorizationUrl(e),n=await this.config.openAuthSession(t,this.config.redirectUri);if(n.type!==`success`||!n.url)throw await this.storage.clearTemporary(),Error(`Authentication ${n.type}`);return this.handleCallbackUrl(n.url)}async handleCallbackUrl(e){let{code:t,state:n,error:r,errorDescription:i}=N(e);if(r)throw await this.storage.clearTemporary(),Error(i?`${r}: ${i}`:r);if(!t||!n)throw await this.storage.clearTemporary(),Error(`Callback URL is missing required code or state parameters.`);return this.exchangeCodeForTokens(t,n)}async exchangeCodeForTokens(e,t){await this.storage.hydrate();let n=await this.storage.getItem(u.STATE),r=await this.storage.getItem(u.CODE_VERIFIER);if(!n||t!==n)throw await this.storage.clearTemporary(),Error(`Invalid state parameter. Possible CSRF attack.`);if(!r)throw await this.storage.clearTemporary(),Error(`Code verifier missing from storage.`);let i=new URLSearchParams({grant_type:`authorization_code`,client_id:this.config.clientId,redirect_uri:this.config.redirectUri,code:e,code_verifier:r}),a=await fetch(this.config.tokenEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:i.toString()});if(!a.ok)throw Error(`Failed to exchange code for tokens`);let o=await a.json(),s={accessToken:o.access_token,refreshToken:o.refresh_token,tokenType:o.token_type||`Bearer`,expiresAt:Date.now()+o.expires_in*1e3,scope:o.scope,idToken:o.id_token};return await this.saveTokenInfo(s),await this.storage.clearTemporary(),s}async refreshAccessToken(){return this.refreshPromise||=(async()=>{await this.storage.hydrate();let e=await this.storage.getItem(u.REFRESH_TOKEN);if(!e)throw Error(`No refresh token available`);let t=new URLSearchParams({grant_type:`refresh_token`,client_id:this.config.clientId,refresh_token:e}),n;try{n=await fetch(this.config.tokenEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:t.toString()})}catch(e){throw this.log(`error`,`Token refresh network request failed:`,e),e}if(!n.ok){let e={};try{e=await n.json()}catch{try{e={error:await n.text()}}catch{e={}}}this.log(`error`,`Token refresh failed with status:`,n.status,e),(n.status===400||n.status===401)&&await this.storage.clearTokens();let t=e.error_description||e.error||n.statusText||`Failed to refresh token`;throw Error(`Failed to refresh token (${n.status}): ${t}`)}let r=await n.json(),i={accessToken:r.access_token,refreshToken:r.refresh_token||e,tokenType:r.token_type||`Bearer`,expiresAt:Date.now()+r.expires_in*1e3,scope:r.scope,idToken:r.id_token};return await this.saveTokenInfo(i),i})().finally(()=>{this.refreshPromise=null}),this.refreshPromise}async saveTokenInfo(e){await this.storage.setItem(u.ACCESS_TOKEN,e.accessToken),await this.storage.setItem(u.TOKEN_EXPIRES_AT,e.expiresAt.toString()),await this.storage.setItem(u.TOKEN_TYPE,e.tokenType),e.refreshToken&&await this.storage.setItem(u.REFRESH_TOKEN,e.refreshToken),e.scope&&await this.storage.setItem(u.SCOPE,e.scope),e.idToken&&await this.storage.setItem(u.ID_TOKEN,e.idToken)}getAccessToken(){return this.storage.getItemSync(u.ACCESS_TOKEN)}getIdToken(){return this.storage.getItemSync(u.ID_TOKEN)}getUserProfile(){return y(this.getIdToken()||this.getAccessToken())}getRefreshToken(){return this.storage.getItemSync(u.REFRESH_TOKEN)}getExpiresAt(){let e=this.storage.getItemSync(u.TOKEN_EXPIRES_AT);if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null}isAuthenticated(){let e=this.getExpiresAt();return e!==null&&Date.now()<e-3e4&&!!this.getAccessToken()}async navigateToAccountManagement(){await this.storage.hydrate();let e=await this.storage.getItem(u.REFRESH_TOKEN);if(!e)throw Error(`No refresh token available to access account management.`);if(!this.config.openAuthSession)throw Error(`No openAuthSession launcher configured. Pass createExpoWebBrowserLauncher(WebBrowser) or createInAppBrowserLauncher(InAppBrowser) in OAuth2NativeConfig.`);let t=this.config.authorizationEndpoint.replace(/\/authorize\/?$/,``),n=await fetch(`${t}/account/ticket`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({refreshToken:e,clientId:this.config.clientId,redirectUri:this.config.redirectUri})});if(!n.ok)throw Error(`Failed to request account management ticket (${n.status})`);let{ticket:r}=await n.json();if(!r)throw Error(`Account management ticket missing from server response.`);let i=`${t}/account?ticket=${encodeURIComponent(r)}&return_to=${encodeURIComponent(this.config.redirectUri)}`;await this.config.openAuthSession(i,this.config.redirectUri)}async logout(e){await this.storage.hydrate();let t=this.getAccessToken(),n=await this.storage.getItem(u.REFRESH_TOKEN);if(await this.storage.clearTokens(),await this.storage.clearTemporary(),this.config.revokeEndpoint){let e=[...n?[{token:n,token_type_hint:`refresh_token`}]:[],...t?[{token:t,token_type_hint:`access_token`}]:[]];for(let t of e)try{await fetch(this.config.revokeEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:new URLSearchParams({token:t.token,token_type_hint:t.token_type_hint,client_id:this.config.clientId}).toString()})}catch(e){this.log(`error`,`Failed to revoke token during logout`,e)}}if(e?.clearBrowserSession&&this.config.logoutEndpoint&&this.config.openAuthSession){let e=new URLSearchParams({client_id:this.config.clientId,redirect_uri:this.config.redirectUri}),t=this.config.logoutEndpoint.includes(`?`)?`&`:`?`,n=`${this.config.logoutEndpoint}${t}${e.toString()}`;try{await this.config.openAuthSession(n,this.config.redirectUri)}catch(e){this.log(`warn`,`Browser logout session dismissed or failed`,e)}}}},F=(0,t.createContext)(null),I=({config:e,client:r,children:i,onLoginSuccess:a,onLoginError:o,subscribeAppState:s})=>{let[c]=(0,t.useState)(()=>{if(r)return r;if(e)return new P(e);throw Error("AuthProvider requires either a `client` or `config` prop.")}),[l,u]=(0,t.useState)(!1),[d,f]=(0,t.useState)(null),[p,m]=(0,t.useState)(null),[h,g]=(0,t.useState)(null),[_,v]=(0,t.useState)(!0),[y,b]=(0,t.useState)(null),x=(0,t.useCallback)(async()=>{if(await c.initialize(),c.isAuthenticated())u(!0),f(c.getAccessToken()),m(c.getIdToken()),g(c.getUserProfile());else{if(c.getRefreshToken())try{let e=await c.refreshAccessToken();u(!0),f(e.accessToken),m(c.getIdToken()),g(c.getUserProfile());return}catch{u(!1),f(null),m(null),g(null);return}u(!1),f(null),m(null),g(null)}},[c]);(0,t.useEffect)(()=>{let e=!0;return(async()=>{try{await x()}finally{e&&v(!1)}})(),()=>{e=!1}},[x]),(0,t.useEffect)(()=>{if(!l)return;let e,t=()=>{let n=c.getExpiresAt();if(!n)return;let r=n-Date.now()-6e4;e=setTimeout(async()=>{try{let e=await c.refreshAccessToken();f(e.accessToken),m(c.getIdToken()),g(c.getUserProfile()),u(!0),t()}catch(n){c.log(`error`,`Background token refresh failed:`,n),c.getAccessToken()?e=setTimeout(t,3e4):(u(!1),f(null),m(null),g(null))}},Math.max(0,r))};t();let n=s?s(e=>{e===`active`&&x().catch(()=>{})}):void 0;return()=>{e&&clearTimeout(e),n&&n()}},[l,c,s,x]);let S=(0,t.useCallback)(async e=>{v(!0),b(null);try{let t=await c.authorize(e);f(t.accessToken),m(c.getIdToken()),g(c.getUserProfile()),u(!0),a?.()}catch(e){let t=e instanceof Error?e:Error(`Authentication failed`);b(t),o?.(t)}finally{v(!1)}},[c,a,o]),C=(0,t.useCallback)(async e=>{v(!0);try{await c.logout(e),u(!1),f(null),m(null),g(null)}catch(e){b(e instanceof Error?e:Error(`Logout failed`))}finally{v(!1)}},[c]),w=(0,t.useCallback)(async()=>{if(await c.initialize(),c.isAuthenticated())return c.getAccessToken();try{let e=await c.refreshAccessToken();return f(e.accessToken),m(c.getIdToken()),g(c.getUserProfile()),u(!0),e.accessToken}catch{return u(!1),f(null),m(null),g(null),null}},[c]),T=(0,t.useCallback)(async()=>{await c.navigateToAccountManagement(),await x()},[c,x]);return(0,n.jsx)(F.Provider,{value:{isAuthenticated:l,isLoading:_,accessToken:d,idToken:p,user:h,error:y,login:S,logout:C,getToken:w,navigateToAccountManagement:T,authClient:c},children:i})},L=()=>{let e=(0,t.useContext)(F);if(!e)throw Error(`useAuth must be used within an AuthProvider`);return e};function R(e,t,n={}){let r=!1,i=[],a=(e,t=null)=>{i.forEach(n=>{e?n.reject(e):n.resolve(t)}),i=[]},o=e=>!e||!n.excludeUrls?!1:n.excludeUrls.some(t=>typeof t==`string`?e.includes(t):t.test(e)),s=e.interceptors.request.use(async e=>{if(o(e.url))return e;await t.initialize();let n=t.getAccessToken();if(!t.isAuthenticated()&&n)try{n=(await t.refreshAccessToken()).accessToken}catch{n=null}return n&&e.headers.set(`Authorization`,`Bearer ${n}`),e},e=>Promise.reject(e)),c=e.interceptors.response.use(e=>e,async s=>{let c=s.config;if(!c||o(c.url))return Promise.reject(s);if(s.response?.status===401&&!c._retry){if(r)return new Promise((e,t)=>{i.push({resolve:e,reject:t})}).then(t=>(t&&c.headers.set(`Authorization`,`Bearer ${t}`),e(c))).catch(e=>Promise.reject(e));c._retry=!0,r=!0;try{let n=(await t.refreshAccessToken()).accessToken;return a(null,n),c.headers.set(`Authorization`,`Bearer ${n}`),e(c)}catch(e){return a(e,null),await t.logout(),n.onUnauthorized&&n.onUnauthorized(),Promise.reject(e)}finally{r=!1}}return Promise.reject(s)});return()=>{e.interceptors.request.eject(s),e.interceptors.response.eject(c)}}e.AuthClient=P,e.AuthProvider=I,e.NativeTokenStorage=T,e.STORAGE_KEYS=u,e.base64UrlEncode=g,e.createCodeChallenge=C,e.createExpoSecureStoreAdapter=k,e.createExpoWebBrowserLauncher=j,e.createInAppBrowserLauncher=M,e.createKeychainStorageAdapter=A,e.createMemoryStorageAdapter=E,e.decodeJwtPayload=y,e.generateCodeVerifier=x,e.generateRandomString=b,e.generateState=S,e.getRandomBytes=_,e.parseCallbackUrl=N,e.setupAxiosInterceptors=R,e.sha256Bytes=h,e.useAuth=L});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export interface OAuth2AsyncStorage {
|
|
2
|
+
getItem(key: string): Promise<string | null> | string | null;
|
|
3
|
+
setItem(key: string, value: string): Promise<void> | void;
|
|
4
|
+
removeItem(key: string): Promise<void> | void;
|
|
5
|
+
}
|
|
6
|
+
export interface BrowserSessionResult {
|
|
7
|
+
type: 'success' | 'cancel' | 'dismiss';
|
|
8
|
+
url?: string;
|
|
9
|
+
}
|
|
10
|
+
export type BrowserSessionLauncher = (authUrl: string, redirectUri: string) => Promise<BrowserSessionResult>;
|
|
11
|
+
export interface OAuth2NativeConfig {
|
|
12
|
+
/**
|
|
13
|
+
* The Client ID obtained from the WitsAuth console.
|
|
14
|
+
*/
|
|
15
|
+
clientId: string;
|
|
16
|
+
/**
|
|
17
|
+
* The authorization endpoint URL.
|
|
18
|
+
* E.g., https://auth.example.com/auth/authorize
|
|
19
|
+
*/
|
|
20
|
+
authorizationEndpoint: string;
|
|
21
|
+
/**
|
|
22
|
+
* The token endpoint URL.
|
|
23
|
+
* E.g., https://auth.example.com/oauth/token
|
|
24
|
+
*/
|
|
25
|
+
tokenEndpoint: string;
|
|
26
|
+
/**
|
|
27
|
+
* The callback URI configured for your mobile application.
|
|
28
|
+
* Supports custom schemes (e.g., myapp://auth/callback) and Universal/App Links.
|
|
29
|
+
*/
|
|
30
|
+
redirectUri: string;
|
|
31
|
+
/**
|
|
32
|
+
* Optional userInfo endpoint URL.
|
|
33
|
+
* E.g., https://auth.example.com/oauth/userinfo
|
|
34
|
+
*/
|
|
35
|
+
userInfoEndpoint?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Optional token revocation endpoint URL.
|
|
38
|
+
* E.g., https://auth.example.com/oauth/revoke
|
|
39
|
+
*/
|
|
40
|
+
revokeEndpoint?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Optional logout endpoint URL.
|
|
43
|
+
* E.g., https://auth.example.com/auth/logout
|
|
44
|
+
*/
|
|
45
|
+
logoutEndpoint?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Scopes to request, separated by spaces.
|
|
48
|
+
* E.g., "openid profile email offline_access"
|
|
49
|
+
*/
|
|
50
|
+
scope?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Audience parameter to request (if required by WitsAuth).
|
|
53
|
+
*/
|
|
54
|
+
audience?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Optional static nonce value. If omitted, a random cryptographic nonce is generated per authorization request.
|
|
57
|
+
*/
|
|
58
|
+
nonce?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Optional theme hint ("light" | "dark") or a callback returning the current theme.
|
|
61
|
+
*/
|
|
62
|
+
theme?: 'light' | 'dark' | (() => 'light' | 'dark' | undefined);
|
|
63
|
+
/**
|
|
64
|
+
* Response type. Defaults to "code".
|
|
65
|
+
*/
|
|
66
|
+
responseType?: string;
|
|
67
|
+
/**
|
|
68
|
+
* PKCE code challenge method. Defaults to "S256".
|
|
69
|
+
*/
|
|
70
|
+
codeChallengeMethod?: 'S256' | 'plain';
|
|
71
|
+
/**
|
|
72
|
+
* Secure async storage adapter (e.g., Expo SecureStore or React Native Keychain).
|
|
73
|
+
* If omitted, falls back to an in-memory store.
|
|
74
|
+
*/
|
|
75
|
+
storage?: OAuth2AsyncStorage;
|
|
76
|
+
/**
|
|
77
|
+
* Custom system browser session launcher (ASWebAuthenticationSession on iOS / Chrome Custom Tabs on Android).
|
|
78
|
+
* If omitted, auto-detects `expo-web-browser` or `react-native-inappbrowser-reborn`.
|
|
79
|
+
*/
|
|
80
|
+
openAuthSession?: BrowserSessionLauncher;
|
|
81
|
+
/**
|
|
82
|
+
* Log level for internal client messages. Defaults to 'warn'.
|
|
83
|
+
*/
|
|
84
|
+
logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug';
|
|
85
|
+
}
|
|
86
|
+
export interface TokenInfo {
|
|
87
|
+
accessToken: string;
|
|
88
|
+
refreshToken?: string;
|
|
89
|
+
expiresAt: number;
|
|
90
|
+
tokenType: string;
|
|
91
|
+
scope?: string;
|
|
92
|
+
idToken?: string;
|
|
93
|
+
}
|
|
94
|
+
export interface UserProfile {
|
|
95
|
+
sub?: string;
|
|
96
|
+
name?: string;
|
|
97
|
+
given_name?: string;
|
|
98
|
+
family_name?: string;
|
|
99
|
+
email?: string;
|
|
100
|
+
email_verified?: boolean;
|
|
101
|
+
phone_number?: string;
|
|
102
|
+
phone_number_verified?: boolean;
|
|
103
|
+
roles?: string[];
|
|
104
|
+
permissions?: string[];
|
|
105
|
+
amr?: string[];
|
|
106
|
+
act?: {
|
|
107
|
+
sub: string;
|
|
108
|
+
ticket_ref?: string;
|
|
109
|
+
[key: string]: unknown;
|
|
110
|
+
};
|
|
111
|
+
[key: string]: unknown;
|
|
112
|
+
}
|
|
113
|
+
export interface AuthorizeOptions {
|
|
114
|
+
prompt?: 'login' | 'consent' | 'select_account';
|
|
115
|
+
scope?: string;
|
|
116
|
+
audience?: string;
|
|
117
|
+
}
|
|
118
|
+
export interface LogoutOptions {
|
|
119
|
+
clearBrowserSession?: boolean;
|
|
120
|
+
}
|
|
121
|
+
export declare const STORAGE_KEYS: {
|
|
122
|
+
readonly ACCESS_TOKEN: 'witsauth_access_token';
|
|
123
|
+
readonly REFRESH_TOKEN: 'witsauth_refresh_token';
|
|
124
|
+
readonly TOKEN_EXPIRES_AT: 'witsauth_expires_at';
|
|
125
|
+
readonly TOKEN_TYPE: 'witsauth_token_type';
|
|
126
|
+
readonly SCOPE: 'witsauth_scope';
|
|
127
|
+
readonly ID_TOKEN: 'witsauth_id_token';
|
|
128
|
+
readonly CODE_VERIFIER: 'witsauth_code_verifier';
|
|
129
|
+
readonly STATE: 'witsauth_state';
|
|
130
|
+
readonly NONCE: 'witsauth_nonce';
|
|
131
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AuthClient } from '../core/AuthClient';
|
|
3
|
+
import { AuthorizeOptions, LogoutOptions, OAuth2NativeConfig, UserProfile } from '../models/types';
|
|
4
|
+
export interface AuthContextType {
|
|
5
|
+
isAuthenticated: boolean;
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
accessToken: string | null;
|
|
8
|
+
idToken: string | null;
|
|
9
|
+
user: UserProfile | null;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
login: (options?: AuthorizeOptions) => Promise<void>;
|
|
12
|
+
logout: (options?: LogoutOptions) => Promise<void>;
|
|
13
|
+
getToken: () => Promise<string | null>;
|
|
14
|
+
navigateToAccountManagement: () => Promise<void>;
|
|
15
|
+
authClient: AuthClient;
|
|
16
|
+
}
|
|
17
|
+
export interface AuthProviderProps {
|
|
18
|
+
config?: OAuth2NativeConfig;
|
|
19
|
+
client?: AuthClient;
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
onLoginSuccess?: () => void;
|
|
22
|
+
onLoginError?: (error: Error) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Optional subscription to React Native's `AppState.addEventListener('change', listener)`
|
|
25
|
+
* so tokens automatically refresh when the app returns to the foreground (`active`).
|
|
26
|
+
*/
|
|
27
|
+
subscribeAppState?: (listener: (state: string) => void) => () => void;
|
|
28
|
+
}
|
|
29
|
+
export declare const AuthProvider: React.FC<AuthProviderProps>;
|
|
30
|
+
export declare const useAuth: () => AuthContextType;
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witsauth/react-native-client",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Official React Native client library (Expo and Bare React Native CLI) for integrating WitsAuth OAuth 2.0 + PKCE authentication on Android and iOS.",
|
|
5
|
+
"main": "./dist/index.umd.js",
|
|
6
|
+
"module": "./dist/index.es.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.es.js",
|
|
11
|
+
"require": "./dist/index.umd.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"react-native",
|
|
23
|
+
"expo",
|
|
24
|
+
"android",
|
|
25
|
+
"ios",
|
|
26
|
+
"witsauth",
|
|
27
|
+
"auth",
|
|
28
|
+
"oauth2",
|
|
29
|
+
"pkce"
|
|
30
|
+
],
|
|
31
|
+
"author": "",
|
|
32
|
+
"license": "ISC",
|
|
33
|
+
"type": "module",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"axios": "^1.0.0",
|
|
36
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
37
|
+
"react-native": ">=0.72.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"axios": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"react-native": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@testing-library/jest-dom": "^7.0.0",
|
|
49
|
+
"@testing-library/react": "^16.3.2",
|
|
50
|
+
"@types/node": "^24.13.2",
|
|
51
|
+
"@types/react": "^19.2.17",
|
|
52
|
+
"axios": "^1.18.1",
|
|
53
|
+
"jsdom": "^29.1.1",
|
|
54
|
+
"react": "^19.2.7",
|
|
55
|
+
"typescript": "^7.0.2",
|
|
56
|
+
"vite": "^8.1.5",
|
|
57
|
+
"vitest": "^4.1.10"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"dev": "vite",
|
|
61
|
+
"build": "tsc && vite build && tsc -p tsconfig.build.json",
|
|
62
|
+
"test": "vitest run"
|
|
63
|
+
}
|
|
6
64
|
}
|