@authgate/browser 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -30
- package/dist/index.d.ts +43 -0
- package/dist/index.js +77 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,20 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Minimal browser-side helpers for applications using **AuthGate**.
|
|
4
4
|
|
|
5
|
-
This package provides
|
|
6
|
-
with an AuthGate-backed authentication
|
|
7
|
-
|
|
5
|
+
This package provides **explicit, framework-agnostic primitives** for integrating
|
|
6
|
+
browser-based UIs (SSR or SPA) with an AuthGate-backed authentication system.
|
|
7
|
+
|
|
8
|
+
It intentionally avoids hidden behavior, background state mutation, or
|
|
9
|
+
framework-specific abstractions.
|
|
8
10
|
|
|
9
11
|
---
|
|
10
12
|
|
|
11
|
-
##
|
|
13
|
+
## Design goals
|
|
12
14
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
15
|
+
- Explicit behavior (no magic, no background auth)
|
|
16
|
+
- Browser-only responsibility (cookies, CSRF, refresh)
|
|
17
|
+
- Framework-agnostic (React, Vue, Svelte, vanilla JS)
|
|
18
|
+
- Composable primitives + optional convenience helpers
|
|
17
19
|
- Zero dependencies
|
|
18
|
-
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- Read AuthGate CSRF token from browser cookies
|
|
26
|
+
- Perform a CSRF-protected logout request
|
|
27
|
+
- Explicit browser-side session refresh **with audience selection**
|
|
28
|
+
- Optional `fetch` wrapper with **single-retry refresh semantics**
|
|
29
|
+
- Clear success / failure signaling
|
|
30
|
+
- Optional redirect on logout
|
|
31
|
+
- No runtime dependencies
|
|
19
32
|
|
|
20
33
|
---
|
|
21
34
|
|
|
@@ -39,11 +52,12 @@ const csrf = getCSRFToken();
|
|
|
39
52
|
|
|
40
53
|
Returns the value of the `authgate_csrf` cookie, or `null` if not present.
|
|
41
54
|
|
|
42
|
-
This function only **reads** the CSRF token.
|
|
55
|
+
This function only **reads** the CSRF token.
|
|
56
|
+
It does not generate or validate it.
|
|
43
57
|
|
|
44
58
|
---
|
|
45
59
|
|
|
46
|
-
|
|
60
|
+
## Logout
|
|
47
61
|
|
|
48
62
|
```ts
|
|
49
63
|
import { logout } from "@authgate/browser";
|
|
@@ -57,7 +71,7 @@ This will:
|
|
|
57
71
|
- Attach the CSRF token via `X-CSRF-Token`
|
|
58
72
|
- Include credentials (`cookies`)
|
|
59
73
|
|
|
60
|
-
The function returns
|
|
74
|
+
The function returns an explicit result:
|
|
61
75
|
|
|
62
76
|
```ts
|
|
63
77
|
type LogoutResult =
|
|
@@ -65,8 +79,8 @@ type LogoutResult =
|
|
|
65
79
|
| { ok: false; reason: "missing_csrf" | "request_failed" | "unauthorized" };
|
|
66
80
|
```
|
|
67
81
|
|
|
68
|
-
Applications that do not need to react programmatically
|
|
69
|
-
|
|
82
|
+
Applications that do not need to react programmatically may safely ignore the
|
|
83
|
+
return value.
|
|
70
84
|
|
|
71
85
|
---
|
|
72
86
|
|
|
@@ -79,54 +93,149 @@ await logout({ redirectTo: "/" });
|
|
|
79
93
|
If the logout request succeeds, the browser is redirected to the given path.
|
|
80
94
|
|
|
81
95
|
Redirecting is an optional side-effect and does **not** define success.
|
|
82
|
-
Applications may choose to handle navigation themselves instead.
|
|
83
96
|
|
|
84
97
|
---
|
|
85
98
|
|
|
86
|
-
|
|
99
|
+
## Session refresh (explicit)
|
|
100
|
+
|
|
101
|
+
### `refreshSession`
|
|
87
102
|
|
|
88
103
|
```ts
|
|
89
|
-
|
|
104
|
+
import { refreshSession } from "@authgate/browser";
|
|
105
|
+
|
|
106
|
+
const refreshed = await refreshSession("app");
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Attempts to refresh the current AuthGate session by calling:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
POST /auth/refresh
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
with an explicit **audience declaration**.
|
|
116
|
+
|
|
117
|
+
### Audience
|
|
118
|
+
|
|
119
|
+
The audience determines **which access token is minted** (e.g. `"app"`, `"admin"`).
|
|
120
|
+
|
|
121
|
+
- The client explicitly requests an audience
|
|
122
|
+
- AuthGate validates the requested audience against the user’s roles
|
|
123
|
+
- Requests for unauthorized audiences fail with `401`
|
|
124
|
+
|
|
125
|
+
If no audience is provided, `"app"` is used by default.
|
|
126
|
+
|
|
127
|
+
### Behavior
|
|
128
|
+
|
|
129
|
+
- Returns `true` if refresh succeeded
|
|
130
|
+
- Returns `false` if refresh failed for any reason
|
|
131
|
+
|
|
132
|
+
This function:
|
|
133
|
+
|
|
134
|
+
- does **not** retry
|
|
135
|
+
- does **not** redirect
|
|
136
|
+
- does **not** throw
|
|
137
|
+
- does **not** modify application state
|
|
138
|
+
|
|
139
|
+
It is intended for applications that want **manual control** over refresh logic.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Fetch wrapper (optional convenience)
|
|
90
144
|
|
|
91
|
-
|
|
145
|
+
### `authFetch`
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { authFetch } from "@authgate/browser";
|
|
149
|
+
|
|
150
|
+
const res = await authFetch("/api/data");
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`authFetch` is an **optional convenience wrapper** around `fetch` with
|
|
154
|
+
AuthGate-aware refresh behavior.
|
|
155
|
+
|
|
156
|
+
### Behavior
|
|
157
|
+
|
|
158
|
+
1. Performs the request with credentials
|
|
159
|
+
2. If the response is **not `401`**, returns it directly
|
|
160
|
+
3. If the response **is `401`**:
|
|
161
|
+
- Attempts `refreshSession()` with the same audience
|
|
162
|
+
- If refresh succeeds, retries the original request **once**
|
|
163
|
+
- Otherwise, returns the original `401` response
|
|
164
|
+
|
|
165
|
+
### Audience-aware requests
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
await authFetch("/admin/api/users", {}, { audience: "admin" });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- The same audience is used for the refresh attempt
|
|
172
|
+
- Unauthorized audiences fail cleanly without retry loops
|
|
173
|
+
|
|
174
|
+
### Important properties
|
|
175
|
+
|
|
176
|
+
- At most **one retry**
|
|
177
|
+
- No redirects
|
|
178
|
+
- No background refresh
|
|
179
|
+
- No swallowed failures
|
|
180
|
+
|
|
181
|
+
Applications remain fully in control of UX decisions.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Example (React / SPA)
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const res = await authFetch("/api/me");
|
|
189
|
+
|
|
190
|
+
if (res.status === 401) {
|
|
92
191
|
setUser(null);
|
|
93
|
-
} else {
|
|
94
|
-
console.error("Logout failed:", result.reason);
|
|
95
192
|
}
|
|
96
193
|
```
|
|
97
194
|
|
|
195
|
+
Admin request:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const res = await authFetch(
|
|
199
|
+
"/admin/api/users",
|
|
200
|
+
{},
|
|
201
|
+
{ audience: "admin" },
|
|
202
|
+
);
|
|
203
|
+
```
|
|
204
|
+
|
|
98
205
|
---
|
|
99
206
|
|
|
100
|
-
## Security
|
|
207
|
+
## Security model
|
|
101
208
|
|
|
102
209
|
- CSRF tokens are **not generated** by this package
|
|
103
210
|
- CSRF validation is **enforced by AuthGate**
|
|
104
|
-
-
|
|
211
|
+
- Refresh tokens are **never exposed to JavaScript**
|
|
212
|
+
- All authentication state is owned by AuthGate
|
|
213
|
+
- Audiences are **explicitly requested and server-validated**
|
|
105
214
|
|
|
106
|
-
|
|
215
|
+
This package only forwards existing browser state explicitly.
|
|
107
216
|
|
|
108
217
|
---
|
|
109
218
|
|
|
110
|
-
## What
|
|
219
|
+
## What this package does NOT do
|
|
111
220
|
|
|
112
221
|
- No authentication logic
|
|
113
|
-
- No
|
|
222
|
+
- No credential storage
|
|
223
|
+
- No background token refresh
|
|
114
224
|
- No session management
|
|
225
|
+
- No authorization or role handling
|
|
115
226
|
- No implicit redirects
|
|
116
227
|
- No framework-specific helpers
|
|
117
228
|
|
|
118
|
-
This package exists solely to reduce boilerplate and prevent integration mistakes
|
|
229
|
+
This package exists solely to reduce boilerplate and prevent integration mistakes
|
|
230
|
+
while preserving full application control.
|
|
119
231
|
|
|
120
232
|
---
|
|
121
233
|
|
|
122
234
|
## Compatibility
|
|
123
235
|
|
|
124
236
|
- Works with any backend protected by AuthGate
|
|
125
|
-
-
|
|
237
|
+
- Supports SSR, SPA, and hybrid architectures
|
|
126
238
|
|
|
127
239
|
---
|
|
128
240
|
|
|
129
241
|
## License
|
|
130
|
-
|
|
131
|
-
MIT
|
|
132
|
-
|
package/dist/index.d.ts
CHANGED
|
@@ -37,4 +37,47 @@ type LogoutResult = {
|
|
|
37
37
|
export declare function logout(opts?: {
|
|
38
38
|
redirectTo?: string;
|
|
39
39
|
}): Promise<LogoutResult>;
|
|
40
|
+
/**
|
|
41
|
+
* authFetch performs a fetch request with AuthGate-aware, refresh-once behavior
|
|
42
|
+
* for a specific audience.
|
|
43
|
+
*
|
|
44
|
+
* Behavior:
|
|
45
|
+
* - Always includes credentials
|
|
46
|
+
* - Performs the initial request as-is
|
|
47
|
+
* - If the response is NOT 401, returns it directly
|
|
48
|
+
* - If the response IS 401:
|
|
49
|
+
* - Attempts POST /auth/refresh with CSRF and the same requested audience
|
|
50
|
+
* - If refresh succeeds, retries the original request ONCE
|
|
51
|
+
* - If refresh fails, returns the original 401 response
|
|
52
|
+
*
|
|
53
|
+
* authFetch never redirects or mutates application state. Callers are expected
|
|
54
|
+
* to handle authentication failures explicitly.
|
|
55
|
+
*
|
|
56
|
+
* @param input The resource to fetch (same as `fetch`).
|
|
57
|
+
* @param init Optional fetch options. Credentials are always included.
|
|
58
|
+
* @param opts Optional AuthGate options.
|
|
59
|
+
* @param opts.audience The audience for which the request is made (e.g. "app", "admin").
|
|
60
|
+
* Defaults to "app".
|
|
61
|
+
* @returns The final `Response` from the original request or the retried request.
|
|
62
|
+
*/
|
|
63
|
+
export declare function authFetch(input: RequestInfo | URL, init?: RequestInit, opts?: {
|
|
64
|
+
audience?: string;
|
|
65
|
+
}): Promise<Response>;
|
|
66
|
+
/**
|
|
67
|
+
* refreshSession attempts to refresh the current AuthGate session for a
|
|
68
|
+
* specific audience.
|
|
69
|
+
*
|
|
70
|
+
* It performs:
|
|
71
|
+
* POST /auth/refresh
|
|
72
|
+
* with CSRF protection, credentials, and an explicit audience declaration.
|
|
73
|
+
*
|
|
74
|
+
* The server validates the requested audience against the user's roles and
|
|
75
|
+
* rejects the request if the audience is not permitted.
|
|
76
|
+
*
|
|
77
|
+
* @param audience The audience for which a new access token should be minted.
|
|
78
|
+
* Defaults to "app".
|
|
79
|
+
* @returns `true` if the refresh succeeded, or `false` if refresh failed for
|
|
80
|
+
* any reason (unauthorized, expired session, or error).
|
|
81
|
+
*/
|
|
82
|
+
export declare function refreshSession(audience?: string): Promise<boolean>;
|
|
40
83
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -67,3 +67,80 @@ export async function logout(opts) {
|
|
|
67
67
|
}
|
|
68
68
|
return { ok: true };
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* authFetch performs a fetch request with AuthGate-aware, refresh-once behavior
|
|
72
|
+
* for a specific audience.
|
|
73
|
+
*
|
|
74
|
+
* Behavior:
|
|
75
|
+
* - Always includes credentials
|
|
76
|
+
* - Performs the initial request as-is
|
|
77
|
+
* - If the response is NOT 401, returns it directly
|
|
78
|
+
* - If the response IS 401:
|
|
79
|
+
* - Attempts POST /auth/refresh with CSRF and the same requested audience
|
|
80
|
+
* - If refresh succeeds, retries the original request ONCE
|
|
81
|
+
* - If refresh fails, returns the original 401 response
|
|
82
|
+
*
|
|
83
|
+
* authFetch never redirects or mutates application state. Callers are expected
|
|
84
|
+
* to handle authentication failures explicitly.
|
|
85
|
+
*
|
|
86
|
+
* @param input The resource to fetch (same as `fetch`).
|
|
87
|
+
* @param init Optional fetch options. Credentials are always included.
|
|
88
|
+
* @param opts Optional AuthGate options.
|
|
89
|
+
* @param opts.audience The audience for which the request is made (e.g. "app", "admin").
|
|
90
|
+
* Defaults to "app".
|
|
91
|
+
* @returns The final `Response` from the original request or the retried request.
|
|
92
|
+
*/
|
|
93
|
+
export async function authFetch(input, init = {}, opts) {
|
|
94
|
+
const audience = opts?.audience ?? "app";
|
|
95
|
+
const res = await fetch(input, withCredentials(init));
|
|
96
|
+
if (res.status !== 401) {
|
|
97
|
+
return res;
|
|
98
|
+
}
|
|
99
|
+
const refreshed = await refreshSession(audience);
|
|
100
|
+
if (!refreshed) {
|
|
101
|
+
return res;
|
|
102
|
+
}
|
|
103
|
+
return fetch(input, withCredentials(init));
|
|
104
|
+
}
|
|
105
|
+
function withCredentials(init) {
|
|
106
|
+
return {
|
|
107
|
+
...init,
|
|
108
|
+
credentials: "include",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* refreshSession attempts to refresh the current AuthGate session for a
|
|
113
|
+
* specific audience.
|
|
114
|
+
*
|
|
115
|
+
* It performs:
|
|
116
|
+
* POST /auth/refresh
|
|
117
|
+
* with CSRF protection, credentials, and an explicit audience declaration.
|
|
118
|
+
*
|
|
119
|
+
* The server validates the requested audience against the user's roles and
|
|
120
|
+
* rejects the request if the audience is not permitted.
|
|
121
|
+
*
|
|
122
|
+
* @param audience The audience for which a new access token should be minted.
|
|
123
|
+
* Defaults to "app".
|
|
124
|
+
* @returns `true` if the refresh succeeded, or `false` if refresh failed for
|
|
125
|
+
* any reason (unauthorized, expired session, or error).
|
|
126
|
+
*/
|
|
127
|
+
export async function refreshSession(audience = "app") {
|
|
128
|
+
const csrf = getCSRFToken();
|
|
129
|
+
if (!csrf) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
let res;
|
|
133
|
+
try {
|
|
134
|
+
res = await fetch(`/auth/refresh?audience=${encodeURIComponent(audience)}`, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: {
|
|
137
|
+
"X-CSRF-Token": csrf,
|
|
138
|
+
},
|
|
139
|
+
credentials: "include",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return res.ok;
|
|
146
|
+
}
|
package/package.json
CHANGED