@authu/react 0.1.17
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 +169 -0
- package/dist/AuthUProvider.d.ts +10 -0
- package/dist/AuthUProvider.d.ts.map +1 -0
- package/dist/AuthUProvider.js +250 -0
- package/dist/PrivateRoute.d.ts +9 -0
- package/dist/PrivateRoute.d.ts.map +1 -0
- package/dist/PrivateRoute.js +15 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/pkce.d.ts +14 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +66 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/useApiToken.d.ts +7 -0
- package/dist/useApiToken.d.ts.map +1 -0
- package/dist/useApiToken.js +23 -0
- package/dist/useAuthU.d.ts +3 -0
- package/dist/useAuthU.d.ts.map +1 -0
- package/dist/useAuthU.js +9 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# @authu/react
|
|
2
|
+
|
|
3
|
+
React SDK for AuthU - Centralized Multi-Tenant Authentication Service.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @authu/react
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @authu/react
|
|
11
|
+
# or
|
|
12
|
+
yarn add @authu/react
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### 1. Configure the Provider
|
|
18
|
+
|
|
19
|
+
Wrap your app with `AuthUProvider`:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import {AuthUProvider} from '@authu/react';
|
|
23
|
+
|
|
24
|
+
function App() {
|
|
25
|
+
return (
|
|
26
|
+
<AuthUProvider
|
|
27
|
+
domain="https://auth.example.com"
|
|
28
|
+
clientId="your-client-id"
|
|
29
|
+
redirectUri={window.location.origin + '/callback'}
|
|
30
|
+
>
|
|
31
|
+
<YourApp />
|
|
32
|
+
</AuthUProvider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Use the Hook
|
|
38
|
+
|
|
39
|
+
Access authentication state and methods with `useAuthU`:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import {useAuthU} from '@authu/react';
|
|
43
|
+
|
|
44
|
+
function Profile() {
|
|
45
|
+
const {isAuthenticated, isLoading, user, login, logout} = useAuthU();
|
|
46
|
+
|
|
47
|
+
if (isLoading) return <div>Loading...</div>;
|
|
48
|
+
|
|
49
|
+
if (!isAuthenticated) {
|
|
50
|
+
return <button onClick={() => login()}>Log in</button>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<p>Welcome, {user?.name}</p>
|
|
56
|
+
<button onClick={() => logout()}>Log out</button>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Protect Routes
|
|
63
|
+
|
|
64
|
+
Use `PrivateRoute` to protect authenticated routes:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import {PrivateRoute} from '@authu/react';
|
|
68
|
+
|
|
69
|
+
function AppRoutes() {
|
|
70
|
+
return (
|
|
71
|
+
<Routes>
|
|
72
|
+
<Route path="/" element={<Home />} />
|
|
73
|
+
<Route
|
|
74
|
+
path="/dashboard"
|
|
75
|
+
element={
|
|
76
|
+
<PrivateRoute>
|
|
77
|
+
<Dashboard />
|
|
78
|
+
</PrivateRoute>
|
|
79
|
+
}
|
|
80
|
+
/>
|
|
81
|
+
</Routes>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 4. Get Access Token for API Calls
|
|
87
|
+
|
|
88
|
+
Use `useApiToken` to get tokens for authenticated API requests:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import {useApiToken} from '@authu/react';
|
|
92
|
+
|
|
93
|
+
function ApiComponent() {
|
|
94
|
+
const {getToken} = useApiToken();
|
|
95
|
+
|
|
96
|
+
const fetchData = async () => {
|
|
97
|
+
const token = await getToken();
|
|
98
|
+
const response = await fetch('/api/data', {
|
|
99
|
+
headers: {
|
|
100
|
+
Authorization: `Bearer ${token}`,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
return response.json();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return <button onClick={fetchData}>Fetch Data</button>;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## API Reference
|
|
111
|
+
|
|
112
|
+
### AuthUProvider Props
|
|
113
|
+
|
|
114
|
+
| Prop | Type | Required | Description |
|
|
115
|
+
|------|------|----------|-------------|
|
|
116
|
+
| `domain` | `string` | Yes | AuthU server URL |
|
|
117
|
+
| `clientId` | `string` | Yes | OAuth2 client ID |
|
|
118
|
+
| `redirectUri` | `string` | Yes | Callback URL after login |
|
|
119
|
+
| `scope` | `string` | No | OAuth2 scopes (default: `openid profile email`) |
|
|
120
|
+
| `audience` | `string` | No | API audience for access tokens |
|
|
121
|
+
|
|
122
|
+
### useAuthU Returns
|
|
123
|
+
|
|
124
|
+
| Property | Type | Description |
|
|
125
|
+
|----------|------|-------------|
|
|
126
|
+
| `isLoading` | `boolean` | True while checking auth state |
|
|
127
|
+
| `isAuthenticated` | `boolean` | True if user is logged in |
|
|
128
|
+
| `user` | `AuthUUser \| null` | User profile info |
|
|
129
|
+
| `error` | `Error \| null` | Auth error if any |
|
|
130
|
+
| `login(options?)` | `function` | Redirect to login |
|
|
131
|
+
| `logout(options?)` | `function` | Log out user |
|
|
132
|
+
| `getAccessToken()` | `function` | Get current access token |
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
### Build
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
pnpm run build
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Lint
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
pnpm run lint
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Publishing
|
|
149
|
+
|
|
150
|
+
### Prerequisites
|
|
151
|
+
|
|
152
|
+
- Be logged in to npm: `npm login`
|
|
153
|
+
- Have publish rights on `@authu` scope
|
|
154
|
+
|
|
155
|
+
### Publish a New Version
|
|
156
|
+
|
|
157
|
+
1. Update version in `package.json`
|
|
158
|
+
2. Build and publish:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
pnpm run build
|
|
162
|
+
pnpm publish --access public
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The `--access public` flag is required for scoped packages.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { AuthUConfig, AuthUContextValue } from './types.js';
|
|
3
|
+
export declare const AuthUContext: import("react").Context<AuthUContextValue | null>;
|
|
4
|
+
interface AuthUProviderProps {
|
|
5
|
+
config: AuthUConfig;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export declare function AuthUProvider({ config, children }: AuthUProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=AuthUProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthUProvider.d.ts","sourceRoot":"","sources":["../src/AuthUProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EAGlB,MAAM,YAAY,CAAC;AAgBpB,eAAO,MAAM,YAAY,mDAAgD,CAAC;AAE1E,UAAU,kBAAkB;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAID,wBAAgB,aAAa,CAAC,EAAC,MAAM,EAAE,QAAQ,EAAC,EAAE,kBAAkB,2CAySnE"}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useEffect, useState, useCallback } from 'react';
|
|
3
|
+
import { generateCodeVerifier, generateCodeChallenge, generateState, generateNonce, storeCodeVerifier, storeState, storeNonce, getCodeVerifier, getStoredState, clearCodeVerifier, clearState, clearNonce } from './pkce.js';
|
|
4
|
+
export const AuthUContext = createContext(null);
|
|
5
|
+
const TOKEN_STORAGE_KEY = 'authu_tokens';
|
|
6
|
+
export function AuthUProvider({ config, children }) {
|
|
7
|
+
const [state, setState] = useState({
|
|
8
|
+
isLoading: true,
|
|
9
|
+
isAuthenticated: false,
|
|
10
|
+
user: null,
|
|
11
|
+
accessToken: null,
|
|
12
|
+
refreshToken: null,
|
|
13
|
+
expiresAt: null,
|
|
14
|
+
error: null
|
|
15
|
+
});
|
|
16
|
+
const getAuthorizationUrl = useCallback(async () => {
|
|
17
|
+
const codeVerifier = generateCodeVerifier();
|
|
18
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
19
|
+
const stateValue = generateState();
|
|
20
|
+
const nonceValue = generateNonce();
|
|
21
|
+
storeCodeVerifier(codeVerifier);
|
|
22
|
+
storeState(stateValue);
|
|
23
|
+
storeNonce(nonceValue);
|
|
24
|
+
const params = new URLSearchParams({
|
|
25
|
+
client_id: config.clientId,
|
|
26
|
+
redirect_uri: config.redirectUri,
|
|
27
|
+
response_type: 'code',
|
|
28
|
+
scope: config.scope || 'openid profile email',
|
|
29
|
+
state: stateValue,
|
|
30
|
+
nonce: nonceValue,
|
|
31
|
+
code_challenge: codeChallenge,
|
|
32
|
+
code_challenge_method: 'S256'
|
|
33
|
+
});
|
|
34
|
+
return `https://${config.domain}/authorize?${params.toString()}`;
|
|
35
|
+
}, [config]);
|
|
36
|
+
const exchangeCodeForTokens = useCallback(async (code) => {
|
|
37
|
+
const codeVerifier = getCodeVerifier();
|
|
38
|
+
if (!codeVerifier) {
|
|
39
|
+
throw new Error('No code verifier found');
|
|
40
|
+
}
|
|
41
|
+
const response = await fetch(`https://${config.domain}/oauth/token`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
grant_type: 'authorization_code',
|
|
46
|
+
code,
|
|
47
|
+
redirect_uri: config.redirectUri,
|
|
48
|
+
client_id: config.clientId,
|
|
49
|
+
code_verifier: codeVerifier
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const error = await response.json();
|
|
54
|
+
throw new Error(error.error_description || 'Token exchange failed');
|
|
55
|
+
}
|
|
56
|
+
const tokens = await response.json();
|
|
57
|
+
clearCodeVerifier();
|
|
58
|
+
clearState();
|
|
59
|
+
clearNonce();
|
|
60
|
+
return tokens;
|
|
61
|
+
}, [config]);
|
|
62
|
+
const parseIdToken = (idToken) => {
|
|
63
|
+
const parts = idToken.split('.');
|
|
64
|
+
if (parts.length !== 3) {
|
|
65
|
+
throw new Error('Invalid ID token');
|
|
66
|
+
}
|
|
67
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
68
|
+
return {
|
|
69
|
+
sub: payload.sub,
|
|
70
|
+
email: payload.email,
|
|
71
|
+
emailVerified: payload.email_verified,
|
|
72
|
+
name: payload.name,
|
|
73
|
+
picture: payload.picture
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
const login = useCallback(async () => {
|
|
77
|
+
const url = await getAuthorizationUrl();
|
|
78
|
+
window.location.href = url;
|
|
79
|
+
}, [getAuthorizationUrl]);
|
|
80
|
+
const logout = useCallback(() => {
|
|
81
|
+
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
82
|
+
setState({
|
|
83
|
+
isLoading: false,
|
|
84
|
+
isAuthenticated: false,
|
|
85
|
+
user: null,
|
|
86
|
+
accessToken: null,
|
|
87
|
+
refreshToken: null,
|
|
88
|
+
expiresAt: null,
|
|
89
|
+
error: null
|
|
90
|
+
});
|
|
91
|
+
}, []);
|
|
92
|
+
const fetchUserInfo = useCallback(async (accessToken) => {
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(`https://${config.domain}/oauth/userinfo`, {
|
|
95
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok)
|
|
98
|
+
return null;
|
|
99
|
+
const userInfo = await response.json();
|
|
100
|
+
return {
|
|
101
|
+
sub: userInfo.sub,
|
|
102
|
+
email: userInfo.email,
|
|
103
|
+
emailVerified: userInfo.email_verified,
|
|
104
|
+
name: userInfo.name,
|
|
105
|
+
picture: userInfo.picture
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}, [config.domain]);
|
|
112
|
+
const refreshTokens = useCallback(async () => {
|
|
113
|
+
if (!state.refreshToken)
|
|
114
|
+
return false;
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetch(`https://${config.domain}/oauth/token`, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { 'Content-Type': 'application/json' },
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
grant_type: 'refresh_token',
|
|
121
|
+
refresh_token: state.refreshToken,
|
|
122
|
+
client_id: config.clientId
|
|
123
|
+
})
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
logout();
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const tokens = await response.json();
|
|
130
|
+
const expiresAt = Date.now() + tokens.expires_in * 1000;
|
|
131
|
+
const user = tokens.id_token ? parseIdToken(tokens.id_token) : state.user;
|
|
132
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify({
|
|
133
|
+
accessToken: tokens.access_token,
|
|
134
|
+
refreshToken: tokens.refresh_token,
|
|
135
|
+
expiresAt
|
|
136
|
+
}));
|
|
137
|
+
setState(prev => ({
|
|
138
|
+
...prev,
|
|
139
|
+
accessToken: tokens.access_token,
|
|
140
|
+
refreshToken: tokens.refresh_token,
|
|
141
|
+
expiresAt,
|
|
142
|
+
user: user || prev.user
|
|
143
|
+
}));
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
logout();
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}, [state.refreshToken, state.user, config.domain, config.clientId, logout]);
|
|
151
|
+
const getAccessToken = useCallback(async () => {
|
|
152
|
+
if (!state.accessToken)
|
|
153
|
+
return null;
|
|
154
|
+
if (state.expiresAt && Date.now() >= state.expiresAt - 60000) {
|
|
155
|
+
const refreshed = await refreshTokens();
|
|
156
|
+
if (!refreshed)
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return state.accessToken;
|
|
160
|
+
}, [state.accessToken, state.expiresAt, refreshTokens]);
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const handleCallback = async () => {
|
|
163
|
+
const params = new URLSearchParams(window.location.search);
|
|
164
|
+
const code = params.get('code');
|
|
165
|
+
const returnedState = params.get('state');
|
|
166
|
+
const error = params.get('error');
|
|
167
|
+
if (error) {
|
|
168
|
+
setState(prev => ({
|
|
169
|
+
...prev,
|
|
170
|
+
isLoading: false,
|
|
171
|
+
error: new Error(params.get('error_description') || error)
|
|
172
|
+
}));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (code) {
|
|
176
|
+
const storedState = getStoredState();
|
|
177
|
+
if (returnedState !== storedState) {
|
|
178
|
+
setState(prev => ({
|
|
179
|
+
...prev,
|
|
180
|
+
isLoading: false,
|
|
181
|
+
error: new Error('Invalid state parameter')
|
|
182
|
+
}));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const tokens = await exchangeCodeForTokens(code);
|
|
187
|
+
const user = parseIdToken(tokens.id_token);
|
|
188
|
+
const expiresAt = Date.now() + tokens.expires_in * 1000;
|
|
189
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify({
|
|
190
|
+
accessToken: tokens.access_token,
|
|
191
|
+
refreshToken: tokens.refresh_token,
|
|
192
|
+
expiresAt
|
|
193
|
+
}));
|
|
194
|
+
setState({
|
|
195
|
+
isLoading: false,
|
|
196
|
+
isAuthenticated: true,
|
|
197
|
+
user,
|
|
198
|
+
accessToken: tokens.access_token,
|
|
199
|
+
refreshToken: tokens.refresh_token,
|
|
200
|
+
expiresAt,
|
|
201
|
+
error: null
|
|
202
|
+
});
|
|
203
|
+
window.history.replaceState({}, document.title, window.location.pathname);
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
setState(prev => ({
|
|
207
|
+
...prev,
|
|
208
|
+
isLoading: false,
|
|
209
|
+
error: err instanceof Error ? err : new Error('Authentication failed')
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const stored = localStorage.getItem(TOKEN_STORAGE_KEY);
|
|
215
|
+
if (stored) {
|
|
216
|
+
try {
|
|
217
|
+
const tokens = JSON.parse(stored);
|
|
218
|
+
if (tokens.expiresAt > Date.now()) {
|
|
219
|
+
const user = await fetchUserInfo(tokens.accessToken);
|
|
220
|
+
setState({
|
|
221
|
+
isLoading: false,
|
|
222
|
+
isAuthenticated: true,
|
|
223
|
+
user,
|
|
224
|
+
accessToken: tokens.accessToken,
|
|
225
|
+
refreshToken: tokens.refreshToken,
|
|
226
|
+
expiresAt: tokens.expiresAt,
|
|
227
|
+
error: null
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
setState(prev => ({ ...prev, isLoading: false }));
|
|
237
|
+
};
|
|
238
|
+
handleCallback();
|
|
239
|
+
}, [exchangeCodeForTokens, fetchUserInfo]);
|
|
240
|
+
const contextValue = {
|
|
241
|
+
isLoading: state.isLoading,
|
|
242
|
+
isAuthenticated: state.isAuthenticated,
|
|
243
|
+
user: state.user,
|
|
244
|
+
error: state.error,
|
|
245
|
+
login,
|
|
246
|
+
logout,
|
|
247
|
+
getAccessToken
|
|
248
|
+
};
|
|
249
|
+
return (_jsx(AuthUContext.Provider, { value: contextValue, children: children }));
|
|
250
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
interface PrivateRouteProps {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
fallback?: ReactNode;
|
|
5
|
+
loginOnUnauthenticated?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function PrivateRoute({ children, fallback, loginOnUnauthenticated }: PrivateRouteProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element | null;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=PrivateRoute.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PrivateRoute.d.ts","sourceRoot":"","sources":["../src/PrivateRoute.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAGrC,UAAU,iBAAiB;IACzB,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,QAAe,EACf,sBAA6B,EAC9B,EAAE,iBAAiB,+TAenB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useAuthU } from './useAuthU.js';
|
|
3
|
+
export function PrivateRoute({ children, fallback = null, loginOnUnauthenticated = true }) {
|
|
4
|
+
const { isLoading, isAuthenticated, login } = useAuthU();
|
|
5
|
+
if (isLoading) {
|
|
6
|
+
return fallback;
|
|
7
|
+
}
|
|
8
|
+
if (!isAuthenticated) {
|
|
9
|
+
if (loginOnUnauthenticated) {
|
|
10
|
+
login();
|
|
11
|
+
}
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
return _jsx(_Fragment, { children: children });
|
|
15
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { AuthUProvider } from './AuthUProvider.js';
|
|
2
|
+
export { useAuthU } from './useAuthU.js';
|
|
3
|
+
export { useApiToken } from './useApiToken.js';
|
|
4
|
+
export { PrivateRoute } from './PrivateRoute.js';
|
|
5
|
+
export * from './pkce.js';
|
|
6
|
+
export type { AuthUConfig, AuthUUser, AuthUContextValue } from './types.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAC,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAC,QAAQ,EAAC,MAAM,eAAe,CAAC;AACvC,OAAO,EAAC,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAC/C,cAAc,WAAW,CAAC;AAC1B,YAAY,EAAC,WAAW,EAAE,SAAS,EAAE,iBAAiB,EAAC,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
package/dist/pkce.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function generateCodeVerifier(): string;
|
|
2
|
+
export declare function generateCodeChallenge(verifier: string): Promise<string>;
|
|
3
|
+
export declare function generateState(): string;
|
|
4
|
+
export declare function generateNonce(): string;
|
|
5
|
+
export declare function storeCodeVerifier(verifier: string): void;
|
|
6
|
+
export declare function getCodeVerifier(): string | null;
|
|
7
|
+
export declare function clearCodeVerifier(): void;
|
|
8
|
+
export declare function storeState(state: string): void;
|
|
9
|
+
export declare function getStoredState(): string | null;
|
|
10
|
+
export declare function clearState(): void;
|
|
11
|
+
export declare function storeNonce(nonce: string): void;
|
|
12
|
+
export declare function getStoredNonce(): string | null;
|
|
13
|
+
export declare function clearNonce(): void;
|
|
14
|
+
//# sourceMappingURL=pkce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"AA2BA,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG7E;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAMD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAExD;AAED,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAE/C;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;AAED,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9C;AAED,wBAAgB,UAAU,IAAI,IAAI,CAEjC;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;AAED,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9C;AAED,wBAAgB,UAAU,IAAI,IAAI,CAEjC"}
|
package/dist/pkce.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function generateRandomString(length) {
|
|
2
|
+
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
3
|
+
const randomValues = crypto.getRandomValues(new Uint8Array(length));
|
|
4
|
+
return Array.from(randomValues)
|
|
5
|
+
.map(v => charset[v % charset.length])
|
|
6
|
+
.join('');
|
|
7
|
+
}
|
|
8
|
+
async function sha256(plain) {
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
const data = encoder.encode(plain);
|
|
11
|
+
return crypto.subtle.digest('SHA-256', data);
|
|
12
|
+
}
|
|
13
|
+
function base64UrlEncode(buffer) {
|
|
14
|
+
const bytes = new Uint8Array(buffer);
|
|
15
|
+
let binary = '';
|
|
16
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
17
|
+
binary += String.fromCharCode(bytes[i]);
|
|
18
|
+
}
|
|
19
|
+
return btoa(binary)
|
|
20
|
+
.replace(/\+/g, '-')
|
|
21
|
+
.replace(/\//g, '_')
|
|
22
|
+
.replace(/=+$/, '');
|
|
23
|
+
}
|
|
24
|
+
export function generateCodeVerifier() {
|
|
25
|
+
return generateRandomString(64);
|
|
26
|
+
}
|
|
27
|
+
export async function generateCodeChallenge(verifier) {
|
|
28
|
+
const hash = await sha256(verifier);
|
|
29
|
+
return base64UrlEncode(hash);
|
|
30
|
+
}
|
|
31
|
+
export function generateState() {
|
|
32
|
+
return generateRandomString(32);
|
|
33
|
+
}
|
|
34
|
+
export function generateNonce() {
|
|
35
|
+
return generateRandomString(32);
|
|
36
|
+
}
|
|
37
|
+
const STORAGE_KEY_VERIFIER = 'authu_code_verifier';
|
|
38
|
+
const STORAGE_KEY_STATE = 'authu_state';
|
|
39
|
+
const STORAGE_KEY_NONCE = 'authu_nonce';
|
|
40
|
+
export function storeCodeVerifier(verifier) {
|
|
41
|
+
sessionStorage.setItem(STORAGE_KEY_VERIFIER, verifier);
|
|
42
|
+
}
|
|
43
|
+
export function getCodeVerifier() {
|
|
44
|
+
return sessionStorage.getItem(STORAGE_KEY_VERIFIER);
|
|
45
|
+
}
|
|
46
|
+
export function clearCodeVerifier() {
|
|
47
|
+
sessionStorage.removeItem(STORAGE_KEY_VERIFIER);
|
|
48
|
+
}
|
|
49
|
+
export function storeState(state) {
|
|
50
|
+
sessionStorage.setItem(STORAGE_KEY_STATE, state);
|
|
51
|
+
}
|
|
52
|
+
export function getStoredState() {
|
|
53
|
+
return sessionStorage.getItem(STORAGE_KEY_STATE);
|
|
54
|
+
}
|
|
55
|
+
export function clearState() {
|
|
56
|
+
sessionStorage.removeItem(STORAGE_KEY_STATE);
|
|
57
|
+
}
|
|
58
|
+
export function storeNonce(nonce) {
|
|
59
|
+
sessionStorage.setItem(STORAGE_KEY_NONCE, nonce);
|
|
60
|
+
}
|
|
61
|
+
export function getStoredNonce() {
|
|
62
|
+
return sessionStorage.getItem(STORAGE_KEY_NONCE);
|
|
63
|
+
}
|
|
64
|
+
export function clearNonce() {
|
|
65
|
+
sessionStorage.removeItem(STORAGE_KEY_NONCE);
|
|
66
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface AuthUConfig {
|
|
2
|
+
domain: string;
|
|
3
|
+
clientId: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
scope?: string;
|
|
6
|
+
audience?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AuthUUser {
|
|
9
|
+
sub: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
emailVerified?: boolean;
|
|
12
|
+
name?: string;
|
|
13
|
+
picture?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface AuthUContextValue {
|
|
16
|
+
isLoading: boolean;
|
|
17
|
+
isAuthenticated: boolean;
|
|
18
|
+
user: AuthUUser | null;
|
|
19
|
+
error: Error | null;
|
|
20
|
+
login: (options?: LoginOptions) => void;
|
|
21
|
+
logout: (options?: LogoutOptions) => void;
|
|
22
|
+
getAccessToken: () => Promise<string | null>;
|
|
23
|
+
}
|
|
24
|
+
export interface LoginOptions {
|
|
25
|
+
returnTo?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface LogoutOptions {
|
|
28
|
+
returnTo?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface TokenResponse {
|
|
31
|
+
accessToken: string;
|
|
32
|
+
refreshToken?: string;
|
|
33
|
+
idToken?: string;
|
|
34
|
+
expiresIn: number;
|
|
35
|
+
tokenType: string;
|
|
36
|
+
}
|
|
37
|
+
export interface AuthState {
|
|
38
|
+
isLoading: boolean;
|
|
39
|
+
isAuthenticated: boolean;
|
|
40
|
+
user: AuthUUser | null;
|
|
41
|
+
accessToken: string | null;
|
|
42
|
+
refreshToken: string | null;
|
|
43
|
+
expiresAt: number | null;
|
|
44
|
+
error: Error | null;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface UseApiTokenReturn {
|
|
2
|
+
getToken: () => Promise<string | null>;
|
|
3
|
+
fetchWithToken: (url: string, options?: RequestInit) => Promise<Response>;
|
|
4
|
+
}
|
|
5
|
+
export declare function useApiToken(): UseApiTokenReturn;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=useApiToken.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useApiToken.d.ts","sourceRoot":"","sources":["../src/useApiToken.ts"],"names":[],"mappings":"AAGA,UAAU,iBAAiB;IACzB,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC3E;AAED,wBAAgB,WAAW,IAAI,iBAAiB,CA4B/C"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useAuthU } from './useAuthU.js';
|
|
3
|
+
export function useApiToken() {
|
|
4
|
+
const { getAccessToken, isAuthenticated } = useAuthU();
|
|
5
|
+
const getToken = useCallback(async () => {
|
|
6
|
+
if (!isAuthenticated) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
return getAccessToken();
|
|
10
|
+
}, [isAuthenticated, getAccessToken]);
|
|
11
|
+
const fetchWithToken = useCallback(async (url, options = {}) => {
|
|
12
|
+
const token = await getToken();
|
|
13
|
+
const headers = new Headers(options.headers);
|
|
14
|
+
if (token) {
|
|
15
|
+
headers.set('Authorization', `Bearer ${token}`);
|
|
16
|
+
}
|
|
17
|
+
return fetch(url, {
|
|
18
|
+
...options,
|
|
19
|
+
headers
|
|
20
|
+
});
|
|
21
|
+
}, [getToken]);
|
|
22
|
+
return { getToken, fetchWithToken };
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAuthU.d.ts","sourceRoot":"","sources":["../src/useAuthU.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,YAAY,CAAC;AAElD,wBAAgB,QAAQ,IAAI,iBAAiB,CAQ5C"}
|
package/dist/useAuthU.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { AuthUContext } from './AuthUProvider.js';
|
|
3
|
+
export function useAuthU() {
|
|
4
|
+
const context = useContext(AuthUContext);
|
|
5
|
+
if (!context) {
|
|
6
|
+
throw new Error('useAuthU must be used within an AuthUProvider');
|
|
7
|
+
}
|
|
8
|
+
return context;
|
|
9
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@authu/react",
|
|
3
|
+
"version": "0.1.17",
|
|
4
|
+
"description": "React SDK for AuthU - Centralized Multi-Tenant Authentication Service",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"authu",
|
|
19
|
+
"auth",
|
|
20
|
+
"authentication",
|
|
21
|
+
"oauth2",
|
|
22
|
+
"oidc",
|
|
23
|
+
"react",
|
|
24
|
+
"hooks"
|
|
25
|
+
],
|
|
26
|
+
"author": "Uralys",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@eslint/js": "^9.36.0",
|
|
33
|
+
"@types/react": "^19.0.4",
|
|
34
|
+
"eslint": "^9.36.0",
|
|
35
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
36
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
37
|
+
"prettier": "^3.6.2",
|
|
38
|
+
"react": "^19.0.0",
|
|
39
|
+
"typescript": "^5.7.3",
|
|
40
|
+
"typescript-eslint": "^8.44.1"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc",
|
|
44
|
+
"dev": "tsc --watch",
|
|
45
|
+
"eslint": "eslint src --cache",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"lint": "pnpm run eslint && pnpm run typecheck"
|
|
48
|
+
}
|
|
49
|
+
}
|