@flink-app/oidc-plugin 1.0.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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +846 -0
- package/dist/OidcInternalContext.d.ts +15 -0
- package/dist/OidcInternalContext.d.ts.map +1 -0
- package/dist/OidcInternalContext.js +2 -0
- package/dist/OidcPlugin.d.ts +77 -0
- package/dist/OidcPlugin.d.ts.map +1 -0
- package/dist/OidcPlugin.js +274 -0
- package/dist/OidcPluginContext.d.ts +73 -0
- package/dist/OidcPluginContext.d.ts.map +1 -0
- package/dist/OidcPluginContext.js +2 -0
- package/dist/OidcPluginOptions.d.ts +267 -0
- package/dist/OidcPluginOptions.d.ts.map +1 -0
- package/dist/OidcPluginOptions.js +2 -0
- package/dist/OidcProviderConfig.d.ts +77 -0
- package/dist/OidcProviderConfig.d.ts.map +1 -0
- package/dist/OidcProviderConfig.js +2 -0
- package/dist/handlers/CallbackOidc.d.ts +38 -0
- package/dist/handlers/CallbackOidc.d.ts.map +1 -0
- package/dist/handlers/CallbackOidc.js +219 -0
- package/dist/handlers/InitiateOidc.d.ts +35 -0
- package/dist/handlers/InitiateOidc.d.ts.map +1 -0
- package/dist/handlers/InitiateOidc.js +91 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/providers/OidcProvider.d.ts +90 -0
- package/dist/providers/OidcProvider.d.ts.map +1 -0
- package/dist/providers/OidcProvider.js +208 -0
- package/dist/providers/ProviderRegistry.d.ts +55 -0
- package/dist/providers/ProviderRegistry.d.ts.map +1 -0
- package/dist/providers/ProviderRegistry.js +94 -0
- package/dist/repos/OidcConnectionRepo.d.ts +75 -0
- package/dist/repos/OidcConnectionRepo.d.ts.map +1 -0
- package/dist/repos/OidcConnectionRepo.js +122 -0
- package/dist/repos/OidcSessionRepo.d.ts +57 -0
- package/dist/repos/OidcSessionRepo.d.ts.map +1 -0
- package/dist/repos/OidcSessionRepo.js +91 -0
- package/dist/schemas/CallbackRequest.d.ts +37 -0
- package/dist/schemas/CallbackRequest.d.ts.map +1 -0
- package/dist/schemas/CallbackRequest.js +2 -0
- package/dist/schemas/InitiateRequest.d.ts +17 -0
- package/dist/schemas/InitiateRequest.d.ts.map +1 -0
- package/dist/schemas/InitiateRequest.js +2 -0
- package/dist/schemas/OidcConnection.d.ts +69 -0
- package/dist/schemas/OidcConnection.d.ts.map +1 -0
- package/dist/schemas/OidcConnection.js +2 -0
- package/dist/schemas/OidcProfile.d.ts +69 -0
- package/dist/schemas/OidcProfile.d.ts.map +1 -0
- package/dist/schemas/OidcProfile.js +2 -0
- package/dist/schemas/OidcSession.d.ts +46 -0
- package/dist/schemas/OidcSession.d.ts.map +1 -0
- package/dist/schemas/OidcSession.js +2 -0
- package/dist/schemas/OidcTokenSet.d.ts +42 -0
- package/dist/schemas/OidcTokenSet.d.ts.map +1 -0
- package/dist/schemas/OidcTokenSet.js +2 -0
- package/dist/utils/claims-mapper.d.ts +46 -0
- package/dist/utils/claims-mapper.d.ts.map +1 -0
- package/dist/utils/claims-mapper.js +104 -0
- package/dist/utils/encryption-utils.d.ts +32 -0
- package/dist/utils/encryption-utils.d.ts.map +1 -0
- package/dist/utils/encryption-utils.js +82 -0
- package/dist/utils/error-utils.d.ts +65 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/error-utils.js +150 -0
- package/dist/utils/response-utils.d.ts +18 -0
- package/dist/utils/response-utils.d.ts.map +1 -0
- package/dist/utils/response-utils.js +42 -0
- package/dist/utils/state-utils.d.ts +36 -0
- package/dist/utils/state-utils.d.ts.map +1 -0
- package/dist/utils/state-utils.js +66 -0
- package/examples/basic-oidc.ts +151 -0
- package/examples/multi-provider.ts +146 -0
- package/package.json +44 -0
- package/spec/handlers/InitiateOidc.spec.ts +62 -0
- package/spec/helpers/reporter.ts +34 -0
- package/spec/helpers/test-helpers.ts +108 -0
- package/spec/plugin/OidcPlugin.spec.ts +126 -0
- package/spec/providers/ProviderRegistry.spec.ts +197 -0
- package/spec/repos/OidcConnectionRepo.spec.ts +257 -0
- package/spec/repos/OidcSessionRepo.spec.ts +196 -0
- package/spec/support/jasmine.json +7 -0
- package/spec/utils/claims-mapper.spec.ts +257 -0
- package/spec/utils/encryption-utils.spec.ts +126 -0
- package/spec/utils/error-utils.spec.ts +107 -0
- package/spec/utils/state-utils.spec.ts +102 -0
- package/src/OidcInternalContext.ts +15 -0
- package/src/OidcPlugin.ts +290 -0
- package/src/OidcPluginContext.ts +76 -0
- package/src/OidcPluginOptions.ts +286 -0
- package/src/OidcProviderConfig.ts +87 -0
- package/src/handlers/CallbackOidc.ts +257 -0
- package/src/handlers/InitiateOidc.ts +110 -0
- package/src/index.ts +38 -0
- package/src/providers/OidcProvider.ts +237 -0
- package/src/providers/ProviderRegistry.ts +107 -0
- package/src/repos/OidcConnectionRepo.ts +132 -0
- package/src/repos/OidcSessionRepo.ts +99 -0
- package/src/schemas/CallbackRequest.ts +41 -0
- package/src/schemas/InitiateRequest.ts +17 -0
- package/src/schemas/OidcConnection.ts +80 -0
- package/src/schemas/OidcProfile.ts +79 -0
- package/src/schemas/OidcSession.ts +52 -0
- package/src/schemas/OidcTokenSet.ts +47 -0
- package/src/utils/claims-mapper.ts +114 -0
- package/src/utils/encryption-utils.ts +92 -0
- package/src/utils/error-utils.ts +167 -0
- package/src/utils/response-utils.ts +41 -0
- package/src/utils/state-utils.ts +66 -0
- package/tsconfig.dist.json +9 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Issuer, Client, generators, TokenSet, UserinfoResponse } from "openid-client";
|
|
2
|
+
import { OidcProviderConfig } from "../OidcProviderConfig";
|
|
3
|
+
import OidcProfile from "../schemas/OidcProfile";
|
|
4
|
+
import OidcTokenSet from "../schemas/OidcTokenSet";
|
|
5
|
+
import { mapClaimsToProfile, extractCustomClaims } from "../utils/claims-mapper";
|
|
6
|
+
import { createOidcError, OidcErrorCodes } from "../utils/error-utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generic OIDC Provider implementation using openid-client
|
|
10
|
+
*
|
|
11
|
+
* Supports both OIDC discovery and manual configuration.
|
|
12
|
+
* Handles the complete OIDC flow including:
|
|
13
|
+
* - Authorization URL generation with PKCE
|
|
14
|
+
* - Token exchange (code → tokens)
|
|
15
|
+
* - ID token validation
|
|
16
|
+
* - UserInfo endpoint fetching
|
|
17
|
+
* - Claims mapping to profile
|
|
18
|
+
*/
|
|
19
|
+
export class OidcProvider {
|
|
20
|
+
private config: OidcProviderConfig;
|
|
21
|
+
private issuer: Issuer<Client> | null = null;
|
|
22
|
+
private client: Client | null = null;
|
|
23
|
+
private initialized: boolean = false;
|
|
24
|
+
|
|
25
|
+
constructor(config: OidcProviderConfig) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the provider by discovering or creating the OIDC client
|
|
31
|
+
*
|
|
32
|
+
* Uses OIDC discovery if discoveryUrl is provided, otherwise uses
|
|
33
|
+
* manual endpoint configuration.
|
|
34
|
+
*
|
|
35
|
+
* This is async and should be called before using the provider.
|
|
36
|
+
*/
|
|
37
|
+
async initialize(): Promise<void> {
|
|
38
|
+
if (this.initialized) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Option 1: OIDC Discovery
|
|
44
|
+
if (this.config.discoveryUrl) {
|
|
45
|
+
this.issuer = await Issuer.discover(this.config.discoveryUrl);
|
|
46
|
+
}
|
|
47
|
+
// Option 2: Manual configuration
|
|
48
|
+
else {
|
|
49
|
+
// Validate required endpoints for manual config
|
|
50
|
+
if (!this.config.authorizationEndpoint || !this.config.tokenEndpoint || !this.config.jwksUri) {
|
|
51
|
+
throw createOidcError(
|
|
52
|
+
OidcErrorCodes.PROVIDER_NOT_CONFIGURED,
|
|
53
|
+
"Provider must have either discoveryUrl or manual endpoints (authorizationEndpoint, tokenEndpoint, jwksUri)",
|
|
54
|
+
{ provider: this.config.issuer }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.issuer = new Issuer({
|
|
59
|
+
issuer: this.config.issuer,
|
|
60
|
+
authorization_endpoint: this.config.authorizationEndpoint,
|
|
61
|
+
token_endpoint: this.config.tokenEndpoint,
|
|
62
|
+
userinfo_endpoint: this.config.userinfoEndpoint,
|
|
63
|
+
jwks_uri: this.config.jwksUri,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create OIDC client
|
|
68
|
+
this.client = new this.issuer.Client({
|
|
69
|
+
client_id: this.config.clientId,
|
|
70
|
+
client_secret: this.config.clientSecret,
|
|
71
|
+
redirect_uris: [this.config.callbackUrl],
|
|
72
|
+
response_types: ["code"],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.initialized = true;
|
|
76
|
+
} catch (error: any) {
|
|
77
|
+
throw createOidcError(OidcErrorCodes.DISCOVERY_FAILED, `Failed to initialize OIDC provider: ${error.message}`, {
|
|
78
|
+
issuer: this.config.issuer,
|
|
79
|
+
originalError: error.message,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Generate authorization URL with PKCE and nonce
|
|
86
|
+
*
|
|
87
|
+
* @param params - Authorization parameters
|
|
88
|
+
* @returns Authorization URL to redirect user to
|
|
89
|
+
*/
|
|
90
|
+
async getAuthorizationUrl(params: { state: string; codeVerifier: string; nonce: string }): Promise<string> {
|
|
91
|
+
await this.ensureInitialized();
|
|
92
|
+
|
|
93
|
+
const codeChallenge = generators.codeChallenge(params.codeVerifier);
|
|
94
|
+
const scope = this.config.scope?.join(" ") || "openid email profile";
|
|
95
|
+
|
|
96
|
+
const authUrl = this.client!.authorizationUrl({
|
|
97
|
+
scope,
|
|
98
|
+
state: params.state,
|
|
99
|
+
code_challenge: codeChallenge,
|
|
100
|
+
code_challenge_method: "S256",
|
|
101
|
+
nonce: params.nonce,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return authUrl;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Exchange authorization code for tokens
|
|
109
|
+
*
|
|
110
|
+
* Performs the OAuth 2.0 token exchange and validates the ID token.
|
|
111
|
+
*
|
|
112
|
+
* @param params - Token exchange parameters
|
|
113
|
+
* @returns Token set with access token, ID token, and claims
|
|
114
|
+
*/
|
|
115
|
+
async exchangeCodeForToken(params: { code: string; codeVerifier: string; state: string; nonce: string }): Promise<OidcTokenSet> {
|
|
116
|
+
await this.ensureInitialized();
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const tokenSet: TokenSet = await this.client!.callback(
|
|
120
|
+
this.config.callbackUrl,
|
|
121
|
+
{
|
|
122
|
+
code: params.code,
|
|
123
|
+
state: params.state,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
code_verifier: params.codeVerifier,
|
|
127
|
+
state: params.state,
|
|
128
|
+
nonce: params.nonce,
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Extract claims from ID token (already validated by openid-client)
|
|
133
|
+
const claims = tokenSet.claims();
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
accessToken: tokenSet.access_token!,
|
|
137
|
+
idToken: tokenSet.id_token!,
|
|
138
|
+
refreshToken: tokenSet.refresh_token,
|
|
139
|
+
tokenType: tokenSet.token_type || "Bearer",
|
|
140
|
+
expiresIn: tokenSet.expires_in,
|
|
141
|
+
scope: tokenSet.scope,
|
|
142
|
+
claims,
|
|
143
|
+
};
|
|
144
|
+
} catch (error: any) {
|
|
145
|
+
throw createOidcError(OidcErrorCodes.TOKEN_EXCHANGE_FAILED, `Token exchange failed: ${error.message}`, {
|
|
146
|
+
originalError: error.message,
|
|
147
|
+
errorCode: error.error,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get user profile from UserInfo endpoint
|
|
154
|
+
*
|
|
155
|
+
* Fetches additional user claims from the UserInfo endpoint.
|
|
156
|
+
* Merges with claims from ID token.
|
|
157
|
+
*
|
|
158
|
+
* @param accessToken - Access token from token exchange
|
|
159
|
+
* @returns UserInfo response
|
|
160
|
+
*/
|
|
161
|
+
async getUserInfo(accessToken: string): Promise<UserinfoResponse> {
|
|
162
|
+
await this.ensureInitialized();
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const userinfo = await this.client!.userinfo(accessToken);
|
|
166
|
+
return userinfo;
|
|
167
|
+
} catch (error: any) {
|
|
168
|
+
throw createOidcError(OidcErrorCodes.USERINFO_FAILED, `UserInfo request failed: ${error.message}`, {
|
|
169
|
+
originalError: error.message,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build complete user profile from tokens
|
|
176
|
+
*
|
|
177
|
+
* Combines claims from ID token and UserInfo endpoint,
|
|
178
|
+
* applies custom claim mapping, and returns normalized profile.
|
|
179
|
+
*
|
|
180
|
+
* @param tokenSet - Token set from exchange
|
|
181
|
+
* @param includeUserInfo - Whether to fetch UserInfo endpoint
|
|
182
|
+
* @returns Normalized user profile
|
|
183
|
+
*/
|
|
184
|
+
async buildProfile(tokenSet: OidcTokenSet, includeUserInfo: boolean = true): Promise<OidcProfile> {
|
|
185
|
+
let claims = { ...tokenSet.claims };
|
|
186
|
+
|
|
187
|
+
// Optionally fetch additional claims from UserInfo endpoint
|
|
188
|
+
if (includeUserInfo && this.config.userinfoEndpoint) {
|
|
189
|
+
try {
|
|
190
|
+
const userinfo = await this.getUserInfo(tokenSet.accessToken);
|
|
191
|
+
// Merge UserInfo claims with ID token claims
|
|
192
|
+
claims = { ...claims, ...userinfo };
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// UserInfo is optional - continue with ID token claims only
|
|
195
|
+
console.warn("Failed to fetch UserInfo, using ID token claims only:", error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Apply custom claim mapping if configured
|
|
200
|
+
if (this.config.claimMapping) {
|
|
201
|
+
const customClaims = extractCustomClaims(claims, this.config.claimMapping);
|
|
202
|
+
claims = { ...claims, ...customClaims };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Map to normalized profile
|
|
206
|
+
const profile = mapClaimsToProfile(claims);
|
|
207
|
+
|
|
208
|
+
return profile;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Ensure provider is initialized before use
|
|
213
|
+
*
|
|
214
|
+
* @throws Error if not initialized
|
|
215
|
+
*/
|
|
216
|
+
private async ensureInitialized(): Promise<void> {
|
|
217
|
+
if (!this.initialized) {
|
|
218
|
+
await this.initialize();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!this.client) {
|
|
222
|
+
throw createOidcError(OidcErrorCodes.PROVIDER_NOT_CONFIGURED, "OIDC client not initialized");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get issuer metadata (after initialization)
|
|
228
|
+
*
|
|
229
|
+
* @returns Issuer metadata
|
|
230
|
+
*/
|
|
231
|
+
getIssuerMetadata(): any {
|
|
232
|
+
if (!this.issuer) {
|
|
233
|
+
throw createOidcError(OidcErrorCodes.PROVIDER_NOT_CONFIGURED, "Provider not initialized");
|
|
234
|
+
}
|
|
235
|
+
return this.issuer.metadata;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { OidcProvider } from "./OidcProvider";
|
|
2
|
+
import { OidcProviderConfig } from "../OidcProviderConfig";
|
|
3
|
+
import { createOidcError, OidcErrorCodes } from "../utils/error-utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Provider registry for managing OIDC provider instances
|
|
7
|
+
*
|
|
8
|
+
* Handles:
|
|
9
|
+
* - Static provider configuration
|
|
10
|
+
* - Dynamic provider loading from database
|
|
11
|
+
* - Provider instance caching
|
|
12
|
+
* - Lazy initialization
|
|
13
|
+
*/
|
|
14
|
+
export class ProviderRegistry {
|
|
15
|
+
private staticProviders: Record<string, OidcProviderConfig>;
|
|
16
|
+
private providerInstances: Map<string, OidcProvider> = new Map();
|
|
17
|
+
private providerLoader?: (providerName: string) => Promise<OidcProviderConfig | null>;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
staticProviders: Record<string, OidcProviderConfig>,
|
|
21
|
+
providerLoader?: (providerName: string) => Promise<OidcProviderConfig | null>
|
|
22
|
+
) {
|
|
23
|
+
this.staticProviders = staticProviders;
|
|
24
|
+
this.providerLoader = providerLoader;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get provider instance by name
|
|
29
|
+
*
|
|
30
|
+
* Looks up provider in the following order:
|
|
31
|
+
* 1. Cached instance
|
|
32
|
+
* 2. Static configuration
|
|
33
|
+
* 3. Dynamic loader (if configured)
|
|
34
|
+
*
|
|
35
|
+
* Providers are lazy-initialized on first use and cached.
|
|
36
|
+
*
|
|
37
|
+
* @param providerName - Provider identifier
|
|
38
|
+
* @returns Initialized OIDC provider instance
|
|
39
|
+
* @throws Error if provider not found or initialization fails
|
|
40
|
+
*/
|
|
41
|
+
async getProvider(providerName: string): Promise<OidcProvider> {
|
|
42
|
+
// Check cache first
|
|
43
|
+
const cachedProvider = this.providerInstances.get(providerName);
|
|
44
|
+
if (cachedProvider) {
|
|
45
|
+
return cachedProvider;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Try static configuration
|
|
49
|
+
let config: OidcProviderConfig | null = this.staticProviders[providerName] || null;
|
|
50
|
+
|
|
51
|
+
// Try dynamic loader if not in static config
|
|
52
|
+
if (!config && this.providerLoader) {
|
|
53
|
+
config = await this.providerLoader(providerName);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!config) {
|
|
57
|
+
throw createOidcError(OidcErrorCodes.PROVIDER_NOT_CONFIGURED, `OIDC provider '${providerName}' is not configured`, {
|
|
58
|
+
providerName,
|
|
59
|
+
availableProviders: Object.keys(this.staticProviders),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Create and initialize provider
|
|
64
|
+
const provider = new OidcProvider(config);
|
|
65
|
+
await provider.initialize();
|
|
66
|
+
|
|
67
|
+
// Cache the instance
|
|
68
|
+
this.providerInstances.set(providerName, provider);
|
|
69
|
+
|
|
70
|
+
return provider;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if provider exists in static configuration or can be loaded dynamically
|
|
75
|
+
*
|
|
76
|
+
* @param providerName - Provider identifier
|
|
77
|
+
* @returns true if provider exists, false otherwise
|
|
78
|
+
*/
|
|
79
|
+
hasProvider(providerName: string): boolean {
|
|
80
|
+
return providerName in this.staticProviders || !!this.providerLoader;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clear provider cache
|
|
85
|
+
*
|
|
86
|
+
* Forces re-initialization of providers on next access.
|
|
87
|
+
* Useful when provider configurations change.
|
|
88
|
+
*
|
|
89
|
+
* @param providerName - Optional provider to clear (clears all if not specified)
|
|
90
|
+
*/
|
|
91
|
+
clearCache(providerName?: string): void {
|
|
92
|
+
if (providerName) {
|
|
93
|
+
this.providerInstances.delete(providerName);
|
|
94
|
+
} else {
|
|
95
|
+
this.providerInstances.clear();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get list of configured provider names
|
|
101
|
+
*
|
|
102
|
+
* @returns Array of provider names from static configuration
|
|
103
|
+
*/
|
|
104
|
+
getProviderNames(): string[] {
|
|
105
|
+
return Object.keys(this.staticProviders);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Collection, Db } from "mongodb";
|
|
2
|
+
import OidcConnection from "../schemas/OidcConnection";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Repository for OIDC connections
|
|
6
|
+
*
|
|
7
|
+
* Manages persistent connections between users and OIDC providers.
|
|
8
|
+
* Stores the mapping of app users to IdP subjects, and optionally
|
|
9
|
+
* stores encrypted tokens for API access.
|
|
10
|
+
*/
|
|
11
|
+
export default class OidcConnectionRepo {
|
|
12
|
+
private collection: Collection<OidcConnection>;
|
|
13
|
+
|
|
14
|
+
constructor(collectionName: string, db: Db) {
|
|
15
|
+
this.collection = db.collection<OidcConnection>(collectionName);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a new OIDC connection
|
|
20
|
+
*
|
|
21
|
+
* @param connection - Connection data
|
|
22
|
+
* @returns Created connection with _id
|
|
23
|
+
*/
|
|
24
|
+
async create(connection: Omit<OidcConnection, "_id">): Promise<OidcConnection> {
|
|
25
|
+
const result = await this.collection.insertOne(connection as any);
|
|
26
|
+
return {
|
|
27
|
+
...connection,
|
|
28
|
+
_id: result.insertedId.toString(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Find connection by user ID and provider
|
|
34
|
+
*
|
|
35
|
+
* @param userId - Application user ID
|
|
36
|
+
* @param provider - Provider name
|
|
37
|
+
* @returns Connection or null if not found
|
|
38
|
+
*/
|
|
39
|
+
async findByUserAndProvider(userId: string, provider: string): Promise<OidcConnection | null> {
|
|
40
|
+
const connection = await this.collection.findOne({ userId, provider });
|
|
41
|
+
if (!connection) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
...connection,
|
|
46
|
+
_id: connection._id?.toString(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Find connection by subject and issuer
|
|
52
|
+
*
|
|
53
|
+
* Used to look up users by their IdP identity.
|
|
54
|
+
*
|
|
55
|
+
* @param subject - OIDC subject (sub claim)
|
|
56
|
+
* @param issuer - OIDC issuer (iss claim)
|
|
57
|
+
* @returns Connection or null if not found
|
|
58
|
+
*/
|
|
59
|
+
async findBySubjectAndIssuer(subject: string, issuer: string): Promise<OidcConnection | null> {
|
|
60
|
+
const connection = await this.collection.findOne({ subject, issuer });
|
|
61
|
+
if (!connection) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
...connection,
|
|
66
|
+
_id: connection._id?.toString(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Find all connections for a user
|
|
72
|
+
*
|
|
73
|
+
* @param userId - Application user ID
|
|
74
|
+
* @returns Array of connections
|
|
75
|
+
*/
|
|
76
|
+
async findByUserId(userId: string): Promise<OidcConnection[]> {
|
|
77
|
+
const connections = await this.collection.find({ userId }).toArray();
|
|
78
|
+
return connections.map((conn) => ({
|
|
79
|
+
...conn,
|
|
80
|
+
_id: conn._id?.toString(),
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Update connection
|
|
86
|
+
*
|
|
87
|
+
* Typically used to update tokens when they're refreshed.
|
|
88
|
+
*
|
|
89
|
+
* @param connectionId - Connection _id
|
|
90
|
+
* @param updates - Fields to update
|
|
91
|
+
*/
|
|
92
|
+
async updateOne(connectionId: string, updates: Partial<OidcConnection>): Promise<void> {
|
|
93
|
+
await this.collection.updateOne({ _id: connectionId as any }, { $set: updates });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Delete connection by user and provider
|
|
98
|
+
*
|
|
99
|
+
* @param userId - Application user ID
|
|
100
|
+
* @param provider - Provider name
|
|
101
|
+
*/
|
|
102
|
+
async deleteByUserAndProvider(userId: string, provider: string): Promise<void> {
|
|
103
|
+
await this.collection.deleteOne({ userId, provider });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Delete all connections for a user
|
|
108
|
+
*
|
|
109
|
+
* @param userId - Application user ID
|
|
110
|
+
*/
|
|
111
|
+
async deleteByUserId(userId: string): Promise<number> {
|
|
112
|
+
const result = await this.collection.deleteMany({ userId });
|
|
113
|
+
return result.deletedCount;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Find one connection by query
|
|
118
|
+
*
|
|
119
|
+
* @param query - MongoDB query
|
|
120
|
+
* @returns Connection or null if not found
|
|
121
|
+
*/
|
|
122
|
+
async getOne(query: Partial<OidcConnection>): Promise<OidcConnection | null> {
|
|
123
|
+
const connection = await this.collection.findOne(query as any);
|
|
124
|
+
if (!connection) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
...connection,
|
|
129
|
+
_id: connection._id?.toString(),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Collection, Db } from "mongodb";
|
|
2
|
+
import OidcSession from "../schemas/OidcSession";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Repository for OIDC sessions
|
|
6
|
+
*
|
|
7
|
+
* Manages temporary sessions during the OIDC authorization flow.
|
|
8
|
+
* Sessions are automatically deleted by MongoDB TTL index after expiration.
|
|
9
|
+
*/
|
|
10
|
+
export default class OidcSessionRepo {
|
|
11
|
+
private collection: Collection<OidcSession>;
|
|
12
|
+
|
|
13
|
+
constructor(collectionName: string, db: Db) {
|
|
14
|
+
this.collection = db.collection<OidcSession>(collectionName);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a new OIDC session
|
|
19
|
+
*
|
|
20
|
+
* @param session - Session data
|
|
21
|
+
* @returns Created session with _id
|
|
22
|
+
*/
|
|
23
|
+
async create(session: Omit<OidcSession, "_id">): Promise<OidcSession> {
|
|
24
|
+
const result = await this.collection.insertOne(session as any);
|
|
25
|
+
return {
|
|
26
|
+
...session,
|
|
27
|
+
_id: result.insertedId.toString(),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find session by state parameter
|
|
33
|
+
*
|
|
34
|
+
* Used during callback to validate the state and retrieve session data.
|
|
35
|
+
*
|
|
36
|
+
* @param state - State parameter from callback
|
|
37
|
+
* @returns Session or null if not found
|
|
38
|
+
*/
|
|
39
|
+
async getByState(state: string): Promise<OidcSession | null> {
|
|
40
|
+
const session = await this.collection.findOne({ state });
|
|
41
|
+
if (!session) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
...session,
|
|
46
|
+
_id: session._id?.toString(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Find one session by query
|
|
52
|
+
*
|
|
53
|
+
* @param query - MongoDB query
|
|
54
|
+
* @returns Session or null if not found
|
|
55
|
+
*/
|
|
56
|
+
async getOne(query: Partial<OidcSession>): Promise<OidcSession | null> {
|
|
57
|
+
const session = await this.collection.findOne(query as any);
|
|
58
|
+
if (!session) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...session,
|
|
63
|
+
_id: session._id?.toString(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Delete session by session ID
|
|
69
|
+
*
|
|
70
|
+
* Sessions are one-time use - delete after successful validation.
|
|
71
|
+
*
|
|
72
|
+
* @param sessionId - Session identifier
|
|
73
|
+
*/
|
|
74
|
+
async deleteBySessionId(sessionId: string): Promise<void> {
|
|
75
|
+
await this.collection.deleteOne({ sessionId });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Delete session by state
|
|
80
|
+
*
|
|
81
|
+
* @param state - State parameter
|
|
82
|
+
*/
|
|
83
|
+
async deleteByState(state: string): Promise<void> {
|
|
84
|
+
await this.collection.deleteOne({ state });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Delete all expired sessions
|
|
89
|
+
*
|
|
90
|
+
* This is handled automatically by MongoDB TTL index,
|
|
91
|
+
* but can be called manually for testing or cleanup.
|
|
92
|
+
*/
|
|
93
|
+
async deleteExpired(): Promise<number> {
|
|
94
|
+
const result = await this.collection.deleteMany({
|
|
95
|
+
createdAt: { $lt: new Date(Date.now() - 600000) }, // 10 minutes
|
|
96
|
+
});
|
|
97
|
+
return result.deletedCount;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query parameters for the OIDC callback endpoint
|
|
3
|
+
*
|
|
4
|
+
* GET /oidc/:provider/callback?code=...&state=...&response_type=json
|
|
5
|
+
*/
|
|
6
|
+
export default interface CallbackRequest {
|
|
7
|
+
/**
|
|
8
|
+
* Authorization code from the IdP
|
|
9
|
+
* Required for successful authentication
|
|
10
|
+
*/
|
|
11
|
+
code?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* State parameter for CSRF protection
|
|
15
|
+
* Must match the state stored in the session
|
|
16
|
+
*/
|
|
17
|
+
state?: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Error code from the IdP (if authorization failed)
|
|
21
|
+
* e.g., "access_denied" if user cancelled
|
|
22
|
+
*/
|
|
23
|
+
error?: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Human-readable error description from the IdP
|
|
27
|
+
*/
|
|
28
|
+
error_description?: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Response format for the callback
|
|
32
|
+
* - "json": Return JSON response with user and token
|
|
33
|
+
* - undefined: Redirect to redirectUri with token in URL fragment
|
|
34
|
+
*/
|
|
35
|
+
response_type?: "json";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Index signature for Flink Query type compatibility
|
|
39
|
+
*/
|
|
40
|
+
[key: string]: string | string[] | undefined;
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query parameters for the OIDC initiate endpoint
|
|
3
|
+
*
|
|
4
|
+
* GET /oidc/:provider/initiate?redirectUri=...
|
|
5
|
+
*/
|
|
6
|
+
export default interface InitiateRequest {
|
|
7
|
+
/**
|
|
8
|
+
* Optional redirect URI after successful authentication
|
|
9
|
+
* If not provided, uses the default callbackUrl from provider config
|
|
10
|
+
*/
|
|
11
|
+
redirectUri?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Index signature for Flink Query type compatibility
|
|
15
|
+
*/
|
|
16
|
+
[key: string]: string | string[] | undefined;
|
|
17
|
+
}
|