@owlmeans/server-oidc-rp 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 +520 -0
- package/build/.gitkeep +0 -0
- package/build/actions/index.d.ts +3 -0
- package/build/actions/index.d.ts.map +1 -0
- package/build/actions/index.js +3 -0
- package/build/actions/index.js.map +1 -0
- package/build/actions/init.d.ts +3 -0
- package/build/actions/init.d.ts.map +1 -0
- package/build/actions/init.js +68 -0
- package/build/actions/init.js.map +1 -0
- package/build/actions/process.d.ts +17 -0
- package/build/actions/process.d.ts.map +1 -0
- package/build/actions/process.js +80 -0
- package/build/actions/process.js.map +1 -0
- package/build/auth/consts.d.ts +2 -0
- package/build/auth/consts.d.ts.map +1 -0
- package/build/auth/consts.js +2 -0
- package/build/auth/consts.js.map +1 -0
- package/build/auth/index.d.ts +2 -0
- package/build/auth/index.d.ts.map +1 -0
- package/build/auth/index.js +2 -0
- package/build/auth/index.js.map +1 -0
- package/build/auth/plugins/index.d.ts +2 -0
- package/build/auth/plugins/index.d.ts.map +1 -0
- package/build/auth/plugins/index.js +6 -0
- package/build/auth/plugins/index.js.map +1 -0
- package/build/auth/plugins/oidc-client.d.ts +50 -0
- package/build/auth/plugins/oidc-client.d.ts.map +1 -0
- package/build/auth/plugins/oidc-client.js +211 -0
- package/build/auth/plugins/oidc-client.js.map +1 -0
- package/build/auth/plugins/types.d.ts +1 -0
- package/build/auth/plugins/types.d.ts.map +1 -0
- package/build/auth/plugins/types.js +2 -0
- package/build/auth/plugins/types.js.map +1 -0
- package/build/consts.d.ts +20 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +20 -0
- package/build/consts.js.map +1 -0
- package/build/gate.d.ts +3 -0
- package/build/gate.d.ts.map +1 -0
- package/build/gate.js +22 -0
- package/build/gate.js.map +1 -0
- package/build/guard.d.ts +6 -0
- package/build/guard.d.ts.map +1 -0
- package/build/guard.js +13 -0
- package/build/guard.js.map +1 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/model/consts.d.ts +6 -0
- package/build/model/consts.d.ts.map +1 -0
- package/build/model/consts.js +7 -0
- package/build/model/consts.js.map +1 -0
- package/build/model/gate.d.ts +4 -0
- package/build/model/gate.d.ts.map +1 -0
- package/build/model/gate.js +98 -0
- package/build/model/gate.js.map +1 -0
- package/build/model/types.d.ts +25 -0
- package/build/model/types.d.ts.map +1 -0
- package/build/model/types.js +2 -0
- package/build/model/types.js.map +1 -0
- package/build/models.d.ts +1 -0
- package/build/models.d.ts.map +1 -0
- package/build/models.js +2 -0
- package/build/models.js.map +1 -0
- package/build/modules.d.ts +3 -0
- package/build/modules.d.ts.map +1 -0
- package/build/modules.js +12 -0
- package/build/modules.js.map +1 -0
- package/build/service.d.ts +3 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +186 -0
- package/build/service.js.map +1 -0
- package/build/types.d.ts +55 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/auth.d.ts +5 -0
- package/build/utils/auth.d.ts.map +1 -0
- package/build/utils/auth.js +48 -0
- package/build/utils/auth.js.map +1 -0
- package/build/utils/cache.d.ts +8 -0
- package/build/utils/cache.d.ts.map +1 -0
- package/build/utils/cache.js +7 -0
- package/build/utils/cache.js.map +1 -0
- package/build/utils/index.d.ts +5 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +4 -0
- package/build/utils/index.js.map +1 -0
- package/build/utils/types.d.ts +10 -0
- package/build/utils/types.d.ts.map +1 -0
- package/build/utils/types.js +2 -0
- package/build/utils/types.js.map +1 -0
- package/build/utils/wrapped.d.ts +4 -0
- package/build/utils/wrapped.d.ts.map +1 -0
- package/build/utils/wrapped.js +3 -0
- package/build/utils/wrapped.js.map +1 -0
- package/build/wrapper.d.ts +3 -0
- package/build/wrapper.d.ts.map +1 -0
- package/build/wrapper.js +75 -0
- package/build/wrapper.js.map +1 -0
- package/package.json +71 -0
- package/src/actions/index.ts +3 -0
- package/src/actions/init.ts +90 -0
- package/src/actions/process.ts +113 -0
- package/src/auth/consts.ts +2 -0
- package/src/auth/index.ts +2 -0
- package/src/auth/plugins/index.ts +8 -0
- package/src/auth/plugins/oidc-client.ts +246 -0
- package/src/auth/plugins/types.ts +0 -0
- package/src/consts.ts +26 -0
- package/src/gate.ts +29 -0
- package/src/guard.ts +25 -0
- package/src/index.ts +7 -0
- package/src/model/consts.ts +6 -0
- package/src/model/gate.ts +129 -0
- package/src/model/types.ts +31 -0
- package/src/models.ts +1 -0
- package/src/modules.ts +30 -0
- package/src/service.ts +251 -0
- package/src/types.ts +64 -0
- package/src/utils/auth.ts +67 -0
- package/src/utils/cache.ts +12 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/types.ts +10 -0
- package/src/utils/wrapped.ts +6 -0
- package/src/wrapper.ts +92 -0
- package/tsconfig.json +15 -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,520 @@
|
|
|
1
|
+
# @owlmeans/server-oidc-rp
|
|
2
|
+
|
|
3
|
+
The **@owlmeans/server-oidc-rp** package provides server-side OpenID Connect Relying Party (RP) functionality for OwlMeans Common Libraries, enabling secure authentication and authorization through OIDC protocols in fullstack applications.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This package serves as a comprehensive OIDC client implementation for server-side applications, specifically designed for:
|
|
8
|
+
|
|
9
|
+
- **OIDC Client Management**: Create and manage OpenID Connect clients with automatic discovery
|
|
10
|
+
- **Token Operations**: Handle authorization code grants, client credentials, token refresh, and introspection
|
|
11
|
+
- **Provider Integration**: Connect to multiple OIDC providers with flexible configuration
|
|
12
|
+
- **Account Linking**: Link external OIDC accounts with internal user profiles
|
|
13
|
+
- **Security Integration**: Integrate with OwlMeans authentication and authorization system
|
|
14
|
+
- **Module-based Authentication**: Provide OIDC guards and gates for route protection
|
|
15
|
+
|
|
16
|
+
## Key Concepts
|
|
17
|
+
|
|
18
|
+
### OpenID Connect Relying Party
|
|
19
|
+
A Relying Party (RP) is an OAuth 2.0 client application that uses OpenID Connect to verify the identity of end-users. This package implements server-side RP functionality.
|
|
20
|
+
|
|
21
|
+
### Provider Configuration
|
|
22
|
+
The package supports multiple OIDC providers with configuration including discovery URLs, client credentials, and service mappings.
|
|
23
|
+
|
|
24
|
+
### Account Linking
|
|
25
|
+
Enables linking external OIDC provider accounts with internal user accounts, supporting both automatic linking and manual credential mapping.
|
|
26
|
+
|
|
27
|
+
### Token Management
|
|
28
|
+
Comprehensive token lifecycle management including acquisition, refresh, introspection, and validation.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @owlmeans/server-oidc-rp
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API Reference
|
|
37
|
+
|
|
38
|
+
### Types
|
|
39
|
+
|
|
40
|
+
#### `OidcClientService`
|
|
41
|
+
Main service interface for OIDC client operations.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
interface OidcClientService extends InitializedService {
|
|
45
|
+
getConfiguration(clientId: string | Partial<OidcProviderConfig>): Promise<OidcClientDescriptor>
|
|
46
|
+
getClient(clientId: string | OidcClientDescriptor | Partial<OidcProviderConfig>): Promise<OidcClientAdapter>
|
|
47
|
+
getConfig(clientId: string | Partial<OidcProviderConfig>): Promise<OidcProviderConfig | undefined>
|
|
48
|
+
getDefault(): string | undefined
|
|
49
|
+
|
|
50
|
+
registerTemporaryProvider(config: OidcProviderConfig): OidcProviderConfig
|
|
51
|
+
unregisterTemporaryProvider(params: Partial<OidcProviderConfig>): void
|
|
52
|
+
hasProvider(params: Partial<OidcProviderConfig>): boolean
|
|
53
|
+
entityToClientId(params: Partial<OidcProviderConfig>): string
|
|
54
|
+
|
|
55
|
+
providerApi(): ProviderApiService | null
|
|
56
|
+
accountLinking(): AccountLinkingService | null
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### `OidcClientAdapter`
|
|
61
|
+
Interface for individual OIDC client instances.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
interface OidcClientAdapter {
|
|
65
|
+
getMetadata(): ServerMetadata
|
|
66
|
+
getClientId(): string
|
|
67
|
+
getConfig(): OidcProviderConfig
|
|
68
|
+
makeAuthUrl(params: Record<string, string>): string
|
|
69
|
+
grantWithCredentials(): Promise<TokenSet>
|
|
70
|
+
grantWithCode(currentUrl: string, checks: AuthorizationCodeGrantChecks, params: Record<string, string>): Promise<TokenSet>
|
|
71
|
+
refresh(tokenSet: TokenSetParameters | string): Promise<TokenSetParameters>
|
|
72
|
+
introspect(tokenSet: TokenSetParameters, type?: string): Promise<IntrospectionResponse>
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### `AccountLinkingService`
|
|
77
|
+
Service interface for linking OIDC accounts with internal profiles.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
interface AccountLinkingService extends InitializedService {
|
|
81
|
+
getLinkedProfile(details: ProviderProfileDetails): Promise<AuthPayload | null>
|
|
82
|
+
linkProfile(details: ProviderProfileDetails, meta: AccountMeta): Promise<AuthPayload>
|
|
83
|
+
linkCredentials(details: ProviderProfileDetails): Promise<AuthPayload>
|
|
84
|
+
getOwnerProfiles(entityId: string): Promise<Profile[]>
|
|
85
|
+
getOwnerCredentials(userId: string, entityId?: string, type?: string): Promise<AuthCredentials | undefined>
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### `ProviderApiService`
|
|
90
|
+
Service interface for interacting with OIDC provider APIs.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
interface ProviderApiService extends InitializedService {
|
|
94
|
+
getUserDetails(token: string, userId: string): Promise<OidcUserDetails>
|
|
95
|
+
getSettings(token: string, realm: string): Promise<OidcProviderSettings>
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### `Config`
|
|
100
|
+
Configuration interface extending ServerConfig.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
interface Config extends ServerConfig, WithSharedConfig {
|
|
104
|
+
oidc: OidcRpConfig
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface OidcRpConfig extends OidcSharedConfig {
|
|
108
|
+
accountLinkingService?: string
|
|
109
|
+
providerApiService?: string
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Factory Functions
|
|
114
|
+
|
|
115
|
+
#### `makeOidcClientService(alias?: string): OidcClientService`
|
|
116
|
+
|
|
117
|
+
Creates an OIDC client service instance.
|
|
118
|
+
|
|
119
|
+
**Parameters:**
|
|
120
|
+
- `alias`: Service alias (defaults to 'oidc-client')
|
|
121
|
+
|
|
122
|
+
**Returns:** OidcClientService instance
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { makeOidcClientService } from '@owlmeans/server-oidc-rp'
|
|
126
|
+
|
|
127
|
+
const oidcService = makeOidcClientService('my-oidc-client')
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Guard Functions
|
|
131
|
+
|
|
132
|
+
#### `appendOidcGuard<C extends Config, T extends Context<C>>(context: T, opts?: OidcGuardOptions): T`
|
|
133
|
+
|
|
134
|
+
Appends OIDC guard functionality to a context.
|
|
135
|
+
|
|
136
|
+
**Parameters:**
|
|
137
|
+
- `context`: Server context instance
|
|
138
|
+
- `opts`: Optional OIDC guard configuration
|
|
139
|
+
|
|
140
|
+
**Returns:** Enhanced context with OIDC guard capabilities
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { appendOidcGuard } from '@owlmeans/server-oidc-rp'
|
|
144
|
+
|
|
145
|
+
const enhancedContext = appendOidcGuard(context, {
|
|
146
|
+
defaultProvider: 'keycloak'
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### `setupOidcGuard(modules: CommonModule[], coguards?: string | string[]): void`
|
|
151
|
+
|
|
152
|
+
Sets up OIDC guard functionality for modules.
|
|
153
|
+
|
|
154
|
+
**Parameters:**
|
|
155
|
+
- `modules`: Array of modules to enhance
|
|
156
|
+
- `coguards`: Optional co-guard configurations
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { setupOidcGuard } from '@owlmeans/server-oidc-rp'
|
|
160
|
+
|
|
161
|
+
setupOidcGuard(modules, ['authenticated', 'oidc-verified'])
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Module Setup
|
|
165
|
+
|
|
166
|
+
#### `setupAuthServiceModules(modules: CommonModule[], serviceAlias: string, prefix?: string): void`
|
|
167
|
+
|
|
168
|
+
Sets up authentication service modules with OIDC routes.
|
|
169
|
+
|
|
170
|
+
**Parameters:**
|
|
171
|
+
- `modules`: Array of modules to add OIDC routes to
|
|
172
|
+
- `serviceAlias`: Service alias for the routes
|
|
173
|
+
- `prefix`: URL prefix for OIDC routes (defaults to 'oidc-api')
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { setupAuthServiceModules } from '@owlmeans/server-oidc-rp'
|
|
177
|
+
|
|
178
|
+
setupAuthServiceModules(modules, 'auth-service', 'oidc-api')
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Constants
|
|
182
|
+
|
|
183
|
+
#### Service Aliases
|
|
184
|
+
```typescript
|
|
185
|
+
const DEFAULT_ALIAS = 'oidc-client'
|
|
186
|
+
const DEF_OIDC_ACCOUNT_LINKING = 'oidc-consumer-account-linking'
|
|
187
|
+
const DEF_OIDC_PROVIDER_API = 'oidc-consumer-provider-api'
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### Route Definitions
|
|
191
|
+
```typescript
|
|
192
|
+
const authService = {
|
|
193
|
+
provider: {
|
|
194
|
+
list: 'external-auth:provider:list'
|
|
195
|
+
},
|
|
196
|
+
auth: {
|
|
197
|
+
update: 'external-auth:auth:update'
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### Cache and Timing Constants
|
|
203
|
+
```typescript
|
|
204
|
+
const OIDC_TOKEN_STORE = 'oidc-token-store'
|
|
205
|
+
const PROVIDER_CACHE_TTL = (5 * 60 - 1) * 1000 // ~5 minutes
|
|
206
|
+
const OIDC_AUTH_LIFTETIME = 24 * 3600 * 1000 // 24 hours
|
|
207
|
+
const OIDC_WRAP_FRESHNESS = (5 * 60 - 1) * 1000 // ~5 minutes
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Usage Examples
|
|
211
|
+
|
|
212
|
+
### Basic OIDC Client Setup
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { makeOidcClientService, setupOidcGuard } from '@owlmeans/server-oidc-rp'
|
|
216
|
+
import { makeServerContext, makeServerConfig } from '@owlmeans/server-context'
|
|
217
|
+
|
|
218
|
+
// Create server context with OIDC configuration
|
|
219
|
+
const config = makeServerConfig('auth-server', {
|
|
220
|
+
oidc: {
|
|
221
|
+
providers: [{
|
|
222
|
+
clientId: 'my-app',
|
|
223
|
+
secret: 'client-secret',
|
|
224
|
+
service: 'keycloak',
|
|
225
|
+
basePath: '/realms/my-realm',
|
|
226
|
+
def: true
|
|
227
|
+
}]
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const context = makeServerContext(config)
|
|
232
|
+
|
|
233
|
+
// Create and register OIDC client service
|
|
234
|
+
const oidcService = makeOidcClientService()
|
|
235
|
+
context.registerService(oidcService)
|
|
236
|
+
|
|
237
|
+
// Initialize context
|
|
238
|
+
await context.configure().init()
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Using OIDC Client
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// Get the OIDC service
|
|
245
|
+
const oidcService = context.service<OidcClientService>('oidc-client')
|
|
246
|
+
|
|
247
|
+
// Get OIDC client for default provider
|
|
248
|
+
const defaultClientId = oidcService.getDefault()
|
|
249
|
+
const client = await oidcService.getClient(defaultClientId!)
|
|
250
|
+
|
|
251
|
+
// Create authorization URL
|
|
252
|
+
const authUrl = client.makeAuthUrl({
|
|
253
|
+
redirect_uri: 'https://myapp.com/callback',
|
|
254
|
+
scope: 'openid profile email',
|
|
255
|
+
state: 'random-state'
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
console.log('Redirect user to:', authUrl)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Authorization Code Flow
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// Handle callback from OIDC provider
|
|
265
|
+
app.get('/callback', async (req, res) => {
|
|
266
|
+
const client = await oidcService.getClient('my-app')
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const tokenSet = await client.grantWithCode(
|
|
270
|
+
req.url,
|
|
271
|
+
{
|
|
272
|
+
state: req.query.state,
|
|
273
|
+
code_verifier: 'stored-code-verifier' // if using PKCE
|
|
274
|
+
},
|
|
275
|
+
{ flow: 'authorization_code' }
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
console.log('Access token:', tokenSet.access_token)
|
|
279
|
+
console.log('ID token:', tokenSet.id_token)
|
|
280
|
+
|
|
281
|
+
// Store tokens securely
|
|
282
|
+
res.redirect('/dashboard')
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error('OIDC authentication failed:', error)
|
|
285
|
+
res.redirect('/login?error=auth_failed')
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Client Credentials Flow
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
// Get access token using client credentials
|
|
294
|
+
const client = await oidcService.getClient('service-account')
|
|
295
|
+
const tokenSet = await client.grantWithCredentials()
|
|
296
|
+
|
|
297
|
+
console.log('Service access token:', tokenSet.access_token)
|
|
298
|
+
|
|
299
|
+
// Use token for API calls
|
|
300
|
+
const response = await fetch('https://api.service.com/data', {
|
|
301
|
+
headers: {
|
|
302
|
+
'Authorization': `Bearer ${tokenSet.access_token}`
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Token Refresh
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// Refresh an expired token
|
|
311
|
+
try {
|
|
312
|
+
const newTokenSet = await client.refresh(oldTokenSet)
|
|
313
|
+
console.log('New access token:', newTokenSet.access_token)
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error('Token refresh failed:', error)
|
|
316
|
+
// Redirect to login
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Token Introspection
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// Validate and introspect a token
|
|
324
|
+
const introspection = await client.introspect(tokenSet, 'access_token')
|
|
325
|
+
|
|
326
|
+
if (introspection.active) {
|
|
327
|
+
console.log('Token is valid')
|
|
328
|
+
console.log('Token expires at:', new Date(introspection.exp! * 1000))
|
|
329
|
+
console.log('Token subject:', introspection.sub)
|
|
330
|
+
} else {
|
|
331
|
+
console.log('Token is invalid or expired')
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Multiple Provider Configuration
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
const config = makeServerConfig('multi-provider-app', {
|
|
339
|
+
oidc: {
|
|
340
|
+
providers: [
|
|
341
|
+
{
|
|
342
|
+
clientId: 'keycloak-client',
|
|
343
|
+
secret: 'secret1',
|
|
344
|
+
service: 'keycloak',
|
|
345
|
+
basePath: '/realms/main',
|
|
346
|
+
def: true
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
clientId: 'google-client',
|
|
350
|
+
secret: 'secret2',
|
|
351
|
+
discoveryUrl: 'https://accounts.google.com/.well-known/openid_configuration'
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
// Get specific provider clients
|
|
358
|
+
const keycloakClient = await oidcService.getClient('keycloak-client')
|
|
359
|
+
const googleClient = await oidcService.getClient('google-client')
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Account Linking
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// Setup account linking service
|
|
366
|
+
const accountLinkingService: AccountLinkingService = createService('account-linking', {
|
|
367
|
+
getLinkedProfile: async (details) => {
|
|
368
|
+
// Check if external profile is already linked
|
|
369
|
+
const existing = await database.findLinkedAccount(details.sub, details.iss)
|
|
370
|
+
return existing ? existing.authPayload : null
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
linkProfile: async (details, meta) => {
|
|
374
|
+
// Link external account to existing user
|
|
375
|
+
const user = await database.findUserByUsername(meta.username)
|
|
376
|
+
await database.linkExternalAccount(user.id, details)
|
|
377
|
+
return createAuthPayload(user)
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
linkCredentials: async (details) => {
|
|
381
|
+
// Auto-link based on email or other criteria
|
|
382
|
+
const user = await database.findUserByEmail(details.email)
|
|
383
|
+
if (user) {
|
|
384
|
+
await database.linkExternalAccount(user.id, details)
|
|
385
|
+
return createAuthPayload(user)
|
|
386
|
+
}
|
|
387
|
+
throw new Error('No matching user found')
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
context.registerService(accountLinkingService)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Module Protection with OIDC Guards
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
import { module, guard } from '@owlmeans/module'
|
|
398
|
+
import { route } from '@owlmeans/route'
|
|
399
|
+
|
|
400
|
+
// Create protected modules
|
|
401
|
+
const protectedModules = [
|
|
402
|
+
module(route('profile', '/profile'), guard('oidc-authenticated')),
|
|
403
|
+
module(route('admin', '/admin'), guard(['oidc-authenticated', 'admin-role']))
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
// Setup OIDC guards
|
|
407
|
+
setupOidcGuard(protectedModules, 'oidc-authenticated')
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Error Handling
|
|
411
|
+
|
|
412
|
+
The package throws `AuthManagerError` for various OIDC-related issues:
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import { AuthManagerError } from '@owlmeans/auth'
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const client = await oidcService.getClient('invalid-client')
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (error instanceof AuthManagerError) {
|
|
421
|
+
switch (error.message) {
|
|
422
|
+
case 'oidc.client.basepath':
|
|
423
|
+
console.error('OIDC provider base path not configured')
|
|
424
|
+
break
|
|
425
|
+
case 'oidc.client.service':
|
|
426
|
+
console.error('OIDC provider service not specified')
|
|
427
|
+
break
|
|
428
|
+
case 'oidc.client.client-id':
|
|
429
|
+
console.error('OIDC client ID missing')
|
|
430
|
+
break
|
|
431
|
+
case 'oidc.client.secert':
|
|
432
|
+
console.error('OIDC client secret missing')
|
|
433
|
+
break
|
|
434
|
+
default:
|
|
435
|
+
console.error('OIDC error:', error.message)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Integration Patterns
|
|
442
|
+
|
|
443
|
+
### Express.js Integration
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
import express from 'express'
|
|
447
|
+
import { makeOidcClientService } from '@owlmeans/server-oidc-rp'
|
|
448
|
+
|
|
449
|
+
const app = express()
|
|
450
|
+
|
|
451
|
+
// OIDC login route
|
|
452
|
+
app.get('/login/:provider?', async (req, res) => {
|
|
453
|
+
const providerId = req.params.provider || oidcService.getDefault()
|
|
454
|
+
const client = await oidcService.getClient(providerId!)
|
|
455
|
+
|
|
456
|
+
const authUrl = client.makeAuthUrl({
|
|
457
|
+
redirect_uri: `${req.protocol}://${req.get('host')}/callback`,
|
|
458
|
+
scope: 'openid profile email',
|
|
459
|
+
state: generateState()
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
res.redirect(authUrl)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
// OIDC callback route
|
|
466
|
+
app.get('/callback', async (req, res) => {
|
|
467
|
+
// Handle authorization code flow as shown above
|
|
468
|
+
})
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### API Gateway Integration
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
// Create API gateway with OIDC token validation
|
|
475
|
+
const validateToken = async (req: Request) => {
|
|
476
|
+
const authHeader = req.headers.authorization
|
|
477
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
478
|
+
throw new Error('Missing or invalid authorization header')
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const token = authHeader.substring(7)
|
|
482
|
+
const client = await oidcService.getClient(oidcService.getDefault()!)
|
|
483
|
+
|
|
484
|
+
const introspection = await client.introspect({ access_token: token })
|
|
485
|
+
if (!introspection.active) {
|
|
486
|
+
throw new Error('Invalid or expired token')
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return introspection
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Best Practices
|
|
494
|
+
|
|
495
|
+
1. **Secure Configuration**: Store client secrets securely and never expose them in client-side code
|
|
496
|
+
2. **Token Storage**: Store tokens securely with appropriate encryption and expiration handling
|
|
497
|
+
3. **Error Handling**: Implement comprehensive error handling for all OIDC operations
|
|
498
|
+
4. **Provider Discovery**: Use OIDC discovery when possible for automatic configuration
|
|
499
|
+
5. **Token Validation**: Always validate tokens before using them for authorization decisions
|
|
500
|
+
6. **Account Linking**: Implement secure account linking with proper user consent flows
|
|
501
|
+
7. **Monitoring**: Monitor OIDC operations for security and performance issues
|
|
502
|
+
|
|
503
|
+
## Dependencies
|
|
504
|
+
|
|
505
|
+
This package depends on:
|
|
506
|
+
- `@owlmeans/auth` - Authentication framework
|
|
507
|
+
- `@owlmeans/auth-common` - Common authentication utilities
|
|
508
|
+
- `@owlmeans/context` - Context management
|
|
509
|
+
- `@owlmeans/oidc` - Core OIDC functionality
|
|
510
|
+
- `@owlmeans/server-context` - Server context management
|
|
511
|
+
- `@owlmeans/server-module` - Server module system
|
|
512
|
+
- `openid-client` - OpenID Connect client library
|
|
513
|
+
- `jose` - JSON Web Token utilities
|
|
514
|
+
|
|
515
|
+
## Related Packages
|
|
516
|
+
|
|
517
|
+
- [`@owlmeans/web-oidc-rp`](../web-oidc-rp) - Web client OIDC Relying Party
|
|
518
|
+
- [`@owlmeans/oidc`](../oidc) - Core OIDC functionality
|
|
519
|
+
- [`@owlmeans/server-auth`](../server-auth) - Server authentication framework
|
|
520
|
+
- [`@owlmeans/auth`](../auth) - Authentication foundation
|
package/build/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/actions/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAiBjE,eAAO,MAAM,IAAI,EAAE,kBAwEjB,CAAA"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { handleBody } from '@owlmeans/server-api';
|
|
2
|
+
import { AuthenPayloadError, AuthUnknown, DISPATCHER } from '@owlmeans/auth';
|
|
3
|
+
import { assertContext } from '@owlmeans/context';
|
|
4
|
+
import { authService, DEFAULT_ALIAS } from '../consts.js';
|
|
5
|
+
// import { PROVIDER_CACHE_TTL } from '../consts.js'
|
|
6
|
+
// import type { Client } from 'openid-client'
|
|
7
|
+
import { base64urlnopad as base64 } from '@scure/base';
|
|
8
|
+
import { randomBytes } from '@noble/hashes/utils';
|
|
9
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
10
|
+
import { cache, verifierId } from '../utils/cache.js';
|
|
11
|
+
import { AUTHEN_TIMEFRAME } from '@owlmeans/server-auth';
|
|
12
|
+
export const init = handleBody(async (body, ctx) => {
|
|
13
|
+
if (body.entity == null) {
|
|
14
|
+
throw new AuthenPayloadError('entity');
|
|
15
|
+
}
|
|
16
|
+
const entityId = body.entity;
|
|
17
|
+
const context = assertContext(ctx);
|
|
18
|
+
const oidc = context.service(DEFAULT_ALIAS);
|
|
19
|
+
let client;
|
|
20
|
+
// if (!oidc.hasProvider({ entityId })) {
|
|
21
|
+
/**
|
|
22
|
+
* @TODO We need to move it to some remote resource.
|
|
23
|
+
* And make oidc service itself use such a resource to get required client.
|
|
24
|
+
*/
|
|
25
|
+
const [providers] = await context.module(authService.provider.list).call({
|
|
26
|
+
params: { service: context.cfg.alias ?? context.cfg.service },
|
|
27
|
+
query: { entityId }
|
|
28
|
+
});
|
|
29
|
+
if (providers.length < 1) {
|
|
30
|
+
throw new AuthUnknown();
|
|
31
|
+
}
|
|
32
|
+
console.log("\n\n ************* \n Providers we get from remote:", providers);
|
|
33
|
+
const provider = providers.find(p => p.def === true) ?? providers.shift();
|
|
34
|
+
console.log("\n\nProvider we choose:", provider);
|
|
35
|
+
console.log("************* \n\n");
|
|
36
|
+
if (provider == null) {
|
|
37
|
+
throw new AuthUnknown();
|
|
38
|
+
}
|
|
39
|
+
oidc.registerTemporaryProvider(provider);
|
|
40
|
+
client = await oidc.getClient({ clientId: provider.clientId, entityId });
|
|
41
|
+
// Cache provider for a while - we dont actually need to clean it up
|
|
42
|
+
// setTimeout(() => provider && oidc.unregisterTemporaryProvider(provider), PROVIDER_CACHE_TTL)
|
|
43
|
+
// } else {
|
|
44
|
+
// client = await oidc.getClient({
|
|
45
|
+
// entityId, clientId: oidc.entityToClientId({ entityId })
|
|
46
|
+
// })
|
|
47
|
+
// }
|
|
48
|
+
const verifier = base64.encode(randomBytes(32));
|
|
49
|
+
const challenge = base64.encode(sha256(verifier));
|
|
50
|
+
console.log("\n\n We create verifierID: ", verifierId(challenge), verifier, " with client: ", client.getClientId(), "\n\n");
|
|
51
|
+
await cache(context).create({
|
|
52
|
+
id: verifierId(challenge),
|
|
53
|
+
verifier,
|
|
54
|
+
client: client.getClientId(),
|
|
55
|
+
entityId,
|
|
56
|
+
}, { ttl: AUTHEN_TIMEFRAME / 1000 });
|
|
57
|
+
const [dispatcherUrl] = await context.module(DISPATCHER).call();
|
|
58
|
+
const cfg = client.getConfig();
|
|
59
|
+
const url = client.makeAuthUrl({
|
|
60
|
+
scope: `openid profile email ${cfg?.extraScopes ?? ''}`,
|
|
61
|
+
code_challenge: challenge,
|
|
62
|
+
code_challenge_method: 'S256',
|
|
63
|
+
redirect_uri: dispatcherUrl,
|
|
64
|
+
});
|
|
65
|
+
console.log('Url we got', url);
|
|
66
|
+
return url;
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/actions/init.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RefedModuleHandler } from '@owlmeans/server-module';
|
|
2
|
+
/**
|
|
3
|
+
* Authentication method links together OIDC and OwlMeans auth.
|
|
4
|
+
* 1. We authenticate OIDC redirect back params with the classic OIDC client.
|
|
5
|
+
* 2. We temporary store on the server refresh token and OIDC token.
|
|
6
|
+
* 3. We supply the client with perked OwlMeans Auth token that in fact
|
|
7
|
+
* is the set of OIDC tokens (except refresh token - we keep it here?)
|
|
8
|
+
* 4. We used it in a centralized way to check permissions over OwlMeans Auth
|
|
9
|
+
* central service irrespectively is it OIDC or our custom permissions engine
|
|
10
|
+
* (this is useful to properly support paid accounts and bridge the underlying
|
|
11
|
+
* IAM system implemetation - abstract from it).
|
|
12
|
+
* 5. The method ends with production of OwlMeans authorization token.
|
|
13
|
+
*
|
|
14
|
+
* ! There is also token renewal mechanism that should be implemented nearby.
|
|
15
|
+
*/
|
|
16
|
+
export declare const authenticate: RefedModuleHandler;
|
|
17
|
+
//# sourceMappingURL=process.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/actions/process.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAkBjE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,EAAE,kBAgFzB,CAAA"}
|