@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 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,8 @@
1
+ export declare enum OidcAuthState {
2
+ Authenticated = "authenticated",
3
+ SameEntity = "same-entity",
4
+ IdLinked = "id-linked",
5
+ ProfileExists = "profile-exists",
6
+ RegistrationAllowed = "registration-allowed"
7
+ }
8
+ //# sourceMappingURL=consts.d.ts.map
@@ -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"}
@@ -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"}
@@ -0,0 +1,3 @@
1
+ export type * from './types.js';
2
+ export * from './auth-state.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export * from './auth-state.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,iBAAiB,CAAA"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
@@ -0,0 +1,8 @@
1
+
2
+ export enum OidcAuthState {
3
+ Authenticated = 'authenticated',
4
+ SameEntity = 'same-entity',
5
+ IdLinked = 'id-linked',
6
+ ProfileExists = 'profile-exists',
7
+ RegistrationAllowed = 'registration-allowed',
8
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+
2
+ export type * from './types.js'
3
+ export * from './auth-state.js'
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"}