@owlmeans/web-oidc-provider 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +677 -0
- package/build/.gitkeep +0 -0
- package/build/auth-state.d.ts +7 -0
- package/build/auth-state.d.ts.map +1 -0
- package/build/auth-state.js +97 -0
- package/build/auth-state.js.map +1 -0
- package/build/consts.d.ts +8 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +9 -0
- package/build/consts.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +2 -0
- package/build/index.js.map +1 -0
- package/build/types.d.ts +30 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +42 -0
- package/src/auth-state.ts +127 -0
- package/src/consts.ts +8 -0
- package/src/index.ts +3 -0
- package/src/types.ts +37 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OwlMeans Common — Fullstack typescript framework
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
# @owlmeans/web-oidc-provider
|
|
2
|
+
|
|
3
|
+
Web-based OpenID Connect Provider functionality for OwlMeans Common Libraries. This package provides client-side authentication state management and interaction handling for OIDC Provider implementations, designed for React-based web applications with secure authentication flows.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `@owlmeans/web-oidc-provider` package serves as the web frontend component for OIDC Provider functionality in the OwlMeans ecosystem. It handles client-side authentication state management, interaction flows, and cookie-based session management. This package is designed for fullstack applications with focus on security and proper OIDC authentication flows.
|
|
8
|
+
|
|
9
|
+
**Key Features:**
|
|
10
|
+
- **Authentication State Management**: Client-side OIDC authentication state tracking and validation
|
|
11
|
+
- **Interaction Handling**: OIDC interaction flow management with session persistence
|
|
12
|
+
- **Cookie Management**: Secure cookie-based session and interaction tracking
|
|
13
|
+
- **Multi-Entity Support**: Support for multiple entity authentication within the same session
|
|
14
|
+
- **DID Integration**: Decentralized Identity (DID) linking and validation
|
|
15
|
+
- **Stack-based Sessions**: Session stacking for complex authentication flows
|
|
16
|
+
|
|
17
|
+
This package follows the OwlMeans "quadra" pattern as the **web** implementation, complementing:
|
|
18
|
+
- **@owlmeans/oidc**: Common OIDC declarations and base functionality *(base package)*
|
|
19
|
+
- **@owlmeans/server-oidc-provider**: Server-side OIDC provider implementation
|
|
20
|
+
- **@owlmeans/web-oidc-provider**: Web client OIDC provider integration *(this package)*
|
|
21
|
+
- **@owlmeans/web-oidc-rp**: Web client OIDC Relying Party implementation
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @owlmeans/web-oidc-provider
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Dependencies
|
|
30
|
+
|
|
31
|
+
This package requires and integrates with:
|
|
32
|
+
- `@owlmeans/oidc`: Core OIDC functionality and shared configuration
|
|
33
|
+
- `@owlmeans/auth`: Authentication system and types
|
|
34
|
+
- `@owlmeans/resource`: Resource management for interaction storage
|
|
35
|
+
- `@owlmeans/web-client`: Web client framework and context
|
|
36
|
+
- `universal-cookie`: Cookie management for cross-browser compatibility
|
|
37
|
+
- React: Peer dependency for web components
|
|
38
|
+
|
|
39
|
+
## Key Concepts
|
|
40
|
+
|
|
41
|
+
### OIDC Authentication States
|
|
42
|
+
|
|
43
|
+
The package manages various authentication states throughout the OIDC flow:
|
|
44
|
+
|
|
45
|
+
- **Authenticated**: User has valid authentication credentials
|
|
46
|
+
- **SameEntity**: Current user belongs to the same entity as the OIDC interaction
|
|
47
|
+
- **IdLinked**: User has a linked Decentralized Identity (DID)
|
|
48
|
+
- **ProfileExists**: User profile exists in the system
|
|
49
|
+
- **RegistrationAllowed**: New user registration is permitted
|
|
50
|
+
|
|
51
|
+
### Interaction Management
|
|
52
|
+
|
|
53
|
+
OIDC interactions are managed with:
|
|
54
|
+
- **Session Persistence**: Interactions persist across browser sessions via cookies
|
|
55
|
+
- **Stack-based Flow**: Support for nested authentication flows and entity switching
|
|
56
|
+
- **State Validation**: Continuous validation of authentication state
|
|
57
|
+
- **Secure Storage**: Encrypted storage of interaction data via OwlMeans resource system
|
|
58
|
+
|
|
59
|
+
### Cookie-based Session Management
|
|
60
|
+
|
|
61
|
+
Secure session management using:
|
|
62
|
+
- **Interaction Cookies**: Track current OIDC interaction sessions
|
|
63
|
+
- **Configurable TTL**: Adjustable session timeouts and expiration
|
|
64
|
+
- **Cross-domain Support**: Support for multi-domain OIDC flows
|
|
65
|
+
- **Secure Settings**: HttpOnly and Secure cookie flags for production environments
|
|
66
|
+
|
|
67
|
+
## API Reference
|
|
68
|
+
|
|
69
|
+
### Factory Functions
|
|
70
|
+
|
|
71
|
+
#### `makeAuthStateModel<C, T>(context, updateState): OidcAuthStateModel`
|
|
72
|
+
|
|
73
|
+
Creates an OIDC authentication state model for managing client-side authentication flow.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { makeAuthStateModel } from '@owlmeans/web-oidc-provider'
|
|
77
|
+
import { makeWebContext } from '@owlmeans/web-client'
|
|
78
|
+
|
|
79
|
+
const context = makeWebContext(config)
|
|
80
|
+
|
|
81
|
+
const authStateModel = makeAuthStateModel(context, async (uid: string) => {
|
|
82
|
+
// Update authentication state from server
|
|
83
|
+
const response = await fetch(`/api/oidc/auth-state/${uid}`)
|
|
84
|
+
return response.json()
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Parameters:**
|
|
89
|
+
- `context`: `AppContext<C>` - Web application context
|
|
90
|
+
- `updateState`: `(uid: string) => Promise<{entityId?: string, did?: string}>` - Function to update authentication state from server
|
|
91
|
+
|
|
92
|
+
**Returns:** `OidcAuthStateModel` - Authentication state model instance
|
|
93
|
+
|
|
94
|
+
### Core Interfaces
|
|
95
|
+
|
|
96
|
+
#### `OidcAuthStateModel`
|
|
97
|
+
|
|
98
|
+
Main interface for managing OIDC authentication state on the client side.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
interface OidcAuthStateModel extends AuthStateProperties {
|
|
102
|
+
// Initialization and state management
|
|
103
|
+
init: (uid: string, reset?: boolean) => Promise<OidcAuthStateModel>
|
|
104
|
+
updateAuthState: (uid: string) => Promise<OidcAuthState[]>
|
|
105
|
+
|
|
106
|
+
// State validation methods
|
|
107
|
+
isAuthenticated: () => boolean
|
|
108
|
+
isSameEntity: () => boolean
|
|
109
|
+
isIdLinked: () => boolean
|
|
110
|
+
profileExists: () => boolean
|
|
111
|
+
isRegistrationAllowed: () => boolean
|
|
112
|
+
|
|
113
|
+
// Interaction management
|
|
114
|
+
finishInteraction: (skipState?: boolean) => Promise<void>
|
|
115
|
+
getState: () => OidcAuthState[]
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Methods:**
|
|
120
|
+
|
|
121
|
+
**`init(uid: string, reset?: boolean): Promise<OidcAuthStateModel>`**
|
|
122
|
+
- **Purpose**: Initialize the authentication state model for a specific interaction
|
|
123
|
+
- **Parameters**:
|
|
124
|
+
- `uid`: Unique interaction identifier
|
|
125
|
+
- `reset`: Optional flag to reset existing interaction stack
|
|
126
|
+
- **Behavior**:
|
|
127
|
+
- Loads existing state from cache or creates new state
|
|
128
|
+
- Manages interaction stack for nested flows
|
|
129
|
+
- Sets interaction cookies with appropriate TTL
|
|
130
|
+
- **Returns**: Promise resolving to the initialized model
|
|
131
|
+
- **Throws**: `SyntaxError` if no valid UID is provided
|
|
132
|
+
|
|
133
|
+
**`updateAuthState(uid: string): Promise<OidcAuthState[]>`**
|
|
134
|
+
- **Purpose**: Update authentication state by querying server and validating current session
|
|
135
|
+
- **Parameters**: `uid` - Interaction identifier
|
|
136
|
+
- **Behavior**:
|
|
137
|
+
- Calls server to get updated authentication status
|
|
138
|
+
- Validates current user against interaction entity
|
|
139
|
+
- Updates DID linking status
|
|
140
|
+
- Caches updated state
|
|
141
|
+
- **Returns**: Promise resolving to array of current authentication states
|
|
142
|
+
|
|
143
|
+
**`isAuthenticated(): boolean`**
|
|
144
|
+
- **Purpose**: Check if user is currently authenticated
|
|
145
|
+
- **Returns**: `true` if user has valid authentication
|
|
146
|
+
|
|
147
|
+
**`isSameEntity(): boolean`**
|
|
148
|
+
- **Purpose**: Check if authenticated user belongs to the same entity as the OIDC interaction
|
|
149
|
+
- **Returns**: `true` if user entity matches interaction entity
|
|
150
|
+
|
|
151
|
+
**`isIdLinked(): boolean`**
|
|
152
|
+
- **Purpose**: Check if user has a linked Decentralized Identity (DID)
|
|
153
|
+
- **Returns**: `true` if DID is linked to user account
|
|
154
|
+
|
|
155
|
+
**`profileExists(): boolean`**
|
|
156
|
+
- **Purpose**: Check if user profile exists in the system
|
|
157
|
+
- **Returns**: `true` if user profile is available
|
|
158
|
+
|
|
159
|
+
**`isRegistrationAllowed(): boolean`**
|
|
160
|
+
- **Purpose**: Check if new user registration is permitted
|
|
161
|
+
- **Returns**: `true` if registration is allowed
|
|
162
|
+
|
|
163
|
+
**`finishInteraction(skipState?: boolean): Promise<void>`**
|
|
164
|
+
- **Purpose**: Complete current interaction and restore previous session from stack
|
|
165
|
+
- **Parameters**: `skipState` - Optional flag to skip state update
|
|
166
|
+
- **Behavior**:
|
|
167
|
+
- Removes current interaction from cache
|
|
168
|
+
- Pops previous interaction from stack
|
|
169
|
+
- Updates cookies to previous session
|
|
170
|
+
- Optionally updates authentication state
|
|
171
|
+
|
|
172
|
+
**`getState(): OidcAuthState[]`**
|
|
173
|
+
- **Purpose**: Get current authentication states as array
|
|
174
|
+
- **Returns**: Array of active authentication state flags
|
|
175
|
+
|
|
176
|
+
#### `AuthStateProperties`
|
|
177
|
+
|
|
178
|
+
Properties maintained by the authentication state model.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
interface AuthStateProperties {
|
|
182
|
+
did?: string // Decentralized Identity identifier
|
|
183
|
+
entityId?: string // Entity identifier for multi-tenant support
|
|
184
|
+
state: Set<OidcAuthState> // Set of current authentication states
|
|
185
|
+
uid: string // Unique interaction identifier
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### `OidcInteraction`
|
|
190
|
+
|
|
191
|
+
Resource record for persisting interaction state across sessions.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
interface OidcInteraction extends ResourceRecord {
|
|
195
|
+
stack: Array<{
|
|
196
|
+
token: string | null // Previous session authentication token
|
|
197
|
+
uid: string // Previous interaction identifier
|
|
198
|
+
}>
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### `WithSharedConfig`
|
|
203
|
+
|
|
204
|
+
Configuration interface for OIDC provider settings.
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
interface WithSharedConfig {
|
|
208
|
+
oidc: OidcSharedConfig
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Enums
|
|
213
|
+
|
|
214
|
+
#### `OidcAuthState`
|
|
215
|
+
|
|
216
|
+
Enumeration of possible authentication states during OIDC flows.
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
enum OidcAuthState {
|
|
220
|
+
Authenticated = 'authenticated', // User is authenticated
|
|
221
|
+
SameEntity = 'same-entity', // User belongs to interaction entity
|
|
222
|
+
IdLinked = 'id-linked', // DID is linked to user
|
|
223
|
+
ProfileExists = 'profile-exists', // User profile exists
|
|
224
|
+
RegistrationAllowed = 'registration-allowed' // Registration permitted
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Usage Examples
|
|
229
|
+
|
|
230
|
+
### Basic OIDC Provider Integration
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { makeAuthStateModel } from '@owlmeans/web-oidc-provider'
|
|
234
|
+
import { makeWebContext } from '@owlmeans/web-client'
|
|
235
|
+
|
|
236
|
+
// Configure web context with OIDC settings
|
|
237
|
+
const config = {
|
|
238
|
+
service: 'oidc-provider',
|
|
239
|
+
oidc: {
|
|
240
|
+
clientCookie: {
|
|
241
|
+
interaction: {
|
|
242
|
+
name: '_oidc_interaction',
|
|
243
|
+
ttl: 3600 // 1 hour
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const context = makeWebContext(config)
|
|
250
|
+
|
|
251
|
+
// Create authentication state model
|
|
252
|
+
const authState = makeAuthStateModel(context, async (uid) => {
|
|
253
|
+
// Fetch authentication state from server
|
|
254
|
+
const response = await fetch(`/api/oidc/interaction/${uid}/state`, {
|
|
255
|
+
credentials: 'include'
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
throw new Error('Failed to fetch authentication state')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return response.json()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// Initialize for specific interaction
|
|
266
|
+
await authState.init(interactionId)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### OIDC Authentication Flow Management
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// Handle OIDC authentication flow
|
|
273
|
+
const handleOidcFlow = async (interactionId: string) => {
|
|
274
|
+
try {
|
|
275
|
+
// Initialize authentication state
|
|
276
|
+
await authState.init(interactionId)
|
|
277
|
+
|
|
278
|
+
// Check current authentication status
|
|
279
|
+
if (authState.isAuthenticated()) {
|
|
280
|
+
if (authState.isSameEntity()) {
|
|
281
|
+
// User is authenticated and belongs to correct entity
|
|
282
|
+
console.log('User authenticated for correct entity')
|
|
283
|
+
|
|
284
|
+
if (authState.isIdLinked()) {
|
|
285
|
+
console.log('DID is linked to user account')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Proceed with OIDC authorization
|
|
289
|
+
await proceedWithAuthorization()
|
|
290
|
+
} else {
|
|
291
|
+
// User authenticated but for different entity
|
|
292
|
+
console.log('User needs to switch entities or re-authenticate')
|
|
293
|
+
await promptEntitySwitch()
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
// User not authenticated
|
|
297
|
+
if (authState.isRegistrationAllowed()) {
|
|
298
|
+
console.log('User can register or login')
|
|
299
|
+
await showLoginOrRegisterForm()
|
|
300
|
+
} else {
|
|
301
|
+
console.log('Registration not allowed, login required')
|
|
302
|
+
await showLoginForm()
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error('OIDC flow error:', error)
|
|
307
|
+
await handleAuthenticationError(error)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const proceedWithAuthorization = async () => {
|
|
312
|
+
// Continue with OIDC authorization grant
|
|
313
|
+
const states = authState.getState()
|
|
314
|
+
console.log('Current states:', states)
|
|
315
|
+
|
|
316
|
+
// Make authorization decision based on current state
|
|
317
|
+
window.location.href = '/oidc/auth/consent'
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const showLoginForm = async () => {
|
|
321
|
+
// Display login form component
|
|
322
|
+
// After successful login, update authentication state
|
|
323
|
+
await authState.updateAuthState(authState.uid)
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Multi-Entity Authentication
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// Handle entity switching in OIDC flows
|
|
331
|
+
const handleEntitySwitch = async (newEntityId: string) => {
|
|
332
|
+
try {
|
|
333
|
+
// Store current interaction in stack
|
|
334
|
+
await authState.init(newEntityId, false) // Don't reset stack
|
|
335
|
+
|
|
336
|
+
// Check if user is already authenticated for new entity
|
|
337
|
+
if (authState.isAuthenticated() && authState.isSameEntity()) {
|
|
338
|
+
console.log('User already authenticated for target entity')
|
|
339
|
+
return true
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Redirect to authentication for new entity
|
|
343
|
+
window.location.href = `/auth/entity/${newEntityId}`
|
|
344
|
+
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error('Entity switch error:', error)
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Complete interaction and return to previous session
|
|
352
|
+
const completeInteraction = async () => {
|
|
353
|
+
try {
|
|
354
|
+
await authState.finishInteraction()
|
|
355
|
+
console.log('Returned to previous interaction:', authState.uid)
|
|
356
|
+
|
|
357
|
+
// Redirect to original application
|
|
358
|
+
window.location.href = '/dashboard'
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.error('Failed to complete interaction:', error)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### DID Integration
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
// Handle Decentralized Identity linking
|
|
369
|
+
const handleDidLinking = async (didDocument: any) => {
|
|
370
|
+
try {
|
|
371
|
+
// Link DID to user account
|
|
372
|
+
const response = await fetch('/api/did/link', {
|
|
373
|
+
method: 'POST',
|
|
374
|
+
headers: { 'Content-Type': 'application/json' },
|
|
375
|
+
credentials: 'include',
|
|
376
|
+
body: JSON.stringify({
|
|
377
|
+
did: didDocument.id,
|
|
378
|
+
interactionId: authState.uid
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
if (response.ok) {
|
|
383
|
+
// Update authentication state to reflect DID linking
|
|
384
|
+
await authState.updateAuthState(authState.uid)
|
|
385
|
+
|
|
386
|
+
if (authState.isIdLinked()) {
|
|
387
|
+
console.log('DID successfully linked')
|
|
388
|
+
return true
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
throw new Error('DID linking failed')
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.error('DID linking error:', error)
|
|
395
|
+
return false
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Verify DID authentication
|
|
400
|
+
const verifyDidAuth = async (signature: string, challenge: string) => {
|
|
401
|
+
if (!authState.isIdLinked()) {
|
|
402
|
+
throw new Error('DID not linked to account')
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Verify DID signature with server
|
|
406
|
+
const response = await fetch('/api/did/verify', {
|
|
407
|
+
method: 'POST',
|
|
408
|
+
headers: { 'Content-Type': 'application/json' },
|
|
409
|
+
credentials: 'include',
|
|
410
|
+
body: JSON.stringify({
|
|
411
|
+
signature,
|
|
412
|
+
challenge,
|
|
413
|
+
interactionId: authState.uid
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
return response.ok
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### React Component Integration
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import React, { useEffect, useState } from 'react'
|
|
425
|
+
import { makeAuthStateModel, OidcAuthState } from '@owlmeans/web-oidc-provider'
|
|
426
|
+
|
|
427
|
+
interface OidcProviderProps {
|
|
428
|
+
interactionId: string
|
|
429
|
+
onStateChange?: (states: OidcAuthState[]) => void
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const OidcProvider: React.FC<OidcProviderProps> = ({
|
|
433
|
+
interactionId,
|
|
434
|
+
onStateChange
|
|
435
|
+
}) => {
|
|
436
|
+
const [authState, setAuthState] = useState<any>(null)
|
|
437
|
+
const [currentStates, setCurrentStates] = useState<OidcAuthState[]>([])
|
|
438
|
+
const [loading, setLoading] = useState(true)
|
|
439
|
+
const [error, setError] = useState<string | null>(null)
|
|
440
|
+
|
|
441
|
+
useEffect(() => {
|
|
442
|
+
const initializeAuth = async () => {
|
|
443
|
+
try {
|
|
444
|
+
setLoading(true)
|
|
445
|
+
|
|
446
|
+
const model = makeAuthStateModel(context, async (uid) => {
|
|
447
|
+
// Fetch state from server
|
|
448
|
+
const response = await fetch(`/api/oidc/state/${uid}`)
|
|
449
|
+
return response.json()
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
await model.init(interactionId)
|
|
453
|
+
const states = model.getState()
|
|
454
|
+
|
|
455
|
+
setAuthState(model)
|
|
456
|
+
setCurrentStates(states)
|
|
457
|
+
onStateChange?.(states)
|
|
458
|
+
} catch (err) {
|
|
459
|
+
setError(err.message)
|
|
460
|
+
} finally {
|
|
461
|
+
setLoading(false)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
initializeAuth()
|
|
466
|
+
}, [interactionId])
|
|
467
|
+
|
|
468
|
+
const handleLogin = async (credentials: any) => {
|
|
469
|
+
try {
|
|
470
|
+
// Perform login
|
|
471
|
+
await performLogin(credentials)
|
|
472
|
+
|
|
473
|
+
// Update authentication state
|
|
474
|
+
const states = await authState.updateAuthState(interactionId)
|
|
475
|
+
setCurrentStates(states)
|
|
476
|
+
onStateChange?.(states)
|
|
477
|
+
} catch (err) {
|
|
478
|
+
setError(err.message)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const handleComplete = async () => {
|
|
483
|
+
try {
|
|
484
|
+
await authState.finishInteraction()
|
|
485
|
+
// Redirect or update UI
|
|
486
|
+
} catch (err) {
|
|
487
|
+
setError(err.message)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (loading) return <div>Loading authentication...</div>
|
|
492
|
+
if (error) return <div>Error: {error}</div>
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
<div className="oidc-provider">
|
|
496
|
+
<div className="auth-status">
|
|
497
|
+
<h3>Authentication Status</h3>
|
|
498
|
+
<ul>
|
|
499
|
+
{currentStates.map(state => (
|
|
500
|
+
<li key={state}>{state}</li>
|
|
501
|
+
))}
|
|
502
|
+
</ul>
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
{!authState.isAuthenticated() && (
|
|
506
|
+
<LoginForm onLogin={handleLogin} />
|
|
507
|
+
)}
|
|
508
|
+
|
|
509
|
+
{authState.isAuthenticated() && authState.isSameEntity() && (
|
|
510
|
+
<ConsentForm onConsent={handleComplete} />
|
|
511
|
+
)}
|
|
512
|
+
|
|
513
|
+
{authState.isAuthenticated() && !authState.isSameEntity() && (
|
|
514
|
+
<EntitySwitchForm onSwitch={handleEntitySwitch} />
|
|
515
|
+
)}
|
|
516
|
+
</div>
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
## Configuration
|
|
522
|
+
|
|
523
|
+
### OIDC Provider Configuration
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
interface OidcProviderConfig {
|
|
527
|
+
oidc: {
|
|
528
|
+
clientCookie: {
|
|
529
|
+
interaction: {
|
|
530
|
+
name: string // Cookie name for interaction tracking
|
|
531
|
+
ttl: number // Time to live in seconds
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
provider: {
|
|
535
|
+
issuer: string // OIDC provider issuer URL
|
|
536
|
+
clientId: string // OAuth2 client identifier
|
|
537
|
+
redirectUri: string // Redirect URI after authentication
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
defaultEntityId?: string // Default entity for multi-tenant scenarios
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Cookie Security Settings
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
// Production cookie settings
|
|
548
|
+
const productionCookieConfig = {
|
|
549
|
+
secure: true, // Require HTTPS
|
|
550
|
+
httpOnly: true, // Prevent XSS access
|
|
551
|
+
sameSite: 'strict', // CSRF protection
|
|
552
|
+
domain: '.example.com', // Cross-subdomain access
|
|
553
|
+
path: '/' // Site-wide access
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
## Error Handling
|
|
558
|
+
|
|
559
|
+
### Common Error Scenarios
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// Handle authentication state errors
|
|
563
|
+
const handleAuthErrors = async (error: Error) => {
|
|
564
|
+
switch (error.message) {
|
|
565
|
+
case 'no-uid':
|
|
566
|
+
console.error('No interaction UID provided')
|
|
567
|
+
// Redirect to OIDC entry point
|
|
568
|
+
window.location.href = '/oidc/auth'
|
|
569
|
+
break
|
|
570
|
+
|
|
571
|
+
case 'invalid-interaction':
|
|
572
|
+
console.error('Invalid or expired interaction')
|
|
573
|
+
// Clear cookies and restart flow
|
|
574
|
+
await clearInteractionCookies()
|
|
575
|
+
window.location.href = '/oidc/auth'
|
|
576
|
+
break
|
|
577
|
+
|
|
578
|
+
case 'entity-mismatch':
|
|
579
|
+
console.error('User entity does not match interaction')
|
|
580
|
+
// Prompt for entity switch or re-authentication
|
|
581
|
+
await promptEntitySwitch()
|
|
582
|
+
break
|
|
583
|
+
|
|
584
|
+
default:
|
|
585
|
+
console.error('Authentication error:', error)
|
|
586
|
+
// Show generic error message
|
|
587
|
+
showErrorMessage('Authentication failed. Please try again.')
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Cleanup after errors
|
|
592
|
+
const clearInteractionCookies = async () => {
|
|
593
|
+
const cookies = new Cookies()
|
|
594
|
+
cookies.remove('_oidc_interaction', { path: '/' })
|
|
595
|
+
|
|
596
|
+
// Clear cached state
|
|
597
|
+
await authState.finishInteraction(true)
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
## Security Considerations
|
|
602
|
+
|
|
603
|
+
### Cookie Security
|
|
604
|
+
- **Secure Flags**: Always use `Secure` and `HttpOnly` flags in production
|
|
605
|
+
- **SameSite Protection**: Configure `SameSite` attribute for CSRF protection
|
|
606
|
+
- **TTL Management**: Implement appropriate session timeouts
|
|
607
|
+
|
|
608
|
+
### State Validation
|
|
609
|
+
- **Server Verification**: Always verify authentication state with server
|
|
610
|
+
- **Entity Validation**: Validate user entity matches interaction requirements
|
|
611
|
+
- **DID Verification**: Verify DID signatures using cryptographic validation
|
|
612
|
+
|
|
613
|
+
### Session Management
|
|
614
|
+
- **Stack Protection**: Limit interaction stack depth to prevent abuse
|
|
615
|
+
- **Cache Security**: Secure in-memory state cache with appropriate cleanup
|
|
616
|
+
- **Token Handling**: Secure handling of authentication tokens in stacked sessions
|
|
617
|
+
|
|
618
|
+
## Integration with OwlMeans Ecosystem
|
|
619
|
+
|
|
620
|
+
### Context Integration
|
|
621
|
+
```typescript
|
|
622
|
+
import { makeWebContext } from '@owlmeans/web-client'
|
|
623
|
+
|
|
624
|
+
const context = makeWebContext(config)
|
|
625
|
+
const authService = context.auth()
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### Resource System Integration
|
|
629
|
+
```typescript
|
|
630
|
+
import { useStoreModel } from '@owlmeans/web-client'
|
|
631
|
+
|
|
632
|
+
// Persistent interaction storage
|
|
633
|
+
const interactionStore = useStoreModel<OidcInteraction>('oidc-interactions')
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### Authentication System Integration
|
|
637
|
+
```typescript
|
|
638
|
+
import type { Auth } from '@owlmeans/auth'
|
|
639
|
+
|
|
640
|
+
// Validate against OwlMeans auth system
|
|
641
|
+
const currentUser: Auth = context.auth().user()
|
|
642
|
+
const isAuthenticated = await context.auth().authenticated()
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
## Best Practices
|
|
646
|
+
|
|
647
|
+
1. **State Synchronization**: Always synchronize client state with server state
|
|
648
|
+
2. **Error Recovery**: Implement robust error recovery mechanisms
|
|
649
|
+
3. **Security**: Use secure cookie settings and validate all state transitions
|
|
650
|
+
4. **Performance**: Cache authentication state appropriately with proper TTL
|
|
651
|
+
5. **User Experience**: Provide clear feedback during authentication flows
|
|
652
|
+
6. **Multi-Entity**: Handle entity switching gracefully in multi-tenant scenarios
|
|
653
|
+
|
|
654
|
+
## Related Packages
|
|
655
|
+
|
|
656
|
+
- **@owlmeans/oidc**: Core OIDC functionality and shared configuration
|
|
657
|
+
- **@owlmeans/server-oidc-provider**: Server-side OIDC provider implementation
|
|
658
|
+
- **@owlmeans/web-oidc-rp**: Web OIDC Relying Party implementation
|
|
659
|
+
- **@owlmeans/auth**: Core authentication system
|
|
660
|
+
- **@owlmeans/web-client**: Web client framework and context
|
|
661
|
+
- **@owlmeans/resource**: Resource management for state persistence
|
|
662
|
+
|
|
663
|
+
## TypeScript Support
|
|
664
|
+
|
|
665
|
+
This package is written in TypeScript and provides full type safety:
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
import type {
|
|
669
|
+
OidcAuthStateModel,
|
|
670
|
+
OidcAuthState,
|
|
671
|
+
OidcInteraction,
|
|
672
|
+
WithSharedConfig
|
|
673
|
+
} from '@owlmeans/web-oidc-provider'
|
|
674
|
+
|
|
675
|
+
const authState: OidcAuthStateModel = makeAuthStateModel(context, updateFunction)
|
|
676
|
+
const states: OidcAuthState[] = authState.getState()
|
|
677
|
+
```
|
package/build/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AppConfig, AppContext } from '@owlmeans/web-client';
|
|
2
|
+
import type { OidcAuthStateModel } from './types.js';
|
|
3
|
+
export declare const makeAuthStateModel: <C extends AppConfig, T extends AppContext<C>>(context: T, updateState: (uid: string) => Promise<{
|
|
4
|
+
entityId?: string;
|
|
5
|
+
did?: string;
|
|
6
|
+
}>) => OidcAuthStateModel;
|
|
7
|
+
//# sourceMappingURL=auth-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-state.d.ts","sourceRoot":"","sources":["../src/auth-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjE,OAAO,KAAK,EAAuB,kBAAkB,EAAqC,MAAM,YAAY,CAAA;AAM5G,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,WACpE,CAAC,eACG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,KACzE,kBAoHF,CAAA"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import Cookies from 'universal-cookie';
|
|
2
|
+
import { OidcAuthState } from './consts.js';
|
|
3
|
+
const stateCache = {};
|
|
4
|
+
export const makeAuthStateModel = (context, updateState) => {
|
|
5
|
+
const cookies = new Cookies(null, { path: '/' });
|
|
6
|
+
const config = context.cfg;
|
|
7
|
+
const store = () => context.auth().store();
|
|
8
|
+
const stackKey = `_oidc-interaction:stack`;
|
|
9
|
+
const cookieKey = config.oidc.clientCookie?.interaction?.name ?? '_interaction';
|
|
10
|
+
const cookieTtl = () => {
|
|
11
|
+
const ttl = (config.oidc.clientCookie?.interaction?.ttl ?? 3600) * 1000;
|
|
12
|
+
const expiration = new Date();
|
|
13
|
+
expiration.setTime(expiration.getTime() + ttl);
|
|
14
|
+
return expiration;
|
|
15
|
+
};
|
|
16
|
+
const model = {
|
|
17
|
+
state: new Set(),
|
|
18
|
+
uid: '',
|
|
19
|
+
getState: () => [...model.state],
|
|
20
|
+
init: async (uid, reset = false) => {
|
|
21
|
+
const currentUid = cookies.get(cookieKey);
|
|
22
|
+
if (currentUid != null && uid === '-') {
|
|
23
|
+
uid = currentUid;
|
|
24
|
+
}
|
|
25
|
+
if (uid == null) {
|
|
26
|
+
throw new SyntaxError('no-uid');
|
|
27
|
+
}
|
|
28
|
+
if (stateCache[uid] != null) {
|
|
29
|
+
Object.assign(model, stateCache[uid]);
|
|
30
|
+
return model;
|
|
31
|
+
}
|
|
32
|
+
if (currentUid != null && currentUid !== uid) {
|
|
33
|
+
let stack = await store().load(stackKey);
|
|
34
|
+
if (stack == null) {
|
|
35
|
+
stack = { stack: [], id: stackKey };
|
|
36
|
+
}
|
|
37
|
+
else if (reset) {
|
|
38
|
+
await store().save({ stack: [], id: stackKey });
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const token = await context.auth().authenticated();
|
|
42
|
+
stack.stack.push({ uid: currentUid, token });
|
|
43
|
+
await store().save(stack);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// cookies.remove(cookieKey)
|
|
47
|
+
cookies.set(cookieKey, uid, { expires: cookieTtl() });
|
|
48
|
+
model.uid = uid;
|
|
49
|
+
await model.updateAuthState(uid);
|
|
50
|
+
stateCache[uid] = { ...model };
|
|
51
|
+
return model;
|
|
52
|
+
},
|
|
53
|
+
updateAuthState: async (uid) => {
|
|
54
|
+
model.state = new Set();
|
|
55
|
+
const serverState = await updateState(uid);
|
|
56
|
+
model.entityId = serverState.entityId ?? config.defaultEntityId;
|
|
57
|
+
model.did = serverState.did;
|
|
58
|
+
let user = null;
|
|
59
|
+
if (await context.auth().authenticated()) {
|
|
60
|
+
user = context.auth().user();
|
|
61
|
+
model.state.add(OidcAuthState.Authenticated);
|
|
62
|
+
}
|
|
63
|
+
if (model.entityId === user?.entityId) {
|
|
64
|
+
model.state.add(OidcAuthState.SameEntity);
|
|
65
|
+
}
|
|
66
|
+
if (model.did != null) {
|
|
67
|
+
model.state.add(OidcAuthState.IdLinked);
|
|
68
|
+
}
|
|
69
|
+
stateCache[uid] = { ...model };
|
|
70
|
+
return [...model.state];
|
|
71
|
+
},
|
|
72
|
+
isAuthenticated: () => model.state.has(OidcAuthState.Authenticated),
|
|
73
|
+
isSameEntity: () => model.state.has(OidcAuthState.SameEntity),
|
|
74
|
+
isIdLinked: () => model.state.has(OidcAuthState.IdLinked),
|
|
75
|
+
profileExists: () => model.state.has(OidcAuthState.ProfileExists),
|
|
76
|
+
isRegistrationAllowed: () => model.state.has(OidcAuthState.RegistrationAllowed),
|
|
77
|
+
finishInteraction: async (skipState = false) => {
|
|
78
|
+
const stack = await store().load(stackKey);
|
|
79
|
+
if (stateCache[model.uid] != null) {
|
|
80
|
+
delete stateCache[model.uid];
|
|
81
|
+
}
|
|
82
|
+
if (stack != null) {
|
|
83
|
+
const item = stack.stack.pop();
|
|
84
|
+
if (item != null) {
|
|
85
|
+
model.uid = item.uid;
|
|
86
|
+
cookies.set(cookieKey, model.uid, { expires: cookieTtl() });
|
|
87
|
+
await store().save(stack);
|
|
88
|
+
if (!skipState) {
|
|
89
|
+
await model.updateAuthState(model.uid);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
return model;
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=auth-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-state.js","sourceRoot":"","sources":["../src/auth-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,oBAAY,aAAa;IACvB,aAAa,kBAAkB;IAC/B,UAAU,gBAAgB;IAC1B,QAAQ,cAAc;IACtB,aAAa,mBAAmB;IAChC,mBAAmB,yBAAyB;CAC7C"}
|
package/build/consts.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export var OidcAuthState;
|
|
2
|
+
(function (OidcAuthState) {
|
|
3
|
+
OidcAuthState["Authenticated"] = "authenticated";
|
|
4
|
+
OidcAuthState["SameEntity"] = "same-entity";
|
|
5
|
+
OidcAuthState["IdLinked"] = "id-linked";
|
|
6
|
+
OidcAuthState["ProfileExists"] = "profile-exists";
|
|
7
|
+
OidcAuthState["RegistrationAllowed"] = "registration-allowed";
|
|
8
|
+
})(OidcAuthState || (OidcAuthState = {}));
|
|
9
|
+
//# sourceMappingURL=consts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAN,IAAY,aAMX;AAND,WAAY,aAAa;IACvB,gDAA+B,CAAA;IAC/B,2CAA0B,CAAA;IAC1B,uCAAsB,CAAA;IACtB,iDAAgC,CAAA;IAChC,6DAA4C,CAAA;AAC9C,CAAC,EANW,aAAa,KAAb,aAAa,QAMxB"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,iBAAiB,CAAA"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,iBAAiB,CAAA"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { OidcSharedConfig } from '@owlmeans/oidc';
|
|
2
|
+
import type { OidcAuthState } from './consts.js';
|
|
3
|
+
import type { ResourceRecord } from '@owlmeans/resource';
|
|
4
|
+
export interface OidcAuthStateModel extends AuthStateProperties {
|
|
5
|
+
init: (uid: string, reset?: boolean) => Promise<OidcAuthStateModel>;
|
|
6
|
+
updateAuthState: (uid: string) => Promise<OidcAuthState[]>;
|
|
7
|
+
isAuthenticated: () => boolean;
|
|
8
|
+
isSameEntity: () => boolean;
|
|
9
|
+
isIdLinked: () => boolean;
|
|
10
|
+
profileExists: () => boolean;
|
|
11
|
+
isRegistrationAllowed: () => boolean;
|
|
12
|
+
finishInteraction: (skipState?: boolean) => Promise<void>;
|
|
13
|
+
getState: () => OidcAuthState[];
|
|
14
|
+
}
|
|
15
|
+
export interface AuthStateProperties {
|
|
16
|
+
did?: string;
|
|
17
|
+
entityId?: string;
|
|
18
|
+
state: Set<OidcAuthState>;
|
|
19
|
+
uid: string;
|
|
20
|
+
}
|
|
21
|
+
export interface WithSharedConfig {
|
|
22
|
+
oidc: OidcSharedConfig;
|
|
23
|
+
}
|
|
24
|
+
export interface OidcInteraction extends ResourceRecord {
|
|
25
|
+
stack: {
|
|
26
|
+
token: string | null;
|
|
27
|
+
uid: string;
|
|
28
|
+
}[];
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB;IAC7D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAA;IACnE,eAAe,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;IAE1D,eAAe,EAAE,MAAM,OAAO,CAAA;IAC9B,YAAY,EAAE,MAAM,OAAO,CAAA;IAC3B,UAAU,EAAE,MAAM,OAAO,CAAA;IACzB,aAAa,EAAE,MAAM,OAAO,CAAA;IAC5B,qBAAqB,EAAE,MAAM,OAAO,CAAA;IAEpC,iBAAiB,EAAE,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAEzD,QAAQ,EAAE,MAAM,aAAa,EAAE,CAAA;CAChC;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,GAAG,CAAC,aAAa,CAAC,CAAA;IACzB,GAAG,EAAE,MAAM,CAAA;CACZ;AAGD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,gBAAgB,CAAA;CACvB;AAED,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,KAAK,EAAE;QACL,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;QACpB,GAAG,EAAE,MAAM,CAAA;KACZ,EAAE,CAAA;CACJ"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/web-oidc-provider",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc -b",
|
|
7
|
+
"dev": "sleep 372 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
8
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty"
|
|
9
|
+
},
|
|
10
|
+
"main": "build/index.js",
|
|
11
|
+
"module": "build/index.js",
|
|
12
|
+
"types": "build/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./build/index.js",
|
|
16
|
+
"require": "./build/index.js",
|
|
17
|
+
"default": "./build/index.js",
|
|
18
|
+
"module": "./build/index.js",
|
|
19
|
+
"types": "./build/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@owlmeans/auth": "^0.1.0",
|
|
24
|
+
"@owlmeans/oidc": "^0.1.0",
|
|
25
|
+
"@owlmeans/resource": "^0.1.0",
|
|
26
|
+
"@owlmeans/web-client": "^0.1.0",
|
|
27
|
+
"universal-cookie": "^7.2.1"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": "*"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "^18.3.11",
|
|
34
|
+
"nodemon": "^3.1.7",
|
|
35
|
+
"npm-check": "^6.0.1",
|
|
36
|
+
"typescript": "^5.6.3"
|
|
37
|
+
},
|
|
38
|
+
"private": false,
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { AppConfig, AppContext } from '@owlmeans/web-client'
|
|
2
|
+
import type { AuthStateProperties, OidcAuthStateModel, OidcInteraction, WithSharedConfig } from './types.js'
|
|
3
|
+
import Cookies from 'universal-cookie'
|
|
4
|
+
import { OidcAuthState } from './consts.js'
|
|
5
|
+
import type { Auth } from '@owlmeans/auth'
|
|
6
|
+
|
|
7
|
+
const stateCache: Record<string, AuthStateProperties> = {}
|
|
8
|
+
export const makeAuthStateModel = <C extends AppConfig, T extends AppContext<C>>(
|
|
9
|
+
context: T,
|
|
10
|
+
updateState: (uid: string) => Promise<{ entityId?: string, did?: string }>,
|
|
11
|
+
): OidcAuthStateModel => {
|
|
12
|
+
const cookies = new Cookies(null, { path: '/' })
|
|
13
|
+
const config = context.cfg as Partial<AppConfig> as (Partial<AppConfig> & WithSharedConfig)
|
|
14
|
+
|
|
15
|
+
const store = () => context.auth().store<OidcInteraction>()
|
|
16
|
+
|
|
17
|
+
const stackKey = `_oidc-interaction:stack`
|
|
18
|
+
const cookieKey = config.oidc.clientCookie?.interaction?.name ?? '_interaction'
|
|
19
|
+
const cookieTtl = () => {
|
|
20
|
+
const ttl = (config.oidc.clientCookie?.interaction?.ttl ?? 3600) * 1000
|
|
21
|
+
|
|
22
|
+
const expiration = new Date()
|
|
23
|
+
expiration.setTime(expiration.getTime() + ttl)
|
|
24
|
+
|
|
25
|
+
return expiration
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const model: OidcAuthStateModel = {
|
|
29
|
+
state: new Set(),
|
|
30
|
+
|
|
31
|
+
uid: '',
|
|
32
|
+
|
|
33
|
+
getState: () => [...model.state],
|
|
34
|
+
|
|
35
|
+
init: async (uid, reset = false) => {
|
|
36
|
+
const currentUid = cookies.get(cookieKey)
|
|
37
|
+
if (currentUid != null && uid === '-') {
|
|
38
|
+
uid = currentUid
|
|
39
|
+
}
|
|
40
|
+
if (uid == null) {
|
|
41
|
+
throw new SyntaxError('no-uid')
|
|
42
|
+
}
|
|
43
|
+
if (stateCache[uid] != null) {
|
|
44
|
+
Object.assign(model, stateCache[uid])
|
|
45
|
+
|
|
46
|
+
return model
|
|
47
|
+
}
|
|
48
|
+
if (currentUid != null && currentUid !== uid) {
|
|
49
|
+
let stack = await store().load(stackKey)
|
|
50
|
+
if (stack == null) {
|
|
51
|
+
stack = { stack: [], id: stackKey }
|
|
52
|
+
} else if (reset) {
|
|
53
|
+
await store().save({ stack: [], id: stackKey })
|
|
54
|
+
} else {
|
|
55
|
+
const token = await context.auth().authenticated()
|
|
56
|
+
stack.stack.push({ uid: currentUid, token })
|
|
57
|
+
await store().save(stack)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// cookies.remove(cookieKey)
|
|
62
|
+
cookies.set(cookieKey, uid, { expires: cookieTtl() })
|
|
63
|
+
|
|
64
|
+
model.uid = uid
|
|
65
|
+
await model.updateAuthState(uid)
|
|
66
|
+
|
|
67
|
+
stateCache[uid] = { ...model }
|
|
68
|
+
|
|
69
|
+
return model
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
updateAuthState: async uid => {
|
|
73
|
+
model.state = new Set<OidcAuthState>()
|
|
74
|
+
|
|
75
|
+
const serverState = await updateState(uid)
|
|
76
|
+
model.entityId = serverState.entityId ?? config.defaultEntityId
|
|
77
|
+
model.did = serverState.did
|
|
78
|
+
let user: Auth | null = null
|
|
79
|
+
if (await context.auth().authenticated()) {
|
|
80
|
+
user = context.auth().user()
|
|
81
|
+
model.state.add(OidcAuthState.Authenticated)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (model.entityId === user?.entityId) {
|
|
85
|
+
model.state.add(OidcAuthState.SameEntity)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (model.did != null) {
|
|
89
|
+
model.state.add(OidcAuthState.IdLinked)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
stateCache[uid] = { ...model }
|
|
93
|
+
|
|
94
|
+
return [...model.state]
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
isAuthenticated: () => model.state.has(OidcAuthState.Authenticated),
|
|
98
|
+
|
|
99
|
+
isSameEntity: () => model.state.has(OidcAuthState.SameEntity),
|
|
100
|
+
|
|
101
|
+
isIdLinked: () => model.state.has(OidcAuthState.IdLinked),
|
|
102
|
+
|
|
103
|
+
profileExists: () => model.state.has(OidcAuthState.ProfileExists),
|
|
104
|
+
|
|
105
|
+
isRegistrationAllowed: () => model.state.has(OidcAuthState.RegistrationAllowed),
|
|
106
|
+
|
|
107
|
+
finishInteraction: async (skipState = false) => {
|
|
108
|
+
const stack = await store().load(stackKey)
|
|
109
|
+
if (stateCache[model.uid] != null) {
|
|
110
|
+
delete stateCache[model.uid]
|
|
111
|
+
}
|
|
112
|
+
if (stack != null) {
|
|
113
|
+
const item = stack.stack.pop()
|
|
114
|
+
if (item != null) {
|
|
115
|
+
model.uid = item.uid
|
|
116
|
+
cookies.set(cookieKey, model.uid, { expires: cookieTtl() })
|
|
117
|
+
await store().save(stack)
|
|
118
|
+
if (!skipState) {
|
|
119
|
+
await model.updateAuthState(model.uid)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return model
|
|
127
|
+
}
|
package/src/consts.ts
ADDED
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { OidcSharedConfig } from '@owlmeans/oidc'
|
|
2
|
+
import type { OidcAuthState } from './consts.js'
|
|
3
|
+
import type { ResourceRecord } from '@owlmeans/resource'
|
|
4
|
+
|
|
5
|
+
export interface OidcAuthStateModel extends AuthStateProperties {
|
|
6
|
+
init: (uid: string, reset?: boolean) => Promise<OidcAuthStateModel>
|
|
7
|
+
updateAuthState: (uid: string) => Promise<OidcAuthState[]>
|
|
8
|
+
|
|
9
|
+
isAuthenticated: () => boolean
|
|
10
|
+
isSameEntity: () => boolean
|
|
11
|
+
isIdLinked: () => boolean
|
|
12
|
+
profileExists: () => boolean
|
|
13
|
+
isRegistrationAllowed: () => boolean
|
|
14
|
+
|
|
15
|
+
finishInteraction: (skipState?: boolean) => Promise<void>
|
|
16
|
+
|
|
17
|
+
getState: () => OidcAuthState[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AuthStateProperties {
|
|
21
|
+
did?: string
|
|
22
|
+
entityId?: string
|
|
23
|
+
state: Set<OidcAuthState>
|
|
24
|
+
uid: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export interface WithSharedConfig {
|
|
29
|
+
oidc: OidcSharedConfig
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OidcInteraction extends ResourceRecord {
|
|
33
|
+
stack: {
|
|
34
|
+
token: string | null
|
|
35
|
+
uid: string
|
|
36
|
+
}[]
|
|
37
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"../tsconfig.default.json",
|
|
4
|
+
"../tsconfig.react.json",
|
|
5
|
+
],
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"rootDir": "./src/", /* Specify the root folder within your source files. */
|
|
8
|
+
"outDir": "./build/", /* Specify an output folder for all emitted files. */
|
|
9
|
+
"moduleResolution": "Bundler",
|
|
10
|
+
},
|
|
11
|
+
"exclude": [
|
|
12
|
+
"./dist/**/*",
|
|
13
|
+
"./build/**/*",
|
|
14
|
+
"./*.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/auth-state.ts","./src/consts.ts","./src/index.ts","./src/types.ts"],"version":"5.6.3"}
|