@nuria-tech/auth-sdk 1.0.0 → 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/LICENSE +21 -21
- package/README.md +270 -270
- package/dist/index.cjs +41 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +41 -29
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Nuria Tech
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nuria Tech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,270 +1,270 @@
|
|
|
1
|
-
# @nuria-tech/auth-sdk
|
|
2
|
-
|
|
3
|
-
A minimal, browser-native TypeScript SDK for OAuth 2.0 Authorization Code flow with PKCE. Redirect-only — no embedded UI, no password flows.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- Authorization Code flow + PKCE (S256, mandatory)
|
|
8
|
-
- State parameter generation and validation
|
|
9
|
-
- Token exchange via standard form-encoded request
|
|
10
|
-
- Automatic token refresh (optional)
|
|
11
|
-
- Pluggable storage adapters (memory, sessionStorage, cookies)
|
|
12
|
-
- Configurable HTTP transport with retry and interceptors
|
|
13
|
-
- Zero production dependencies — uses Web Crypto API and fetch
|
|
14
|
-
|
|
15
|
-
## Installation
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install @nuria-tech/auth-sdk
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Published on [npm](https://www.npmjs.com/package/@nuria-tech/auth-sdk).
|
|
22
|
-
|
|
23
|
-
## Quick start
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
import { createAuthClient } from '@nuria-tech/auth-sdk';
|
|
27
|
-
|
|
28
|
-
const auth = createAuthClient({
|
|
29
|
-
clientId: 'your-client-id',
|
|
30
|
-
authorizationEndpoint: 'https://your-auth-server.example.com/authorize',
|
|
31
|
-
tokenEndpoint: 'https://your-auth-server.example.com/token',
|
|
32
|
-
redirectUri: 'https://your-app.example.com/callback',
|
|
33
|
-
scope: 'openid profile email',
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Redirect to login
|
|
37
|
-
await auth.startLogin();
|
|
38
|
-
|
|
39
|
-
// In your callback page
|
|
40
|
-
const session = await auth.handleRedirectCallback(window.location.href);
|
|
41
|
-
console.log(session.tokens.accessToken);
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Configuration
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
interface AuthConfig {
|
|
48
|
-
// Required
|
|
49
|
-
clientId: string;
|
|
50
|
-
authorizationEndpoint: string;
|
|
51
|
-
tokenEndpoint: string;
|
|
52
|
-
redirectUri: string;
|
|
53
|
-
|
|
54
|
-
// Optional
|
|
55
|
-
scope?: string; // default scope sent with every login
|
|
56
|
-
logoutEndpoint?: string; // if set, logout() redirects here
|
|
57
|
-
userinfoEndpoint?: string; // required for getUserinfo()
|
|
58
|
-
storage?: StorageAdapter; // default: MemoryStorageAdapter
|
|
59
|
-
transport?: AuthTransport; // default: FetchAuthTransport
|
|
60
|
-
onRedirect?: (url: string) => void | Promise<void>; // override browser redirect
|
|
61
|
-
enableRefreshToken?: boolean; // enable automatic token refresh
|
|
62
|
-
now?: () => number; // override Date.now() for testing
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
> **Security note:** Do not include `clientSecret` in browser apps. This SDK is designed for public clients (SPAs, mobile). PKCE provides the proof of possession without a client secret.
|
|
67
|
-
|
|
68
|
-
## Public API
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
interface AuthClient {
|
|
72
|
-
startLogin(options?: StartLoginOptions): Promise<void>;
|
|
73
|
-
handleRedirectCallback(callbackUrl?: string): Promise<Session>;
|
|
74
|
-
getSession(): Session | null;
|
|
75
|
-
getAccessToken(): Promise<string | null>;
|
|
76
|
-
logout(options?: { returnTo?: string }): Promise<void>;
|
|
77
|
-
isAuthenticated(): boolean;
|
|
78
|
-
onAuthStateChanged(handler: (session: Session | null) => void): () => void;
|
|
79
|
-
getUserinfo(): Promise<Record<string, unknown>>;
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### `startLogin(options?)`
|
|
84
|
-
|
|
85
|
-
Generates PKCE `code_verifier` + `code_challenge` (S256), stores them in the configured storage, and redirects to `authorizationEndpoint`. The `state` parameter is always included and validated on callback.
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
await auth.startLogin({
|
|
89
|
-
scopes: ['openid', 'profile'], // overrides config.scope
|
|
90
|
-
loginHint: 'user@example.com',
|
|
91
|
-
extraParams: { prompt: 'login' },
|
|
92
|
-
});
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### `handleRedirectCallback(callbackUrl?)`
|
|
96
|
-
|
|
97
|
-
Parses the callback URL, validates `state`, exchanges `code` for tokens using a form-encoded POST to `tokenEndpoint`, and clears transient PKCE storage. Throws typed `AuthError` on any failure.
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
// In your /callback route
|
|
101
|
-
const session = await auth.handleRedirectCallback(window.location.href);
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### `getAccessToken()`
|
|
105
|
-
|
|
106
|
-
Returns the current access token. If the token is expired and `enableRefreshToken: true`, automatically refreshes it (concurrent calls are deduplicated).
|
|
107
|
-
|
|
108
|
-
### `logout(options?)`
|
|
109
|
-
|
|
110
|
-
Clears the local session. If `logoutEndpoint` is configured, redirects to it. Validates `returnTo` to prevent open redirect attacks.
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
await auth.logout({ returnTo: 'https://your-app.example.com' });
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### `onAuthStateChanged(handler)`
|
|
117
|
-
|
|
118
|
-
Subscribes to session changes. Returns an unsubscribe function.
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
const unsubscribe = auth.onAuthStateChanged((session) => {
|
|
122
|
-
console.log(session ? 'logged in' : 'logged out');
|
|
123
|
-
});
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
## Storage adapters
|
|
127
|
-
|
|
128
|
-
Default is `MemoryStorageAdapter` (most secure for XSS resistance, clears on reload).
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
import { WebStorageAdapter } from '@nuria-tech/auth-sdk';
|
|
132
|
-
|
|
133
|
-
// sessionStorage: persists per tab, JS-readable
|
|
134
|
-
const auth = createAuthClient({
|
|
135
|
-
...config,
|
|
136
|
-
storage: new WebStorageAdapter(window.sessionStorage),
|
|
137
|
-
});
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Cookie adapter for SSR:
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
import { CookieStorageAdapter } from '@nuria-tech/auth-sdk';
|
|
144
|
-
|
|
145
|
-
const auth = createAuthClient({
|
|
146
|
-
...config,
|
|
147
|
-
storage: new CookieStorageAdapter({
|
|
148
|
-
getCookie: async (name) => getCookieFromRequest(name),
|
|
149
|
-
setCookie: async (name, value) => setResponseCookie(name, value),
|
|
150
|
-
removeCookie: async (name) => clearResponseCookie(name),
|
|
151
|
-
}),
|
|
152
|
-
});
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## Transport customization
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
import { FetchAuthTransport } from '@nuria-tech/auth-sdk';
|
|
159
|
-
|
|
160
|
-
const auth = createAuthClient({
|
|
161
|
-
...config,
|
|
162
|
-
transport: new FetchAuthTransport({
|
|
163
|
-
timeoutMs: 10_000,
|
|
164
|
-
retries: 1,
|
|
165
|
-
interceptors: [
|
|
166
|
-
{
|
|
167
|
-
onRequest: async (_url, req) => ({
|
|
168
|
-
...req,
|
|
169
|
-
headers: { ...(req.headers ?? {}), 'X-App-Version': '1.0.0' },
|
|
170
|
-
}),
|
|
171
|
-
},
|
|
172
|
-
],
|
|
173
|
-
}),
|
|
174
|
-
});
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## React example
|
|
178
|
-
|
|
179
|
-
```tsx
|
|
180
|
-
import { useMemo, useState, useEffect } from 'react';
|
|
181
|
-
import { createAuthClient } from '@nuria-tech/auth-sdk';
|
|
182
|
-
|
|
183
|
-
const auth = createAuthClient({
|
|
184
|
-
clientId: 'your-client-id',
|
|
185
|
-
authorizationEndpoint: 'https://auth.example.com/authorize',
|
|
186
|
-
tokenEndpoint: 'https://auth.example.com/token',
|
|
187
|
-
redirectUri: `${window.location.origin}/callback`,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
export function App() {
|
|
191
|
-
const [session, setSession] = useState(auth.getSession());
|
|
192
|
-
|
|
193
|
-
useEffect(() => auth.onAuthStateChanged(setSession), []);
|
|
194
|
-
|
|
195
|
-
if (!session) {
|
|
196
|
-
return <button onClick={() => auth.startLogin()}>Login</button>;
|
|
197
|
-
}
|
|
198
|
-
return <button onClick={() => auth.logout()}>Logout</button>;
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## Next.js SSR example
|
|
203
|
-
|
|
204
|
-
```ts
|
|
205
|
-
// lib/auth.ts
|
|
206
|
-
import { createAuthClient, CookieStorageAdapter } from '@nuria-tech/auth-sdk';
|
|
207
|
-
|
|
208
|
-
export function createServerAuth(cookieApi: {
|
|
209
|
-
get: (name: string) => string | undefined;
|
|
210
|
-
set: (name: string, value: string) => void;
|
|
211
|
-
delete: (name: string) => void;
|
|
212
|
-
}) {
|
|
213
|
-
return createAuthClient({
|
|
214
|
-
clientId: process.env.NEXT_PUBLIC_AUTH_CLIENT_ID!,
|
|
215
|
-
authorizationEndpoint: `${process.env.NEXT_PUBLIC_AUTH_BASE_URL}/authorize`,
|
|
216
|
-
tokenEndpoint: `${process.env.NEXT_PUBLIC_AUTH_BASE_URL}/token`,
|
|
217
|
-
redirectUri: process.env.NEXT_PUBLIC_AUTH_CALLBACK_URL!,
|
|
218
|
-
storage: new CookieStorageAdapter({
|
|
219
|
-
getCookie: async (name) => cookieApi.get(name) ?? null,
|
|
220
|
-
setCookie: async (name, value) => cookieApi.set(name, value),
|
|
221
|
-
removeCookie: async (name) => cookieApi.delete(name),
|
|
222
|
-
}),
|
|
223
|
-
onRedirect: (url) => { redirect(url); },
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
## Security notes
|
|
229
|
-
|
|
230
|
-
- **No secrets:** Never include a `clientSecret` in browser/mobile apps. This SDK supports public clients only.
|
|
231
|
-
- **PKCE is mandatory:** S256 code challenge is always used. Plain PKCE is not supported.
|
|
232
|
-
- **State validation:** State is always generated and validated on callback.
|
|
233
|
-
- **Memory storage default:** Tokens are stored in memory by default to minimize XSS exposure.
|
|
234
|
-
- **Use sessionStorage cautiously:** Survives page reload but is still JS-readable.
|
|
235
|
-
- **Avoid localStorage** for sensitive tokens.
|
|
236
|
-
- **Validate `returnTo`:** The `logout()` method rejects non-`https://` or `http://` `returnTo` values.
|
|
237
|
-
|
|
238
|
-
## Storage trade-offs
|
|
239
|
-
|
|
240
|
-
| Adapter | Persists reload | XSS risk | SSR compatible |
|
|
241
|
-
|---------|----------------|----------|----------------|
|
|
242
|
-
| `MemoryStorageAdapter` | No | Lowest | No |
|
|
243
|
-
| `WebStorageAdapter(sessionStorage)` | Per tab | Medium | No |
|
|
244
|
-
| `WebStorageAdapter(localStorage)` | Yes | High | No |
|
|
245
|
-
| `CookieStorageAdapter` | Configurable | Low (httpOnly) | Yes |
|
|
246
|
-
|
|
247
|
-
## Troubleshooting
|
|
248
|
-
|
|
249
|
-
- **State mismatch:** Ensure the same storage instance/tab is used between `startLogin()` and `handleRedirectCallback()`. Clear `nuria:oauth:state` and retry.
|
|
250
|
-
- **Missing code_verifier:** The `nuria:oauth:code_verifier` key was not found in storage. Ensure `startLogin()` was called before `handleRedirectCallback()`.
|
|
251
|
-
- **Token refresh fails:** Ensure `enableRefreshToken: true` is set and the auth server supports the `refresh_token` grant for your client.
|
|
252
|
-
|
|
253
|
-
## CI/CD
|
|
254
|
-
|
|
255
|
-
This repository uses GitHub Actions (`.github/workflows/ci-publish.yml`):
|
|
256
|
-
|
|
257
|
-
- **PR and `main` push:** typecheck, lint, test (coverage), build — runs on Node 20, 22
|
|
258
|
-
- **Tag `v*` push:** validates tag matches `package.json` version, then publishes to npm via Trusted Publishing (OIDC — no stored tokens)
|
|
259
|
-
|
|
260
|
-
### Publishing
|
|
261
|
-
|
|
262
|
-
1. Update `version` in `package.json`
|
|
263
|
-
2. Push a tag: `git tag v1.0.0 && git push --tags`
|
|
264
|
-
3. The workflow validates the version and publishes to npm
|
|
265
|
-
|
|
266
|
-
> **One-time setup:** after the first manual publish, configure Trusted Publishing at **npmjs.com → package → Settings → Automated Publishing** with repository `nuria-tech/nuria-auth-sdk` and workflow `ci-publish.yml`.
|
|
267
|
-
|
|
268
|
-
## License
|
|
269
|
-
|
|
270
|
-
MIT — see [LICENSE](./LICENSE).
|
|
1
|
+
# @nuria-tech/auth-sdk
|
|
2
|
+
|
|
3
|
+
A minimal, browser-native TypeScript SDK for OAuth 2.0 Authorization Code flow with PKCE. Redirect-only — no embedded UI, no password flows.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Authorization Code flow + PKCE (S256, mandatory)
|
|
8
|
+
- State parameter generation and validation
|
|
9
|
+
- Token exchange via standard form-encoded request
|
|
10
|
+
- Automatic token refresh (optional)
|
|
11
|
+
- Pluggable storage adapters (memory, sessionStorage, cookies)
|
|
12
|
+
- Configurable HTTP transport with retry and interceptors
|
|
13
|
+
- Zero production dependencies — uses Web Crypto API and fetch
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @nuria-tech/auth-sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Published on [npm](https://www.npmjs.com/package/@nuria-tech/auth-sdk).
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createAuthClient } from '@nuria-tech/auth-sdk';
|
|
27
|
+
|
|
28
|
+
const auth = createAuthClient({
|
|
29
|
+
clientId: 'your-client-id',
|
|
30
|
+
authorizationEndpoint: 'https://your-auth-server.example.com/authorize',
|
|
31
|
+
tokenEndpoint: 'https://your-auth-server.example.com/token',
|
|
32
|
+
redirectUri: 'https://your-app.example.com/callback',
|
|
33
|
+
scope: 'openid profile email',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Redirect to login
|
|
37
|
+
await auth.startLogin();
|
|
38
|
+
|
|
39
|
+
// In your callback page
|
|
40
|
+
const session = await auth.handleRedirectCallback(window.location.href);
|
|
41
|
+
console.log(session.tokens.accessToken);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
interface AuthConfig {
|
|
48
|
+
// Required
|
|
49
|
+
clientId: string;
|
|
50
|
+
authorizationEndpoint: string;
|
|
51
|
+
tokenEndpoint: string;
|
|
52
|
+
redirectUri: string;
|
|
53
|
+
|
|
54
|
+
// Optional
|
|
55
|
+
scope?: string; // default scope sent with every login
|
|
56
|
+
logoutEndpoint?: string; // if set, logout() redirects here
|
|
57
|
+
userinfoEndpoint?: string; // required for getUserinfo()
|
|
58
|
+
storage?: StorageAdapter; // default: MemoryStorageAdapter
|
|
59
|
+
transport?: AuthTransport; // default: FetchAuthTransport
|
|
60
|
+
onRedirect?: (url: string) => void | Promise<void>; // override browser redirect
|
|
61
|
+
enableRefreshToken?: boolean; // enable automatic token refresh
|
|
62
|
+
now?: () => number; // override Date.now() for testing
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> **Security note:** Do not include `clientSecret` in browser apps. This SDK is designed for public clients (SPAs, mobile). PKCE provides the proof of possession without a client secret.
|
|
67
|
+
|
|
68
|
+
## Public API
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
interface AuthClient {
|
|
72
|
+
startLogin(options?: StartLoginOptions): Promise<void>;
|
|
73
|
+
handleRedirectCallback(callbackUrl?: string): Promise<Session>;
|
|
74
|
+
getSession(): Session | null;
|
|
75
|
+
getAccessToken(): Promise<string | null>;
|
|
76
|
+
logout(options?: { returnTo?: string }): Promise<void>;
|
|
77
|
+
isAuthenticated(): boolean;
|
|
78
|
+
onAuthStateChanged(handler: (session: Session | null) => void): () => void;
|
|
79
|
+
getUserinfo(): Promise<Record<string, unknown>>;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `startLogin(options?)`
|
|
84
|
+
|
|
85
|
+
Generates PKCE `code_verifier` + `code_challenge` (S256), stores them in the configured storage, and redirects to `authorizationEndpoint`. The `state` parameter is always included and validated on callback.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
await auth.startLogin({
|
|
89
|
+
scopes: ['openid', 'profile'], // overrides config.scope
|
|
90
|
+
loginHint: 'user@example.com',
|
|
91
|
+
extraParams: { prompt: 'login' },
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### `handleRedirectCallback(callbackUrl?)`
|
|
96
|
+
|
|
97
|
+
Parses the callback URL, validates `state`, exchanges `code` for tokens using a form-encoded POST to `tokenEndpoint`, and clears transient PKCE storage. Throws typed `AuthError` on any failure.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// In your /callback route
|
|
101
|
+
const session = await auth.handleRedirectCallback(window.location.href);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `getAccessToken()`
|
|
105
|
+
|
|
106
|
+
Returns the current access token. If the token is expired and `enableRefreshToken: true`, automatically refreshes it (concurrent calls are deduplicated).
|
|
107
|
+
|
|
108
|
+
### `logout(options?)`
|
|
109
|
+
|
|
110
|
+
Clears the local session. If `logoutEndpoint` is configured, redirects to it. Validates `returnTo` to prevent open redirect attacks.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
await auth.logout({ returnTo: 'https://your-app.example.com' });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `onAuthStateChanged(handler)`
|
|
117
|
+
|
|
118
|
+
Subscribes to session changes. Returns an unsubscribe function.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const unsubscribe = auth.onAuthStateChanged((session) => {
|
|
122
|
+
console.log(session ? 'logged in' : 'logged out');
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Storage adapters
|
|
127
|
+
|
|
128
|
+
Default is `MemoryStorageAdapter` (most secure for XSS resistance, clears on reload).
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { WebStorageAdapter } from '@nuria-tech/auth-sdk';
|
|
132
|
+
|
|
133
|
+
// sessionStorage: persists per tab, JS-readable
|
|
134
|
+
const auth = createAuthClient({
|
|
135
|
+
...config,
|
|
136
|
+
storage: new WebStorageAdapter(window.sessionStorage),
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Cookie adapter for SSR:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { CookieStorageAdapter } from '@nuria-tech/auth-sdk';
|
|
144
|
+
|
|
145
|
+
const auth = createAuthClient({
|
|
146
|
+
...config,
|
|
147
|
+
storage: new CookieStorageAdapter({
|
|
148
|
+
getCookie: async (name) => getCookieFromRequest(name),
|
|
149
|
+
setCookie: async (name, value) => setResponseCookie(name, value),
|
|
150
|
+
removeCookie: async (name) => clearResponseCookie(name),
|
|
151
|
+
}),
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Transport customization
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { FetchAuthTransport } from '@nuria-tech/auth-sdk';
|
|
159
|
+
|
|
160
|
+
const auth = createAuthClient({
|
|
161
|
+
...config,
|
|
162
|
+
transport: new FetchAuthTransport({
|
|
163
|
+
timeoutMs: 10_000,
|
|
164
|
+
retries: 1,
|
|
165
|
+
interceptors: [
|
|
166
|
+
{
|
|
167
|
+
onRequest: async (_url, req) => ({
|
|
168
|
+
...req,
|
|
169
|
+
headers: { ...(req.headers ?? {}), 'X-App-Version': '1.0.0' },
|
|
170
|
+
}),
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## React example
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
import { useMemo, useState, useEffect } from 'react';
|
|
181
|
+
import { createAuthClient } from '@nuria-tech/auth-sdk';
|
|
182
|
+
|
|
183
|
+
const auth = createAuthClient({
|
|
184
|
+
clientId: 'your-client-id',
|
|
185
|
+
authorizationEndpoint: 'https://auth.example.com/authorize',
|
|
186
|
+
tokenEndpoint: 'https://auth.example.com/token',
|
|
187
|
+
redirectUri: `${window.location.origin}/callback`,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export function App() {
|
|
191
|
+
const [session, setSession] = useState(auth.getSession());
|
|
192
|
+
|
|
193
|
+
useEffect(() => auth.onAuthStateChanged(setSession), []);
|
|
194
|
+
|
|
195
|
+
if (!session) {
|
|
196
|
+
return <button onClick={() => auth.startLogin()}>Login</button>;
|
|
197
|
+
}
|
|
198
|
+
return <button onClick={() => auth.logout()}>Logout</button>;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Next.js SSR example
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// lib/auth.ts
|
|
206
|
+
import { createAuthClient, CookieStorageAdapter } from '@nuria-tech/auth-sdk';
|
|
207
|
+
|
|
208
|
+
export function createServerAuth(cookieApi: {
|
|
209
|
+
get: (name: string) => string | undefined;
|
|
210
|
+
set: (name: string, value: string) => void;
|
|
211
|
+
delete: (name: string) => void;
|
|
212
|
+
}) {
|
|
213
|
+
return createAuthClient({
|
|
214
|
+
clientId: process.env.NEXT_PUBLIC_AUTH_CLIENT_ID!,
|
|
215
|
+
authorizationEndpoint: `${process.env.NEXT_PUBLIC_AUTH_BASE_URL}/authorize`,
|
|
216
|
+
tokenEndpoint: `${process.env.NEXT_PUBLIC_AUTH_BASE_URL}/token`,
|
|
217
|
+
redirectUri: process.env.NEXT_PUBLIC_AUTH_CALLBACK_URL!,
|
|
218
|
+
storage: new CookieStorageAdapter({
|
|
219
|
+
getCookie: async (name) => cookieApi.get(name) ?? null,
|
|
220
|
+
setCookie: async (name, value) => cookieApi.set(name, value),
|
|
221
|
+
removeCookie: async (name) => cookieApi.delete(name),
|
|
222
|
+
}),
|
|
223
|
+
onRedirect: (url) => { redirect(url); },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Security notes
|
|
229
|
+
|
|
230
|
+
- **No secrets:** Never include a `clientSecret` in browser/mobile apps. This SDK supports public clients only.
|
|
231
|
+
- **PKCE is mandatory:** S256 code challenge is always used. Plain PKCE is not supported.
|
|
232
|
+
- **State validation:** State is always generated and validated on callback.
|
|
233
|
+
- **Memory storage default:** Tokens are stored in memory by default to minimize XSS exposure.
|
|
234
|
+
- **Use sessionStorage cautiously:** Survives page reload but is still JS-readable.
|
|
235
|
+
- **Avoid localStorage** for sensitive tokens.
|
|
236
|
+
- **Validate `returnTo`:** The `logout()` method rejects non-`https://` or `http://` `returnTo` values.
|
|
237
|
+
|
|
238
|
+
## Storage trade-offs
|
|
239
|
+
|
|
240
|
+
| Adapter | Persists reload | XSS risk | SSR compatible |
|
|
241
|
+
|---------|----------------|----------|----------------|
|
|
242
|
+
| `MemoryStorageAdapter` | No | Lowest | No |
|
|
243
|
+
| `WebStorageAdapter(sessionStorage)` | Per tab | Medium | No |
|
|
244
|
+
| `WebStorageAdapter(localStorage)` | Yes | High | No |
|
|
245
|
+
| `CookieStorageAdapter` | Configurable | Low (httpOnly) | Yes |
|
|
246
|
+
|
|
247
|
+
## Troubleshooting
|
|
248
|
+
|
|
249
|
+
- **State mismatch:** Ensure the same storage instance/tab is used between `startLogin()` and `handleRedirectCallback()`. Clear `nuria:oauth:state` and retry.
|
|
250
|
+
- **Missing code_verifier:** The `nuria:oauth:code_verifier` key was not found in storage. Ensure `startLogin()` was called before `handleRedirectCallback()`.
|
|
251
|
+
- **Token refresh fails:** Ensure `enableRefreshToken: true` is set and the auth server supports the `refresh_token` grant for your client.
|
|
252
|
+
|
|
253
|
+
## CI/CD
|
|
254
|
+
|
|
255
|
+
This repository uses GitHub Actions (`.github/workflows/ci-publish.yml`):
|
|
256
|
+
|
|
257
|
+
- **PR and `main` push:** typecheck, lint, test (coverage), build — runs on Node 20, 22
|
|
258
|
+
- **Tag `v*` push:** validates tag matches `package.json` version, then publishes to npm via Trusted Publishing (OIDC — no stored tokens)
|
|
259
|
+
|
|
260
|
+
### Publishing
|
|
261
|
+
|
|
262
|
+
1. Update `version` in `package.json`
|
|
263
|
+
2. Push a tag: `git tag v1.0.0 && git push --tags`
|
|
264
|
+
3. The workflow validates the version and publishes to npm
|
|
265
|
+
|
|
266
|
+
> **One-time setup:** after the first manual publish, configure Trusted Publishing at **npmjs.com → package → Settings → Automated Publishing** with repository `nuria-tech/nuria-auth-sdk` and workflow `ci-publish.yml`.
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT — see [LICENSE](./LICENSE).
|