@backstage/plugin-auth-backend 0.23.1-next.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/dist/authPlugin.cjs.js +75 -0
- package/dist/authPlugin.cjs.js.map +1 -0
- package/dist/database/AuthDatabase.cjs.js +51 -0
- package/dist/database/AuthDatabase.cjs.js.map +1 -0
- package/dist/identity/DatabaseKeyStore.cjs.js +40 -0
- package/dist/identity/DatabaseKeyStore.cjs.js.map +1 -0
- package/dist/identity/FirestoreKeyStore.cjs.js +90 -0
- package/dist/identity/FirestoreKeyStore.cjs.js.map +1 -0
- package/dist/identity/KeyStores.cjs.js +54 -0
- package/dist/identity/KeyStores.cjs.js.map +1 -0
- package/dist/identity/MemoryKeyStore.cjs.js +29 -0
- package/dist/identity/MemoryKeyStore.cjs.js.map +1 -0
- package/dist/identity/StaticKeyStore.cjs.js +91 -0
- package/dist/identity/StaticKeyStore.cjs.js.map +1 -0
- package/dist/identity/StaticTokenIssuer.cjs.js +53 -0
- package/dist/identity/StaticTokenIssuer.cjs.js.map +1 -0
- package/dist/identity/TokenFactory.cjs.js +164 -0
- package/dist/identity/TokenFactory.cjs.js.map +1 -0
- package/dist/identity/UserInfoDatabaseHandler.cjs.js +30 -0
- package/dist/identity/UserInfoDatabaseHandler.cjs.js.map +1 -0
- package/dist/identity/router.cjs.js +77 -0
- package/dist/identity/router.cjs.js.map +1 -0
- package/dist/index.cjs.js +31 -1981
- package/dist/index.cjs.js.map +1 -1
- package/dist/lib/catalog/CatalogIdentityClient.cjs.js +94 -0
- package/dist/lib/catalog/CatalogIdentityClient.cjs.js.map +1 -0
- package/dist/lib/flow/authFlowHelpers.cjs.js +43 -0
- package/dist/lib/flow/authFlowHelpers.cjs.js.map +1 -0
- package/dist/lib/legacy/adaptLegacyOAuthHandler.cjs.js +20 -0
- package/dist/lib/legacy/adaptLegacyOAuthHandler.cjs.js.map +1 -0
- package/dist/lib/legacy/adaptLegacyOAuthSignInResolver.cjs.js +24 -0
- package/dist/lib/legacy/adaptLegacyOAuthSignInResolver.cjs.js.map +1 -0
- package/dist/lib/legacy/adaptOAuthSignInResolverToLegacy.cjs.js +29 -0
- package/dist/lib/legacy/adaptOAuthSignInResolverToLegacy.cjs.js.map +1 -0
- package/dist/lib/oauth/OAuthAdapter.cjs.js +220 -0
- package/dist/lib/oauth/OAuthAdapter.cjs.js.map +1 -0
- package/dist/lib/oauth/OAuthEnvironmentHandler.cjs.js +8 -0
- package/dist/lib/oauth/OAuthEnvironmentHandler.cjs.js.map +1 -0
- package/dist/lib/oauth/helpers.cjs.js +40 -0
- package/dist/lib/oauth/helpers.cjs.js.map +1 -0
- package/dist/lib/passport/PassportStrategyHelper.cjs.js +48 -0
- package/dist/lib/passport/PassportStrategyHelper.cjs.js.map +1 -0
- package/dist/lib/resolvers/CatalogAuthResolverContext.cjs.js +116 -0
- package/dist/lib/resolvers/CatalogAuthResolverContext.cjs.js.map +1 -0
- package/dist/providers/atlassian/provider.cjs.js +20 -0
- package/dist/providers/atlassian/provider.cjs.js.map +1 -0
- package/dist/providers/auth0/provider.cjs.js +20 -0
- package/dist/providers/auth0/provider.cjs.js.map +1 -0
- package/dist/providers/aws-alb/provider.cjs.js +18 -0
- package/dist/providers/aws-alb/provider.cjs.js.map +1 -0
- package/dist/providers/azure-easyauth/provider.cjs.js +18 -0
- package/dist/providers/azure-easyauth/provider.cjs.js.map +1 -0
- package/dist/providers/bitbucket/provider.cjs.js +25 -0
- package/dist/providers/bitbucket/provider.cjs.js.map +1 -0
- package/dist/providers/bitbucketServer/provider.cjs.js +46 -0
- package/dist/providers/bitbucketServer/provider.cjs.js.map +1 -0
- package/dist/providers/cloudflare-access/provider.cjs.js +22 -0
- package/dist/providers/cloudflare-access/provider.cjs.js.map +1 -0
- package/dist/providers/createAuthProviderIntegration.cjs.js +11 -0
- package/dist/providers/createAuthProviderIntegration.cjs.js.map +1 -0
- package/dist/providers/gcp-iap/provider.cjs.js +18 -0
- package/dist/providers/gcp-iap/provider.cjs.js.map +1 -0
- package/dist/providers/github/provider.cjs.js +61 -0
- package/dist/providers/github/provider.cjs.js.map +1 -0
- package/dist/providers/gitlab/provider.cjs.js +20 -0
- package/dist/providers/gitlab/provider.cjs.js.map +1 -0
- package/dist/providers/google/provider.cjs.js +26 -0
- package/dist/providers/google/provider.cjs.js.map +1 -0
- package/dist/providers/microsoft/provider.cjs.js +27 -0
- package/dist/providers/microsoft/provider.cjs.js.map +1 -0
- package/dist/providers/oauth2/provider.cjs.js +20 -0
- package/dist/providers/oauth2/provider.cjs.js.map +1 -0
- package/dist/providers/oauth2-proxy/provider.cjs.js +18 -0
- package/dist/providers/oauth2-proxy/provider.cjs.js.map +1 -0
- package/dist/providers/oidc/provider.cjs.js +37 -0
- package/dist/providers/oidc/provider.cjs.js.map +1 -0
- package/dist/providers/okta/provider.cjs.js +47 -0
- package/dist/providers/okta/provider.cjs.js.map +1 -0
- package/dist/providers/onelogin/provider.cjs.js +20 -0
- package/dist/providers/onelogin/provider.cjs.js.map +1 -0
- package/dist/providers/prepareBackstageIdentityResponse.cjs.js +8 -0
- package/dist/providers/prepareBackstageIdentityResponse.cjs.js.map +1 -0
- package/dist/providers/providers.cjs.js +62 -0
- package/dist/providers/providers.cjs.js.map +1 -0
- package/dist/providers/resolvers.cjs.js +27 -0
- package/dist/providers/resolvers.cjs.js.map +1 -0
- package/dist/providers/router.cjs.js +111 -0
- package/dist/providers/router.cjs.js.map +1 -0
- package/dist/providers/saml/provider.cjs.js +121 -0
- package/dist/providers/saml/provider.cjs.js.map +1 -0
- package/dist/service/readBackstageTokenExpiration.cjs.js +27 -0
- package/dist/service/readBackstageTokenExpiration.cjs.js.map +1 -0
- package/dist/service/router.cjs.js +127 -0
- package/dist/service/router.cjs.js.map +1 -0
- package/package.json +25 -25
package/dist/index.cjs.js
CHANGED
|
@@ -2,1985 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
var
|
|
11
|
-
var
|
|
12
|
-
var
|
|
13
|
-
var
|
|
14
|
-
var
|
|
15
|
-
var
|
|
16
|
-
var
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
var lodash = require('lodash');
|
|
37
|
-
var luxon = require('luxon');
|
|
38
|
-
var uuid = require('uuid');
|
|
39
|
-
var firestore = require('@google-cloud/firestore');
|
|
40
|
-
var fs = require('fs');
|
|
41
|
-
var session = require('express-session');
|
|
42
|
-
var connectSessionKnex = require('connect-session-knex');
|
|
43
|
-
var passport = require('passport');
|
|
44
|
-
var config = require('@backstage/config');
|
|
45
|
-
var types = require('@backstage/types');
|
|
46
|
-
var url = require('url');
|
|
47
|
-
|
|
48
|
-
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
49
|
-
|
|
50
|
-
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
51
|
-
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
52
|
-
var cookieParser__default = /*#__PURE__*/_interopDefaultCompat(cookieParser);
|
|
53
|
-
var crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
|
|
54
|
-
var session__default = /*#__PURE__*/_interopDefaultCompat(session);
|
|
55
|
-
var connectSessionKnex__default = /*#__PURE__*/_interopDefaultCompat(connectSessionKnex);
|
|
56
|
-
var passport__default = /*#__PURE__*/_interopDefaultCompat(passport);
|
|
57
|
-
|
|
58
|
-
function adaptLegacyOAuthHandler(authHandler) {
|
|
59
|
-
return authHandler && (async (result, ctx) => authHandler(
|
|
60
|
-
{
|
|
61
|
-
fullProfile: result.fullProfile,
|
|
62
|
-
accessToken: result.session.accessToken,
|
|
63
|
-
params: {
|
|
64
|
-
scope: result.session.scope,
|
|
65
|
-
id_token: result.session.idToken,
|
|
66
|
-
token_type: result.session.tokenType,
|
|
67
|
-
expires_in: result.session.expiresInSeconds
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
ctx
|
|
71
|
-
));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function adaptLegacyOAuthSignInResolver(signInResolver) {
|
|
75
|
-
return signInResolver && (async (input, ctx) => signInResolver(
|
|
76
|
-
{
|
|
77
|
-
profile: input.profile,
|
|
78
|
-
result: {
|
|
79
|
-
fullProfile: input.result.fullProfile,
|
|
80
|
-
accessToken: input.result.session.accessToken,
|
|
81
|
-
refreshToken: input.result.session.refreshToken,
|
|
82
|
-
params: {
|
|
83
|
-
scope: input.result.session.scope,
|
|
84
|
-
id_token: input.result.session.idToken,
|
|
85
|
-
token_type: input.result.session.tokenType,
|
|
86
|
-
expires_in: input.result.session.expiresInSeconds
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
ctx
|
|
91
|
-
));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function adaptOAuthSignInResolverToLegacy(resolvers) {
|
|
95
|
-
const legacyResolvers = {};
|
|
96
|
-
for (const name of Object.keys(resolvers)) {
|
|
97
|
-
const resolver = resolvers[name];
|
|
98
|
-
legacyResolvers[name] = () => async (input, ctx) => resolver(
|
|
99
|
-
{
|
|
100
|
-
profile: input.profile,
|
|
101
|
-
result: {
|
|
102
|
-
fullProfile: input.result.fullProfile,
|
|
103
|
-
session: {
|
|
104
|
-
accessToken: input.result.accessToken,
|
|
105
|
-
expiresInSeconds: input.result.params.expires_in,
|
|
106
|
-
scope: input.result.params.scope,
|
|
107
|
-
idToken: input.result.params.id_token,
|
|
108
|
-
tokenType: input.result.params.token_type ?? "bearer",
|
|
109
|
-
refreshToken: input.result.refreshToken
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
ctx
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
return legacyResolvers;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function createAuthProviderIntegration(config) {
|
|
120
|
-
return Object.freeze({
|
|
121
|
-
...config,
|
|
122
|
-
resolvers: Object.freeze(config.resolvers ?? {})
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const atlassian = createAuthProviderIntegration({
|
|
127
|
-
create(options) {
|
|
128
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
129
|
-
authenticator: pluginAuthBackendModuleAtlassianProvider.atlassianAuthenticator,
|
|
130
|
-
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
131
|
-
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const auth0 = createAuthProviderIntegration({
|
|
137
|
-
create(options) {
|
|
138
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
139
|
-
authenticator: pluginAuthBackendModuleAuth0Provider.auth0Authenticator,
|
|
140
|
-
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
141
|
-
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const awsAlb = createAuthProviderIntegration({
|
|
147
|
-
create(options) {
|
|
148
|
-
return pluginAuthNode.createProxyAuthProviderFactory({
|
|
149
|
-
authenticator: pluginAuthBackendModuleAwsAlbProvider.awsAlbAuthenticator,
|
|
150
|
-
profileTransform: options?.authHandler,
|
|
151
|
-
signInResolver: options?.signIn?.resolver
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const bitbucket = createAuthProviderIntegration({
|
|
157
|
-
create(options) {
|
|
158
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
159
|
-
authenticator: pluginAuthBackendModuleBitbucketProvider.bitbucketAuthenticator,
|
|
160
|
-
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
161
|
-
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
162
|
-
});
|
|
163
|
-
},
|
|
164
|
-
resolvers: adaptOAuthSignInResolverToLegacy({
|
|
165
|
-
userIdMatchingUserEntityAnnotation: pluginAuthBackendModuleBitbucketProvider.bitbucketSignInResolvers.userIdMatchingUserEntityAnnotation(),
|
|
166
|
-
usernameMatchingUserEntityAnnotation: pluginAuthBackendModuleBitbucketProvider.bitbucketSignInResolvers.usernameMatchingUserEntityAnnotation()
|
|
167
|
-
})
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const cfAccess = createAuthProviderIntegration({
|
|
171
|
-
create(options) {
|
|
172
|
-
return pluginAuthNode.createProxyAuthProviderFactory({
|
|
173
|
-
authenticator: pluginAuthBackendModuleCloudflareAccessProvider.createCloudflareAccessAuthenticator({
|
|
174
|
-
cache: options.cache
|
|
175
|
-
}),
|
|
176
|
-
profileTransform: options?.authHandler,
|
|
177
|
-
signInResolver: options?.signIn?.resolver,
|
|
178
|
-
signInResolverFactories: pluginAuthBackendModuleCloudflareAccessProvider.cloudflareAccessSignInResolvers
|
|
179
|
-
});
|
|
180
|
-
},
|
|
181
|
-
resolvers: pluginAuthBackendModuleCloudflareAccessProvider.cloudflareAccessSignInResolvers
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const gcpIap = createAuthProviderIntegration({
|
|
185
|
-
create(options) {
|
|
186
|
-
return pluginAuthNode.createProxyAuthProviderFactory({
|
|
187
|
-
authenticator: pluginAuthBackendModuleGcpIapProvider.gcpIapAuthenticator,
|
|
188
|
-
profileTransform: options?.authHandler,
|
|
189
|
-
signInResolver: options?.signIn?.resolver
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
const github = createAuthProviderIntegration({
|
|
195
|
-
create(options) {
|
|
196
|
-
const authHandler = options?.authHandler;
|
|
197
|
-
const signInResolver = options?.signIn?.resolver;
|
|
198
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
199
|
-
authenticator: pluginAuthBackendModuleGithubProvider.githubAuthenticator,
|
|
200
|
-
profileTransform: authHandler && (async (result, ctx) => authHandler(
|
|
201
|
-
{
|
|
202
|
-
fullProfile: result.fullProfile,
|
|
203
|
-
accessToken: result.session.accessToken,
|
|
204
|
-
params: {
|
|
205
|
-
scope: result.session.scope,
|
|
206
|
-
expires_in: result.session.expiresInSeconds ? String(result.session.expiresInSeconds) : "",
|
|
207
|
-
refresh_token_expires_in: result.session.refreshTokenExpiresInSeconds ? String(result.session.refreshTokenExpiresInSeconds) : ""
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
ctx
|
|
211
|
-
)),
|
|
212
|
-
signInResolver: signInResolver && (async ({ profile, result }, ctx) => signInResolver(
|
|
213
|
-
{
|
|
214
|
-
profile,
|
|
215
|
-
result: {
|
|
216
|
-
fullProfile: result.fullProfile,
|
|
217
|
-
accessToken: result.session.accessToken,
|
|
218
|
-
refreshToken: result.session.refreshToken,
|
|
219
|
-
params: {
|
|
220
|
-
scope: result.session.scope,
|
|
221
|
-
expires_in: result.session.expiresInSeconds ? String(result.session.expiresInSeconds) : "",
|
|
222
|
-
refresh_token_expires_in: result.session.refreshTokenExpiresInSeconds ? String(result.session.refreshTokenExpiresInSeconds) : ""
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
ctx
|
|
227
|
-
))
|
|
228
|
-
});
|
|
229
|
-
},
|
|
230
|
-
resolvers: {
|
|
231
|
-
/**
|
|
232
|
-
* Looks up the user by matching their GitHub username to the entity name.
|
|
233
|
-
*/
|
|
234
|
-
usernameMatchingUserEntityName: () => {
|
|
235
|
-
return async (info, ctx) => {
|
|
236
|
-
const { fullProfile } = info.result;
|
|
237
|
-
const userId = fullProfile.username;
|
|
238
|
-
if (!userId) {
|
|
239
|
-
throw new Error(`GitHub user profile does not contain a username`);
|
|
240
|
-
}
|
|
241
|
-
return ctx.signInWithCatalogUser({ entityRef: { name: userId } });
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
const gitlab = createAuthProviderIntegration({
|
|
248
|
-
create(options) {
|
|
249
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
250
|
-
authenticator: pluginAuthBackendModuleGitlabProvider.gitlabAuthenticator,
|
|
251
|
-
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
252
|
-
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
const google = createAuthProviderIntegration({
|
|
258
|
-
create(options) {
|
|
259
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
260
|
-
authenticator: pluginAuthBackendModuleGoogleProvider.googleAuthenticator,
|
|
261
|
-
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
262
|
-
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
263
|
-
});
|
|
264
|
-
},
|
|
265
|
-
resolvers: adaptOAuthSignInResolverToLegacy({
|
|
266
|
-
emailLocalPartMatchingUserEntityName: pluginAuthNode.commonSignInResolvers.emailLocalPartMatchingUserEntityName(),
|
|
267
|
-
emailMatchingUserEntityProfileEmail: pluginAuthNode.commonSignInResolvers.emailMatchingUserEntityProfileEmail(),
|
|
268
|
-
emailMatchingUserEntityAnnotation: pluginAuthBackendModuleGoogleProvider.googleSignInResolvers.emailMatchingUserEntityAnnotation()
|
|
269
|
-
})
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const microsoft = createAuthProviderIntegration({
|
|
273
|
-
create(options) {
|
|
274
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
275
|
-
authenticator: pluginAuthBackendModuleMicrosoftProvider.microsoftAuthenticator,
|
|
276
|
-
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
277
|
-
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
278
|
-
});
|
|
279
|
-
},
|
|
280
|
-
resolvers: adaptOAuthSignInResolverToLegacy({
|
|
281
|
-
emailLocalPartMatchingUserEntityName: pluginAuthNode.commonSignInResolvers.emailLocalPartMatchingUserEntityName(),
|
|
282
|
-
emailMatchingUserEntityProfileEmail: pluginAuthNode.commonSignInResolvers.emailMatchingUserEntityProfileEmail(),
|
|
283
|
-
emailMatchingUserEntityAnnotation: pluginAuthBackendModuleMicrosoftProvider.microsoftSignInResolvers.emailMatchingUserEntityAnnotation(),
|
|
284
|
-
userIdMatchingUserEntityAnnotation: pluginAuthBackendModuleMicrosoftProvider.microsoftSignInResolvers.userIdMatchingUserEntityAnnotation()
|
|
285
|
-
})
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const oauth2 = createAuthProviderIntegration({
|
|
289
|
-
create(options) {
|
|
290
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
291
|
-
authenticator: pluginAuthBackendModuleOauth2Provider.oauth2Authenticator,
|
|
292
|
-
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
293
|
-
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
const oauth2Proxy = createAuthProviderIntegration({
|
|
299
|
-
create(options) {
|
|
300
|
-
return pluginAuthNode.createProxyAuthProviderFactory({
|
|
301
|
-
authenticator: pluginAuthBackendModuleOauth2ProxyProvider.oauth2ProxyAuthenticator,
|
|
302
|
-
profileTransform: options?.authHandler,
|
|
303
|
-
signInResolver: options?.signIn?.resolver
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const commonByEmailLocalPartResolver = async (info, ctx) => {
|
|
309
|
-
const { profile } = info;
|
|
310
|
-
if (!profile.email) {
|
|
311
|
-
throw new Error("Login failed, user profile does not contain an email");
|
|
312
|
-
}
|
|
313
|
-
const [localPart] = profile.email.split("@");
|
|
314
|
-
return ctx.signInWithCatalogUser({
|
|
315
|
-
entityRef: { name: localPart }
|
|
316
|
-
});
|
|
317
|
-
};
|
|
318
|
-
const commonByEmailResolver = async (info, ctx) => {
|
|
319
|
-
const { profile } = info;
|
|
320
|
-
if (!profile.email) {
|
|
321
|
-
throw new Error("Login failed, user profile does not contain an email");
|
|
322
|
-
}
|
|
323
|
-
return ctx.signInWithCatalogUser({
|
|
324
|
-
filter: {
|
|
325
|
-
"spec.profile.email": profile.email
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
const oidc = createAuthProviderIntegration({
|
|
331
|
-
create(options) {
|
|
332
|
-
const authHandler = options?.authHandler;
|
|
333
|
-
const signInResolver = options?.signIn?.resolver;
|
|
334
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
335
|
-
authenticator: pluginAuthBackendModuleOidcProvider.oidcAuthenticator,
|
|
336
|
-
profileTransform: authHandler && ((result, context) => authHandler(result.fullProfile, context)),
|
|
337
|
-
signInResolver: signInResolver && ((info, context) => signInResolver(
|
|
338
|
-
{
|
|
339
|
-
result: info.result.fullProfile,
|
|
340
|
-
profile: info.profile
|
|
341
|
-
},
|
|
342
|
-
context
|
|
343
|
-
))
|
|
344
|
-
});
|
|
345
|
-
},
|
|
346
|
-
resolvers: {
|
|
347
|
-
/**
|
|
348
|
-
* Looks up the user by matching their email local part to the entity name.
|
|
349
|
-
*/
|
|
350
|
-
emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver,
|
|
351
|
-
/**
|
|
352
|
-
* Looks up the user by matching their email to the entity email.
|
|
353
|
-
*/
|
|
354
|
-
emailMatchingUserEntityProfileEmail: () => commonByEmailResolver
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
const okta = createAuthProviderIntegration({
|
|
359
|
-
create(options) {
|
|
360
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
361
|
-
authenticator: pluginAuthBackendModuleOktaProvider.oktaAuthenticator,
|
|
362
|
-
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
363
|
-
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
364
|
-
});
|
|
365
|
-
},
|
|
366
|
-
resolvers: {
|
|
367
|
-
/**
|
|
368
|
-
* Looks up the user by matching their email local part to the entity name.
|
|
369
|
-
*/
|
|
370
|
-
emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver,
|
|
371
|
-
/**
|
|
372
|
-
* Looks up the user by matching their email to the entity email.
|
|
373
|
-
*/
|
|
374
|
-
emailMatchingUserEntityProfileEmail: () => commonByEmailResolver,
|
|
375
|
-
/**
|
|
376
|
-
* Looks up the user by matching their email to the `okta.com/email` annotation.
|
|
377
|
-
*/
|
|
378
|
-
emailMatchingUserEntityAnnotation() {
|
|
379
|
-
return async (info, ctx) => {
|
|
380
|
-
const { profile } = info;
|
|
381
|
-
if (!profile.email) {
|
|
382
|
-
throw new Error("Okta profile contained no email");
|
|
383
|
-
}
|
|
384
|
-
return ctx.signInWithCatalogUser({
|
|
385
|
-
annotations: {
|
|
386
|
-
"okta.com/email": profile.email
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const onelogin = createAuthProviderIntegration({
|
|
395
|
-
create(options) {
|
|
396
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
397
|
-
authenticator: pluginAuthBackendModuleOneloginProvider.oneLoginAuthenticator,
|
|
398
|
-
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
399
|
-
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
const executeRedirectStrategy = async (req, providerStrategy, options) => {
|
|
405
|
-
return new Promise((resolve) => {
|
|
406
|
-
const strategy = Object.create(providerStrategy);
|
|
407
|
-
strategy.redirect = (url, status) => {
|
|
408
|
-
resolve({ url, status: status ?? void 0 });
|
|
409
|
-
};
|
|
410
|
-
strategy.authenticate(req, { ...options });
|
|
411
|
-
});
|
|
412
|
-
};
|
|
413
|
-
const executeFrameHandlerStrategy = async (req, providerStrategy, options) => {
|
|
414
|
-
return new Promise(
|
|
415
|
-
(resolve, reject) => {
|
|
416
|
-
const strategy = Object.create(providerStrategy);
|
|
417
|
-
strategy.success = (result, privateInfo) => {
|
|
418
|
-
resolve({ result, privateInfo });
|
|
419
|
-
};
|
|
420
|
-
strategy.fail = (info) => {
|
|
421
|
-
reject(new Error(`Authentication rejected, ${info.message ?? ""}`));
|
|
422
|
-
};
|
|
423
|
-
strategy.error = (error) => {
|
|
424
|
-
let message = `Authentication failed, ${error.message}`;
|
|
425
|
-
if (error.oauthError?.data) {
|
|
426
|
-
try {
|
|
427
|
-
const errorData = JSON.parse(error.oauthError.data);
|
|
428
|
-
if (errorData.message) {
|
|
429
|
-
message += ` - ${errorData.message}`;
|
|
430
|
-
}
|
|
431
|
-
} catch (parseError) {
|
|
432
|
-
message += ` - ${error.oauthError}`;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
reject(new Error(message));
|
|
436
|
-
};
|
|
437
|
-
strategy.redirect = () => {
|
|
438
|
-
reject(new Error("Unexpected redirect"));
|
|
439
|
-
};
|
|
440
|
-
strategy.authenticate(req, { ...{} });
|
|
441
|
-
}
|
|
442
|
-
);
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
const safelyEncodeURIComponent = (value) => {
|
|
446
|
-
return encodeURIComponent(value).replace(/'/g, "%27");
|
|
447
|
-
};
|
|
448
|
-
const postMessageResponse = (res, appOrigin, response) => {
|
|
449
|
-
const jsonData = JSON.stringify(response);
|
|
450
|
-
const base64Data = safelyEncodeURIComponent(jsonData);
|
|
451
|
-
const base64Origin = safelyEncodeURIComponent(appOrigin);
|
|
452
|
-
const script = `
|
|
453
|
-
var authResponse = decodeURIComponent('${base64Data}');
|
|
454
|
-
var origin = decodeURIComponent('${base64Origin}');
|
|
455
|
-
var originInfo = {'type': 'config_info', 'targetOrigin': origin};
|
|
456
|
-
(window.opener || window.parent).postMessage(originInfo, '*');
|
|
457
|
-
(window.opener || window.parent).postMessage(JSON.parse(authResponse), origin);
|
|
458
|
-
setTimeout(() => {
|
|
459
|
-
window.close();
|
|
460
|
-
}, 100); // same as the interval of the core-app-api lib/loginPopup.ts (to address race conditions)
|
|
461
|
-
`;
|
|
462
|
-
const hash = crypto__default.default.createHash("sha256").update(script).digest("base64");
|
|
463
|
-
res.setHeader("Content-Type", "text/html");
|
|
464
|
-
res.setHeader("X-Frame-Options", "sameorigin");
|
|
465
|
-
res.setHeader("Content-Security-Policy", `script-src 'sha256-${hash}'`);
|
|
466
|
-
res.end(`<html><body><script>${script}<\/script></body></html>`);
|
|
467
|
-
};
|
|
468
|
-
const ensuresXRequestedWith = (req) => {
|
|
469
|
-
const requiredHeader = req.header("X-Requested-With");
|
|
470
|
-
if (!requiredHeader || requiredHeader !== "XMLHttpRequest") {
|
|
471
|
-
return false;
|
|
472
|
-
}
|
|
473
|
-
return true;
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
const prepareBackstageIdentityResponse = pluginAuthNode.prepareBackstageIdentityResponse;
|
|
477
|
-
|
|
478
|
-
class SamlAuthProvider {
|
|
479
|
-
strategy;
|
|
480
|
-
signInResolver;
|
|
481
|
-
authHandler;
|
|
482
|
-
resolverContext;
|
|
483
|
-
appUrl;
|
|
484
|
-
constructor(options) {
|
|
485
|
-
this.appUrl = options.appUrl;
|
|
486
|
-
this.signInResolver = options.signInResolver;
|
|
487
|
-
this.authHandler = options.authHandler;
|
|
488
|
-
this.resolverContext = options.resolverContext;
|
|
489
|
-
const verifier = (profile, done) => {
|
|
490
|
-
done(null, { fullProfile: profile });
|
|
491
|
-
};
|
|
492
|
-
this.strategy = new passportSaml.Strategy(options, verifier, verifier);
|
|
493
|
-
}
|
|
494
|
-
async start(req, res) {
|
|
495
|
-
const { url } = await executeRedirectStrategy(req, this.strategy, {});
|
|
496
|
-
res.redirect(url);
|
|
497
|
-
}
|
|
498
|
-
async frameHandler(req, res) {
|
|
499
|
-
try {
|
|
500
|
-
const { result } = await executeFrameHandlerStrategy(
|
|
501
|
-
req,
|
|
502
|
-
this.strategy
|
|
503
|
-
);
|
|
504
|
-
const { profile } = await this.authHandler(result, this.resolverContext);
|
|
505
|
-
const response = {
|
|
506
|
-
profile,
|
|
507
|
-
providerInfo: {}
|
|
508
|
-
};
|
|
509
|
-
if (this.signInResolver) {
|
|
510
|
-
const signInResponse = await this.signInResolver(
|
|
511
|
-
{
|
|
512
|
-
result,
|
|
513
|
-
profile
|
|
514
|
-
},
|
|
515
|
-
this.resolverContext
|
|
516
|
-
);
|
|
517
|
-
response.backstageIdentity = prepareBackstageIdentityResponse(signInResponse);
|
|
518
|
-
}
|
|
519
|
-
return postMessageResponse(res, this.appUrl, {
|
|
520
|
-
type: "authorization_response",
|
|
521
|
-
response
|
|
522
|
-
});
|
|
523
|
-
} catch (error) {
|
|
524
|
-
const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
525
|
-
return postMessageResponse(res, this.appUrl, {
|
|
526
|
-
type: "authorization_response",
|
|
527
|
-
error: { name, message }
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
async logout(_req, res) {
|
|
532
|
-
res.end();
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
const saml = createAuthProviderIntegration({
|
|
536
|
-
create(options) {
|
|
537
|
-
return ({ providerId, globalConfig, config, resolverContext }) => {
|
|
538
|
-
const authHandler = options?.authHandler ? options.authHandler : async ({ fullProfile }) => ({
|
|
539
|
-
profile: {
|
|
540
|
-
email: fullProfile.email,
|
|
541
|
-
displayName: fullProfile.displayName
|
|
542
|
-
}
|
|
543
|
-
});
|
|
544
|
-
return new SamlAuthProvider({
|
|
545
|
-
callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`,
|
|
546
|
-
entryPoint: config.getString("entryPoint"),
|
|
547
|
-
logoutUrl: config.getOptionalString("logoutUrl"),
|
|
548
|
-
audience: config.getString("audience"),
|
|
549
|
-
issuer: config.getString("issuer"),
|
|
550
|
-
idpCert: config.getString("cert"),
|
|
551
|
-
privateKey: config.getOptionalString("privateKey"),
|
|
552
|
-
authnContext: config.getOptionalStringArray("authnContext"),
|
|
553
|
-
identifierFormat: config.getOptionalString("identifierFormat"),
|
|
554
|
-
decryptionPvk: config.getOptionalString("decryptionPvk"),
|
|
555
|
-
signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
|
|
556
|
-
digestAlgorithm: config.getOptionalString("digestAlgorithm"),
|
|
557
|
-
acceptedClockSkewMs: config.getOptionalNumber("acceptedClockSkewMs"),
|
|
558
|
-
wantAuthnResponseSigned: config.getOptionalBoolean(
|
|
559
|
-
"wantAuthnResponseSigned"
|
|
560
|
-
),
|
|
561
|
-
wantAssertionsSigned: config.getOptionalBoolean("wantAssertionsSigned"),
|
|
562
|
-
appUrl: globalConfig.appUrl,
|
|
563
|
-
authHandler,
|
|
564
|
-
signInResolver: options?.signIn?.resolver,
|
|
565
|
-
resolverContext
|
|
566
|
-
});
|
|
567
|
-
};
|
|
568
|
-
},
|
|
569
|
-
resolvers: {
|
|
570
|
-
/**
|
|
571
|
-
* Looks up the user by matching their nameID to the entity name.
|
|
572
|
-
*/
|
|
573
|
-
nameIdMatchingUserEntityName() {
|
|
574
|
-
return async (info, ctx) => {
|
|
575
|
-
const id = info.result.fullProfile.nameID;
|
|
576
|
-
if (!id) {
|
|
577
|
-
throw new errors.AuthenticationError("No nameID found in SAML response");
|
|
578
|
-
}
|
|
579
|
-
return ctx.signInWithCatalogUser({
|
|
580
|
-
entityRef: { name: id }
|
|
581
|
-
});
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
const bitbucketServer = createAuthProviderIntegration({
|
|
588
|
-
create(options) {
|
|
589
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
590
|
-
authenticator: pluginAuthBackendModuleBitbucketServerProvider.bitbucketServerAuthenticator,
|
|
591
|
-
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
592
|
-
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
593
|
-
});
|
|
594
|
-
},
|
|
595
|
-
resolvers: {
|
|
596
|
-
/**
|
|
597
|
-
* Looks up the user by matching their email to the entity email.
|
|
598
|
-
*/
|
|
599
|
-
emailMatchingUserEntityProfileEmail: () => {
|
|
600
|
-
const resolver = pluginAuthBackendModuleBitbucketServerProvider.bitbucketServerSignInResolvers.emailMatchingUserEntityProfileEmail();
|
|
601
|
-
return async (info, ctx) => {
|
|
602
|
-
return resolver(
|
|
603
|
-
{
|
|
604
|
-
profile: info.profile,
|
|
605
|
-
result: {
|
|
606
|
-
fullProfile: info.result.fullProfile,
|
|
607
|
-
session: {
|
|
608
|
-
accessToken: info.result.accessToken,
|
|
609
|
-
tokenType: info.result.params.token_type ?? "bearer",
|
|
610
|
-
scope: info.result.params.scope,
|
|
611
|
-
expiresInSeconds: info.result.params.expires_in,
|
|
612
|
-
refreshToken: info.result.refreshToken
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
},
|
|
616
|
-
ctx
|
|
617
|
-
);
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
const easyAuth = createAuthProviderIntegration({
|
|
624
|
-
create(options) {
|
|
625
|
-
return pluginAuthNode.createProxyAuthProviderFactory({
|
|
626
|
-
authenticator: pluginAuthBackendModuleAzureEasyauthProvider.azureEasyAuthAuthenticator,
|
|
627
|
-
profileTransform: options?.authHandler,
|
|
628
|
-
signInResolver: options?.signIn?.resolver
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
const providers = Object.freeze({
|
|
634
|
-
atlassian,
|
|
635
|
-
auth0,
|
|
636
|
-
awsAlb,
|
|
637
|
-
bitbucket,
|
|
638
|
-
bitbucketServer,
|
|
639
|
-
cfAccess,
|
|
640
|
-
gcpIap,
|
|
641
|
-
github,
|
|
642
|
-
gitlab,
|
|
643
|
-
google,
|
|
644
|
-
microsoft,
|
|
645
|
-
oauth2,
|
|
646
|
-
oauth2Proxy,
|
|
647
|
-
oidc,
|
|
648
|
-
okta,
|
|
649
|
-
onelogin,
|
|
650
|
-
saml,
|
|
651
|
-
easyAuth
|
|
652
|
-
});
|
|
653
|
-
const defaultAuthProviderFactories = {
|
|
654
|
-
google: google.create(),
|
|
655
|
-
github: github.create(),
|
|
656
|
-
gitlab: gitlab.create(),
|
|
657
|
-
saml: saml.create(),
|
|
658
|
-
okta: okta.create(),
|
|
659
|
-
auth0: auth0.create(),
|
|
660
|
-
microsoft: microsoft.create(),
|
|
661
|
-
easyAuth: easyAuth.create(),
|
|
662
|
-
oauth2: oauth2.create(),
|
|
663
|
-
oidc: oidc.create(),
|
|
664
|
-
onelogin: onelogin.create(),
|
|
665
|
-
awsalb: awsAlb.create(),
|
|
666
|
-
bitbucket: bitbucket.create(),
|
|
667
|
-
bitbucketServer: bitbucketServer.create(),
|
|
668
|
-
atlassian: atlassian.create()
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
class CatalogIdentityClient {
|
|
672
|
-
catalogApi;
|
|
673
|
-
auth;
|
|
674
|
-
constructor(options) {
|
|
675
|
-
this.catalogApi = options.catalogApi;
|
|
676
|
-
const { auth } = backendCommon.createLegacyAuthAdapters({
|
|
677
|
-
auth: options.auth,
|
|
678
|
-
httpAuth: options.httpAuth,
|
|
679
|
-
discovery: options.discovery,
|
|
680
|
-
tokenManager: options.tokenManager
|
|
681
|
-
});
|
|
682
|
-
this.auth = auth;
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* Looks up a single user using a query.
|
|
686
|
-
*
|
|
687
|
-
* Throws a NotFoundError or ConflictError if 0 or multiple users are found.
|
|
688
|
-
*/
|
|
689
|
-
async findUser(query) {
|
|
690
|
-
const filter = {
|
|
691
|
-
kind: "user"
|
|
692
|
-
};
|
|
693
|
-
for (const [key, value] of Object.entries(query.annotations)) {
|
|
694
|
-
filter[`metadata.annotations.${key}`] = value;
|
|
695
|
-
}
|
|
696
|
-
const { token } = await this.auth.getPluginRequestToken({
|
|
697
|
-
onBehalfOf: await this.auth.getOwnServiceCredentials(),
|
|
698
|
-
targetPluginId: "catalog"
|
|
699
|
-
});
|
|
700
|
-
const { items } = await this.catalogApi.getEntities({ filter }, { token });
|
|
701
|
-
if (items.length !== 1) {
|
|
702
|
-
if (items.length > 1) {
|
|
703
|
-
throw new errors.ConflictError("User lookup resulted in multiple matches");
|
|
704
|
-
} else {
|
|
705
|
-
throw new errors.NotFoundError("User not found");
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
return items[0];
|
|
709
|
-
}
|
|
710
|
-
/**
|
|
711
|
-
* Resolve additional entity claims from the catalog, using the passed-in entity names. Designed
|
|
712
|
-
* to be used within a `signInResolver` where additional entity claims might be provided, but
|
|
713
|
-
* group membership and transient group membership lean on imported catalog relations.
|
|
714
|
-
*
|
|
715
|
-
* Returns a superset of the entity names that can be passed directly to `issueToken` as `ent`.
|
|
716
|
-
*/
|
|
717
|
-
async resolveCatalogMembership(query) {
|
|
718
|
-
const { entityRefs, logger } = query;
|
|
719
|
-
const resolvedEntityRefs = entityRefs.map((ref) => {
|
|
720
|
-
try {
|
|
721
|
-
const parsedRef = catalogModel.parseEntityRef(ref.toLocaleLowerCase("en-US"), {
|
|
722
|
-
defaultKind: "user",
|
|
723
|
-
defaultNamespace: "default"
|
|
724
|
-
});
|
|
725
|
-
return parsedRef;
|
|
726
|
-
} catch {
|
|
727
|
-
logger?.warn(`Failed to parse entityRef from ${ref}, ignoring`);
|
|
728
|
-
return null;
|
|
729
|
-
}
|
|
730
|
-
}).filter((ref) => ref !== null);
|
|
731
|
-
const filter = resolvedEntityRefs.map((ref) => ({
|
|
732
|
-
kind: ref.kind,
|
|
733
|
-
"metadata.namespace": ref.namespace,
|
|
734
|
-
"metadata.name": ref.name
|
|
735
|
-
}));
|
|
736
|
-
const { token } = await this.auth.getPluginRequestToken({
|
|
737
|
-
onBehalfOf: await this.auth.getOwnServiceCredentials(),
|
|
738
|
-
targetPluginId: "catalog"
|
|
739
|
-
});
|
|
740
|
-
const entities = await this.catalogApi.getEntities({ filter }, { token }).then((r) => r.items);
|
|
741
|
-
if (entityRefs.length !== entities.length) {
|
|
742
|
-
const foundEntityNames = entities.map(catalogModel.stringifyEntityRef);
|
|
743
|
-
const missingEntityNames = resolvedEntityRefs.map(catalogModel.stringifyEntityRef).filter((s) => !foundEntityNames.includes(s));
|
|
744
|
-
logger?.debug(`Entities not found for refs ${missingEntityNames.join()}`);
|
|
745
|
-
}
|
|
746
|
-
const memberOf = entities.flatMap(
|
|
747
|
-
(e) => e.relations?.filter((r) => r.type === catalogModel.RELATION_MEMBER_OF).map((r) => r.targetRef) ?? []
|
|
748
|
-
);
|
|
749
|
-
const newEntityRefs = [
|
|
750
|
-
...new Set(resolvedEntityRefs.map(catalogModel.stringifyEntityRef).concat(memberOf))
|
|
751
|
-
];
|
|
752
|
-
logger?.debug(`Found catalog membership: ${newEntityRefs.join()}`);
|
|
753
|
-
return newEntityRefs;
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function getDefaultOwnershipEntityRefs(entity) {
|
|
758
|
-
const membershipRefs = entity.relations?.filter(
|
|
759
|
-
(r) => r.type === catalogModel.RELATION_MEMBER_OF && r.targetRef.startsWith("group:")
|
|
760
|
-
).map((r) => r.targetRef) ?? [];
|
|
761
|
-
return Array.from(/* @__PURE__ */ new Set([catalogModel.stringifyEntityRef(entity), ...membershipRefs]));
|
|
762
|
-
}
|
|
763
|
-
class CatalogAuthResolverContext {
|
|
764
|
-
constructor(logger, tokenIssuer, catalogIdentityClient, catalogApi, auth, ownershipResolver) {
|
|
765
|
-
this.logger = logger;
|
|
766
|
-
this.tokenIssuer = tokenIssuer;
|
|
767
|
-
this.catalogIdentityClient = catalogIdentityClient;
|
|
768
|
-
this.catalogApi = catalogApi;
|
|
769
|
-
this.auth = auth;
|
|
770
|
-
this.ownershipResolver = ownershipResolver;
|
|
771
|
-
}
|
|
772
|
-
static create(options) {
|
|
773
|
-
const catalogIdentityClient = new CatalogIdentityClient({
|
|
774
|
-
catalogApi: options.catalogApi,
|
|
775
|
-
tokenManager: options.tokenManager,
|
|
776
|
-
discovery: options.discovery,
|
|
777
|
-
auth: options.auth,
|
|
778
|
-
httpAuth: options.httpAuth
|
|
779
|
-
});
|
|
780
|
-
return new CatalogAuthResolverContext(
|
|
781
|
-
options.logger,
|
|
782
|
-
options.tokenIssuer,
|
|
783
|
-
catalogIdentityClient,
|
|
784
|
-
options.catalogApi,
|
|
785
|
-
options.auth,
|
|
786
|
-
options.ownershipResolver
|
|
787
|
-
);
|
|
788
|
-
}
|
|
789
|
-
async issueToken(params) {
|
|
790
|
-
const token = await this.tokenIssuer.issueToken(params);
|
|
791
|
-
return { token };
|
|
792
|
-
}
|
|
793
|
-
async findCatalogUser(query) {
|
|
794
|
-
let result = void 0;
|
|
795
|
-
const { token } = await this.auth.getPluginRequestToken({
|
|
796
|
-
onBehalfOf: await this.auth.getOwnServiceCredentials(),
|
|
797
|
-
targetPluginId: "catalog"
|
|
798
|
-
});
|
|
799
|
-
if ("entityRef" in query) {
|
|
800
|
-
const entityRef = catalogModel.parseEntityRef(query.entityRef, {
|
|
801
|
-
defaultKind: "User",
|
|
802
|
-
defaultNamespace: catalogModel.DEFAULT_NAMESPACE
|
|
803
|
-
});
|
|
804
|
-
result = await this.catalogApi.getEntityByRef(entityRef, { token });
|
|
805
|
-
} else if ("annotations" in query) {
|
|
806
|
-
const filter = {
|
|
807
|
-
kind: "user"
|
|
808
|
-
};
|
|
809
|
-
for (const [key, value] of Object.entries(query.annotations)) {
|
|
810
|
-
filter[`metadata.annotations.${key}`] = value;
|
|
811
|
-
}
|
|
812
|
-
const res = await this.catalogApi.getEntities({ filter }, { token });
|
|
813
|
-
result = res.items;
|
|
814
|
-
} else if ("filter" in query) {
|
|
815
|
-
const filter = [query.filter].flat().map((value) => {
|
|
816
|
-
if (!Object.keys(value).some(
|
|
817
|
-
(key) => key.toLocaleLowerCase("en-US") === "kind"
|
|
818
|
-
)) {
|
|
819
|
-
return {
|
|
820
|
-
...value,
|
|
821
|
-
kind: "user"
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
return value;
|
|
825
|
-
});
|
|
826
|
-
const res = await this.catalogApi.getEntities(
|
|
827
|
-
{ filter },
|
|
828
|
-
{ token }
|
|
829
|
-
);
|
|
830
|
-
result = res.items;
|
|
831
|
-
} else {
|
|
832
|
-
throw new errors.InputError("Invalid user lookup query");
|
|
833
|
-
}
|
|
834
|
-
if (Array.isArray(result)) {
|
|
835
|
-
if (result.length > 1) {
|
|
836
|
-
throw new errors.ConflictError("User lookup resulted in multiple matches");
|
|
837
|
-
}
|
|
838
|
-
result = result[0];
|
|
839
|
-
}
|
|
840
|
-
if (!result) {
|
|
841
|
-
throw new errors.NotFoundError("User not found");
|
|
842
|
-
}
|
|
843
|
-
return { entity: result };
|
|
844
|
-
}
|
|
845
|
-
async signInWithCatalogUser(query) {
|
|
846
|
-
const { entity } = await this.findCatalogUser(query);
|
|
847
|
-
let ent;
|
|
848
|
-
if (this.ownershipResolver) {
|
|
849
|
-
const { ownershipEntityRefs } = await this.ownershipResolver.resolveOwnershipEntityRefs(entity);
|
|
850
|
-
ent = ownershipEntityRefs;
|
|
851
|
-
} else {
|
|
852
|
-
ent = getDefaultOwnershipEntityRefs(entity);
|
|
853
|
-
}
|
|
854
|
-
const token = await this.tokenIssuer.issueToken({
|
|
855
|
-
claims: {
|
|
856
|
-
sub: catalogModel.stringifyEntityRef(entity),
|
|
857
|
-
ent
|
|
858
|
-
}
|
|
859
|
-
});
|
|
860
|
-
return { token };
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
function bindProviderRouters(targetRouter, options) {
|
|
865
|
-
const {
|
|
866
|
-
providers,
|
|
867
|
-
appUrl,
|
|
868
|
-
baseUrl,
|
|
869
|
-
config,
|
|
870
|
-
logger,
|
|
871
|
-
discovery,
|
|
872
|
-
auth,
|
|
873
|
-
httpAuth,
|
|
874
|
-
tokenManager,
|
|
875
|
-
tokenIssuer,
|
|
876
|
-
catalogApi,
|
|
877
|
-
ownershipResolver
|
|
878
|
-
} = options;
|
|
879
|
-
const providersConfig = config.getOptionalConfig("auth.providers");
|
|
880
|
-
const isOriginAllowed = createOriginFilter(config);
|
|
881
|
-
for (const [providerId, providerFactory] of Object.entries(providers)) {
|
|
882
|
-
if (providersConfig?.has(providerId)) {
|
|
883
|
-
logger.info(`Configuring auth provider: ${providerId}`);
|
|
884
|
-
try {
|
|
885
|
-
const provider = providerFactory({
|
|
886
|
-
providerId,
|
|
887
|
-
appUrl,
|
|
888
|
-
baseUrl,
|
|
889
|
-
isOriginAllowed,
|
|
890
|
-
globalConfig: {
|
|
891
|
-
baseUrl,
|
|
892
|
-
appUrl,
|
|
893
|
-
isOriginAllowed
|
|
894
|
-
},
|
|
895
|
-
config: providersConfig.getConfig(providerId),
|
|
896
|
-
logger,
|
|
897
|
-
resolverContext: CatalogAuthResolverContext.create({
|
|
898
|
-
logger,
|
|
899
|
-
catalogApi: catalogApi ?? new catalogClient.CatalogClient({ discoveryApi: discovery }),
|
|
900
|
-
tokenIssuer,
|
|
901
|
-
tokenManager,
|
|
902
|
-
discovery,
|
|
903
|
-
auth,
|
|
904
|
-
httpAuth,
|
|
905
|
-
ownershipResolver
|
|
906
|
-
})
|
|
907
|
-
});
|
|
908
|
-
const r = Router__default.default();
|
|
909
|
-
r.get("/start", provider.start.bind(provider));
|
|
910
|
-
r.get("/handler/frame", provider.frameHandler.bind(provider));
|
|
911
|
-
r.post("/handler/frame", provider.frameHandler.bind(provider));
|
|
912
|
-
if (provider.logout) {
|
|
913
|
-
r.post("/logout", provider.logout.bind(provider));
|
|
914
|
-
}
|
|
915
|
-
if (provider.refresh) {
|
|
916
|
-
r.get("/refresh", provider.refresh.bind(provider));
|
|
917
|
-
r.post("/refresh", provider.refresh.bind(provider));
|
|
918
|
-
}
|
|
919
|
-
targetRouter.use(`/${providerId}`, r);
|
|
920
|
-
} catch (e) {
|
|
921
|
-
errors.assertError(e);
|
|
922
|
-
if (process.env.NODE_ENV !== "development") {
|
|
923
|
-
throw new Error(
|
|
924
|
-
`Failed to initialize ${providerId} auth provider, ${e.message}`
|
|
925
|
-
);
|
|
926
|
-
}
|
|
927
|
-
logger.warn(`Skipping ${providerId} auth provider, ${e.message}`);
|
|
928
|
-
targetRouter.use(`/${providerId}`, () => {
|
|
929
|
-
throw new errors.NotFoundError(
|
|
930
|
-
`Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under auth.providers.${providerId} are missing or the environment variables used are not defined. Check the auth backend plugin logs when the backend starts to see more details.`
|
|
931
|
-
);
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
} else {
|
|
935
|
-
targetRouter.use(`/${providerId}`, () => {
|
|
936
|
-
throw new errors.NotFoundError(
|
|
937
|
-
`No auth provider registered for '${providerId}'`
|
|
938
|
-
);
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
function createOriginFilter(config) {
|
|
944
|
-
const appUrl = config.getString("app.baseUrl");
|
|
945
|
-
const { origin: appOrigin } = new URL(appUrl);
|
|
946
|
-
const allowedOrigins = config.getOptionalStringArray(
|
|
947
|
-
"auth.experimentalExtraAllowedOrigins"
|
|
948
|
-
);
|
|
949
|
-
const allowedOriginPatterns = allowedOrigins?.map(
|
|
950
|
-
(pattern) => new minimatch.Minimatch(pattern, { nocase: true, noglobstar: true })
|
|
951
|
-
) ?? [];
|
|
952
|
-
return (origin) => {
|
|
953
|
-
if (origin === appOrigin) {
|
|
954
|
-
return true;
|
|
955
|
-
}
|
|
956
|
-
return allowedOriginPatterns.some((pattern) => pattern.match(origin));
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
function bindOidcRouter(targetRouter, options) {
|
|
961
|
-
const { baseUrl, auth, tokenIssuer, userInfoDatabaseHandler } = options;
|
|
962
|
-
const router = Router__default.default();
|
|
963
|
-
targetRouter.use(router);
|
|
964
|
-
const config = {
|
|
965
|
-
issuer: baseUrl,
|
|
966
|
-
token_endpoint: `${baseUrl}/v1/token`,
|
|
967
|
-
userinfo_endpoint: `${baseUrl}/v1/userinfo`,
|
|
968
|
-
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
969
|
-
response_types_supported: ["id_token"],
|
|
970
|
-
subject_types_supported: ["public"],
|
|
971
|
-
id_token_signing_alg_values_supported: [
|
|
972
|
-
"RS256",
|
|
973
|
-
"RS384",
|
|
974
|
-
"RS512",
|
|
975
|
-
"ES256",
|
|
976
|
-
"ES384",
|
|
977
|
-
"ES512",
|
|
978
|
-
"PS256",
|
|
979
|
-
"PS384",
|
|
980
|
-
"PS512",
|
|
981
|
-
"EdDSA"
|
|
982
|
-
],
|
|
983
|
-
scopes_supported: ["openid"],
|
|
984
|
-
token_endpoint_auth_methods_supported: [],
|
|
985
|
-
claims_supported: ["sub", "ent"],
|
|
986
|
-
grant_types_supported: []
|
|
987
|
-
};
|
|
988
|
-
router.get("/.well-known/openid-configuration", (_req, res) => {
|
|
989
|
-
res.json(config);
|
|
990
|
-
});
|
|
991
|
-
router.get("/.well-known/jwks.json", async (_req, res) => {
|
|
992
|
-
const { keys } = await tokenIssuer.listPublicKeys();
|
|
993
|
-
res.json({ keys });
|
|
994
|
-
});
|
|
995
|
-
router.get("/v1/token", (_req, res) => {
|
|
996
|
-
res.status(501).send("Not Implemented");
|
|
997
|
-
});
|
|
998
|
-
router.get("/v1/userinfo", async (req, res) => {
|
|
999
|
-
const matches = req.headers.authorization?.match(/^Bearer[ ]+(\S+)$/i);
|
|
1000
|
-
const token = matches?.[1];
|
|
1001
|
-
if (!token) {
|
|
1002
|
-
throw new errors.AuthenticationError("No token provided");
|
|
1003
|
-
}
|
|
1004
|
-
const credentials = await auth.authenticate(token, {
|
|
1005
|
-
allowLimitedAccess: true
|
|
1006
|
-
});
|
|
1007
|
-
if (!auth.isPrincipal(credentials, "user")) {
|
|
1008
|
-
throw new errors.InputError(
|
|
1009
|
-
"Userinfo endpoint must be called with a token that represents a user principal"
|
|
1010
|
-
);
|
|
1011
|
-
}
|
|
1012
|
-
const { sub: userEntityRef } = jose.decodeJwt(token);
|
|
1013
|
-
if (typeof userEntityRef !== "string") {
|
|
1014
|
-
throw new Error("Invalid user token, user entity ref must be a string");
|
|
1015
|
-
}
|
|
1016
|
-
const userInfo = await userInfoDatabaseHandler.getUserInfo(userEntityRef);
|
|
1017
|
-
if (!userInfo) {
|
|
1018
|
-
res.status(404).send("User info not found");
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
res.json(userInfo);
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
const MS_IN_S$1 = 1e3;
|
|
1026
|
-
const MAX_TOKEN_LENGTH = 32768;
|
|
1027
|
-
class TokenFactory {
|
|
1028
|
-
issuer;
|
|
1029
|
-
logger;
|
|
1030
|
-
keyStore;
|
|
1031
|
-
keyDurationSeconds;
|
|
1032
|
-
algorithm;
|
|
1033
|
-
userInfoDatabaseHandler;
|
|
1034
|
-
keyExpiry;
|
|
1035
|
-
privateKeyPromise;
|
|
1036
|
-
constructor(options) {
|
|
1037
|
-
this.issuer = options.issuer;
|
|
1038
|
-
this.logger = options.logger;
|
|
1039
|
-
this.keyStore = options.keyStore;
|
|
1040
|
-
this.keyDurationSeconds = options.keyDurationSeconds;
|
|
1041
|
-
this.algorithm = options.algorithm ?? "ES256";
|
|
1042
|
-
this.userInfoDatabaseHandler = options.userInfoDatabaseHandler;
|
|
1043
|
-
}
|
|
1044
|
-
async issueToken(params) {
|
|
1045
|
-
const key = await this.getKey();
|
|
1046
|
-
const iss = this.issuer;
|
|
1047
|
-
const { sub, ent = [sub], ...additionalClaims } = params.claims;
|
|
1048
|
-
const aud = pluginAuthNode.tokenTypes.user.audClaim;
|
|
1049
|
-
const iat = Math.floor(Date.now() / MS_IN_S$1);
|
|
1050
|
-
const exp = iat + this.keyDurationSeconds;
|
|
1051
|
-
try {
|
|
1052
|
-
catalogModel.parseEntityRef(sub);
|
|
1053
|
-
} catch (error) {
|
|
1054
|
-
throw new Error(
|
|
1055
|
-
'"sub" claim provided by the auth resolver is not a valid EntityRef.'
|
|
1056
|
-
);
|
|
1057
|
-
}
|
|
1058
|
-
if (!key.alg) {
|
|
1059
|
-
throw new errors.AuthenticationError("No algorithm was provided in the key");
|
|
1060
|
-
}
|
|
1061
|
-
this.logger.info(`Issuing token for ${sub}, with entities ${ent}`);
|
|
1062
|
-
const signingKey = await jose.importJWK(key);
|
|
1063
|
-
const uip = await this.createUserIdentityClaim({
|
|
1064
|
-
header: {
|
|
1065
|
-
typ: pluginAuthNode.tokenTypes.limitedUser.typParam,
|
|
1066
|
-
alg: key.alg,
|
|
1067
|
-
kid: key.kid
|
|
1068
|
-
},
|
|
1069
|
-
payload: { sub, iat, exp },
|
|
1070
|
-
key: signingKey
|
|
1071
|
-
});
|
|
1072
|
-
const claims = {
|
|
1073
|
-
...additionalClaims,
|
|
1074
|
-
iss,
|
|
1075
|
-
sub,
|
|
1076
|
-
ent,
|
|
1077
|
-
aud,
|
|
1078
|
-
iat,
|
|
1079
|
-
exp,
|
|
1080
|
-
uip
|
|
1081
|
-
};
|
|
1082
|
-
const token = await new jose.SignJWT(claims).setProtectedHeader({
|
|
1083
|
-
typ: pluginAuthNode.tokenTypes.user.typParam,
|
|
1084
|
-
alg: key.alg,
|
|
1085
|
-
kid: key.kid
|
|
1086
|
-
}).sign(signingKey);
|
|
1087
|
-
if (token.length > MAX_TOKEN_LENGTH) {
|
|
1088
|
-
throw new Error(
|
|
1089
|
-
`Failed to issue a new user token. The resulting token is excessively large, with either too many ownership claims or too large custom claims. You likely have a bug either in the sign-in resolver or catalog data. The following claims were requested: '${JSON.stringify(
|
|
1090
|
-
claims
|
|
1091
|
-
)}'`
|
|
1092
|
-
);
|
|
1093
|
-
}
|
|
1094
|
-
await this.userInfoDatabaseHandler.addUserInfo({
|
|
1095
|
-
claims: lodash.omit(claims, ["aud", "iat", "iss", "uip"])
|
|
1096
|
-
});
|
|
1097
|
-
return token;
|
|
1098
|
-
}
|
|
1099
|
-
// This will be called by other services that want to verify ID tokens.
|
|
1100
|
-
// It is important that it returns a list of all public keys that could
|
|
1101
|
-
// have been used to sign tokens that have not yet expired.
|
|
1102
|
-
async listPublicKeys() {
|
|
1103
|
-
const { items: keys } = await this.keyStore.listKeys();
|
|
1104
|
-
const validKeys = [];
|
|
1105
|
-
const expiredKeys = [];
|
|
1106
|
-
for (const key of keys) {
|
|
1107
|
-
const expireAt = luxon.DateTime.fromJSDate(key.createdAt).plus({
|
|
1108
|
-
seconds: 3 * this.keyDurationSeconds
|
|
1109
|
-
});
|
|
1110
|
-
if (expireAt < luxon.DateTime.local()) {
|
|
1111
|
-
expiredKeys.push(key);
|
|
1112
|
-
} else {
|
|
1113
|
-
validKeys.push(key);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
if (expiredKeys.length > 0) {
|
|
1117
|
-
const kids = expiredKeys.map(({ key }) => key.kid);
|
|
1118
|
-
this.logger.info(`Removing expired signing keys, '${kids.join("', '")}'`);
|
|
1119
|
-
this.keyStore.removeKeys(kids).catch((error) => {
|
|
1120
|
-
this.logger.error(`Failed to remove expired keys, ${error}`);
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
return { keys: validKeys.map(({ key }) => key) };
|
|
1124
|
-
}
|
|
1125
|
-
async getKey() {
|
|
1126
|
-
if (this.privateKeyPromise) {
|
|
1127
|
-
if (this.keyExpiry && luxon.DateTime.fromJSDate(this.keyExpiry) > luxon.DateTime.local()) {
|
|
1128
|
-
return this.privateKeyPromise;
|
|
1129
|
-
}
|
|
1130
|
-
this.logger.info(`Signing key has expired, generating new key`);
|
|
1131
|
-
delete this.privateKeyPromise;
|
|
1132
|
-
}
|
|
1133
|
-
this.keyExpiry = luxon.DateTime.utc().plus({
|
|
1134
|
-
seconds: this.keyDurationSeconds
|
|
1135
|
-
}).toJSDate();
|
|
1136
|
-
const promise = (async () => {
|
|
1137
|
-
const key = await jose.generateKeyPair(this.algorithm);
|
|
1138
|
-
const publicKey = await jose.exportJWK(key.publicKey);
|
|
1139
|
-
const privateKey = await jose.exportJWK(key.privateKey);
|
|
1140
|
-
publicKey.kid = privateKey.kid = uuid.v4();
|
|
1141
|
-
publicKey.alg = privateKey.alg = this.algorithm;
|
|
1142
|
-
this.logger.info(`Created new signing key ${publicKey.kid}`);
|
|
1143
|
-
await this.keyStore.addKey(publicKey);
|
|
1144
|
-
return privateKey;
|
|
1145
|
-
})();
|
|
1146
|
-
this.privateKeyPromise = promise;
|
|
1147
|
-
try {
|
|
1148
|
-
await promise;
|
|
1149
|
-
} catch (error) {
|
|
1150
|
-
this.logger.error(`Failed to generate new signing key, ${error}`);
|
|
1151
|
-
delete this.keyExpiry;
|
|
1152
|
-
delete this.privateKeyPromise;
|
|
1153
|
-
}
|
|
1154
|
-
return promise;
|
|
1155
|
-
}
|
|
1156
|
-
// Creates a string claim that can be used as part of reconstructing a limited
|
|
1157
|
-
// user token. The output of this function is only the signature part of a
|
|
1158
|
-
// JWS.
|
|
1159
|
-
async createUserIdentityClaim(options) {
|
|
1160
|
-
const header = {
|
|
1161
|
-
typ: options.header.typ,
|
|
1162
|
-
alg: options.header.alg,
|
|
1163
|
-
...options.header.kid ? { kid: options.header.kid } : {}
|
|
1164
|
-
};
|
|
1165
|
-
const payload = {
|
|
1166
|
-
sub: options.payload.sub,
|
|
1167
|
-
iat: options.payload.iat,
|
|
1168
|
-
exp: options.payload.exp
|
|
1169
|
-
};
|
|
1170
|
-
const jws = await new jose.GeneralSign(
|
|
1171
|
-
new TextEncoder().encode(JSON.stringify(payload))
|
|
1172
|
-
).addSignature(options.key).setProtectedHeader(header).done().sign();
|
|
1173
|
-
return jws.signatures[0].signature;
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
const TABLE$1 = "signing_keys";
|
|
1178
|
-
const parseDate = (date) => {
|
|
1179
|
-
const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
|
|
1180
|
-
if (!parsedDate.isValid) {
|
|
1181
|
-
throw new Error(
|
|
1182
|
-
`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
return parsedDate.toJSDate();
|
|
1186
|
-
};
|
|
1187
|
-
class DatabaseKeyStore {
|
|
1188
|
-
constructor(client) {
|
|
1189
|
-
this.client = client;
|
|
1190
|
-
}
|
|
1191
|
-
async addKey(key) {
|
|
1192
|
-
await this.client(TABLE$1).insert({
|
|
1193
|
-
kid: key.kid,
|
|
1194
|
-
key: JSON.stringify(key)
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
async listKeys() {
|
|
1198
|
-
const rows = await this.client(TABLE$1).select();
|
|
1199
|
-
return {
|
|
1200
|
-
items: rows.map((row) => ({
|
|
1201
|
-
key: JSON.parse(row.key),
|
|
1202
|
-
createdAt: parseDate(row.created_at)
|
|
1203
|
-
}))
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
async removeKeys(kids) {
|
|
1207
|
-
await this.client(TABLE$1).delete().whereIn("kid", kids);
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
class MemoryKeyStore {
|
|
1212
|
-
keys = /* @__PURE__ */ new Map();
|
|
1213
|
-
async addKey(key) {
|
|
1214
|
-
this.keys.set(key.kid, {
|
|
1215
|
-
createdAt: luxon.DateTime.utc().toJSDate(),
|
|
1216
|
-
key: JSON.stringify(key)
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
async removeKeys(kids) {
|
|
1220
|
-
for (const kid of kids) {
|
|
1221
|
-
this.keys.delete(kid);
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
async listKeys() {
|
|
1225
|
-
return {
|
|
1226
|
-
items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
|
|
1227
|
-
createdAt,
|
|
1228
|
-
key: JSON.parse(keyStr)
|
|
1229
|
-
}))
|
|
1230
|
-
};
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
1235
|
-
const DEFAULT_DOCUMENT_PATH = "sessions";
|
|
1236
|
-
class FirestoreKeyStore {
|
|
1237
|
-
constructor(database, path, timeout) {
|
|
1238
|
-
this.database = database;
|
|
1239
|
-
this.path = path;
|
|
1240
|
-
this.timeout = timeout;
|
|
1241
|
-
}
|
|
1242
|
-
static async create(settings) {
|
|
1243
|
-
const { path, timeout, ...firestoreSettings } = settings ?? {};
|
|
1244
|
-
const database = new firestore.Firestore(firestoreSettings);
|
|
1245
|
-
return new FirestoreKeyStore(
|
|
1246
|
-
database,
|
|
1247
|
-
path ?? DEFAULT_DOCUMENT_PATH,
|
|
1248
|
-
timeout ?? DEFAULT_TIMEOUT_MS
|
|
1249
|
-
);
|
|
1250
|
-
}
|
|
1251
|
-
static async verifyConnection(keyStore, logger) {
|
|
1252
|
-
try {
|
|
1253
|
-
await keyStore.verify();
|
|
1254
|
-
} catch (error) {
|
|
1255
|
-
if (process.env.NODE_ENV !== "development") {
|
|
1256
|
-
throw new Error(
|
|
1257
|
-
`Failed to connect to database: ${error.message}`
|
|
1258
|
-
);
|
|
1259
|
-
}
|
|
1260
|
-
logger?.warn(
|
|
1261
|
-
`Failed to connect to database: ${error.message}`
|
|
1262
|
-
);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
async addKey(key) {
|
|
1266
|
-
await this.withTimeout(
|
|
1267
|
-
this.database.collection(this.path).doc(key.kid).set({
|
|
1268
|
-
kid: key.kid,
|
|
1269
|
-
key: JSON.stringify(key)
|
|
1270
|
-
})
|
|
1271
|
-
);
|
|
1272
|
-
}
|
|
1273
|
-
async listKeys() {
|
|
1274
|
-
const keys = await this.withTimeout(
|
|
1275
|
-
this.database.collection(this.path).get()
|
|
1276
|
-
);
|
|
1277
|
-
return {
|
|
1278
|
-
items: keys.docs.map((key) => ({
|
|
1279
|
-
key: key.data(),
|
|
1280
|
-
createdAt: key.createTime.toDate()
|
|
1281
|
-
}))
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
|
-
async removeKeys(kids) {
|
|
1285
|
-
for (const kid of kids) {
|
|
1286
|
-
await this.withTimeout(
|
|
1287
|
-
this.database.collection(this.path).doc(kid).delete()
|
|
1288
|
-
);
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
/**
|
|
1292
|
-
* Helper function to allow us to modify the timeout used when
|
|
1293
|
-
* performing Firestore database operations.
|
|
1294
|
-
*
|
|
1295
|
-
* The reason for this is that it seems that there's no other
|
|
1296
|
-
* practical solution to change the default timeout of 10mins
|
|
1297
|
-
* that Firestore has.
|
|
1298
|
-
*
|
|
1299
|
-
*/
|
|
1300
|
-
async withTimeout(operation) {
|
|
1301
|
-
const timer = new Promise(
|
|
1302
|
-
(_, reject) => setTimeout(() => {
|
|
1303
|
-
reject(new Error(`Operation timed out after ${this.timeout}ms`));
|
|
1304
|
-
}, this.timeout)
|
|
1305
|
-
);
|
|
1306
|
-
return Promise.race([operation, timer]);
|
|
1307
|
-
}
|
|
1308
|
-
/**
|
|
1309
|
-
* Used to verify that the database is reachable.
|
|
1310
|
-
*/
|
|
1311
|
-
async verify() {
|
|
1312
|
-
await this.withTimeout(this.database.collection(this.path).limit(1).get());
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
const DEFAULT_ALGORITHM = "ES256";
|
|
1317
|
-
class StaticKeyStore {
|
|
1318
|
-
keyPairs;
|
|
1319
|
-
createdAt;
|
|
1320
|
-
constructor(keyPairs) {
|
|
1321
|
-
if (keyPairs.length === 0) {
|
|
1322
|
-
throw new Error("Should provide at least one key pair");
|
|
1323
|
-
}
|
|
1324
|
-
this.keyPairs = keyPairs;
|
|
1325
|
-
this.createdAt = /* @__PURE__ */ new Date();
|
|
1326
|
-
}
|
|
1327
|
-
static async fromConfig(config) {
|
|
1328
|
-
const keyConfigs = config.getConfigArray("auth.keyStore.static.keys").map((c) => {
|
|
1329
|
-
const staticKeyConfig = {
|
|
1330
|
-
publicKeyFile: c.getString("publicKeyFile"),
|
|
1331
|
-
privateKeyFile: c.getString("privateKeyFile"),
|
|
1332
|
-
keyId: c.getString("keyId"),
|
|
1333
|
-
algorithm: c.getOptionalString("algorithm") ?? DEFAULT_ALGORITHM
|
|
1334
|
-
};
|
|
1335
|
-
return staticKeyConfig;
|
|
1336
|
-
});
|
|
1337
|
-
const keyPairs = await Promise.all(
|
|
1338
|
-
keyConfigs.map(async (k) => await this.loadKeyPair(k))
|
|
1339
|
-
);
|
|
1340
|
-
return new StaticKeyStore(keyPairs);
|
|
1341
|
-
}
|
|
1342
|
-
addKey(_key) {
|
|
1343
|
-
throw new Error("Cannot add keys to the static key store");
|
|
1344
|
-
}
|
|
1345
|
-
listKeys() {
|
|
1346
|
-
const keys = this.keyPairs.map((k) => this.keyPairToStoredKey(k));
|
|
1347
|
-
return Promise.resolve({ items: keys });
|
|
1348
|
-
}
|
|
1349
|
-
getPrivateKey(keyId) {
|
|
1350
|
-
const keyPair = this.keyPairs.find((k) => k.publicKey.kid === keyId);
|
|
1351
|
-
if (keyPair === void 0) {
|
|
1352
|
-
throw new Error(`Could not find key with keyId: ${keyId}`);
|
|
1353
|
-
}
|
|
1354
|
-
return keyPair.privateKey;
|
|
1355
|
-
}
|
|
1356
|
-
removeKeys(_kids) {
|
|
1357
|
-
throw new Error("Cannot remove keys from the static key store");
|
|
1358
|
-
}
|
|
1359
|
-
keyPairToStoredKey(keyPair) {
|
|
1360
|
-
const publicKey = {
|
|
1361
|
-
...keyPair.publicKey,
|
|
1362
|
-
use: "sig"
|
|
1363
|
-
};
|
|
1364
|
-
return {
|
|
1365
|
-
key: publicKey,
|
|
1366
|
-
createdAt: this.createdAt
|
|
1367
|
-
};
|
|
1368
|
-
}
|
|
1369
|
-
static async loadKeyPair(options) {
|
|
1370
|
-
const algorithm = options.algorithm;
|
|
1371
|
-
const keyId = options.keyId;
|
|
1372
|
-
const publicKey = await this.loadPublicKeyFromFile(
|
|
1373
|
-
options.publicKeyFile,
|
|
1374
|
-
keyId,
|
|
1375
|
-
algorithm
|
|
1376
|
-
);
|
|
1377
|
-
const privateKey = await this.loadPrivateKeyFromFile(
|
|
1378
|
-
options.privateKeyFile,
|
|
1379
|
-
keyId,
|
|
1380
|
-
algorithm
|
|
1381
|
-
);
|
|
1382
|
-
return { publicKey, privateKey };
|
|
1383
|
-
}
|
|
1384
|
-
static async loadPublicKeyFromFile(path, keyId, algorithm) {
|
|
1385
|
-
return this.loadKeyFromFile(path, keyId, algorithm, jose.importSPKI);
|
|
1386
|
-
}
|
|
1387
|
-
static async loadPrivateKeyFromFile(path, keyId, algorithm) {
|
|
1388
|
-
return this.loadKeyFromFile(path, keyId, algorithm, jose.importPKCS8);
|
|
1389
|
-
}
|
|
1390
|
-
static async loadKeyFromFile(path, keyId, algorithm, importer) {
|
|
1391
|
-
const content = await fs.promises.readFile(path, { encoding: "utf8", flag: "r" });
|
|
1392
|
-
const key = await importer(content, algorithm);
|
|
1393
|
-
const jwk = await jose.exportJWK(key);
|
|
1394
|
-
jwk.kid = keyId;
|
|
1395
|
-
jwk.alg = algorithm;
|
|
1396
|
-
return jwk;
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
class KeyStores {
|
|
1401
|
-
/**
|
|
1402
|
-
* Looks at the `auth.keyStore` section in the application configuration
|
|
1403
|
-
* and returns a KeyStore store. Defaults to `database`
|
|
1404
|
-
*
|
|
1405
|
-
* @returns a KeyStore store
|
|
1406
|
-
*/
|
|
1407
|
-
static async fromConfig(config, options) {
|
|
1408
|
-
const { logger, database } = options;
|
|
1409
|
-
const ks = config.getOptionalConfig("auth.keyStore");
|
|
1410
|
-
const provider = ks?.getOptionalString("provider") ?? "database";
|
|
1411
|
-
logger.info(`Configuring "${provider}" as KeyStore provider`);
|
|
1412
|
-
if (provider === "database") {
|
|
1413
|
-
return new DatabaseKeyStore(await database.get());
|
|
1414
|
-
}
|
|
1415
|
-
if (provider === "memory") {
|
|
1416
|
-
return new MemoryKeyStore();
|
|
1417
|
-
}
|
|
1418
|
-
if (provider === "firestore") {
|
|
1419
|
-
const settings = ks?.getConfig(provider);
|
|
1420
|
-
const keyStore = await FirestoreKeyStore.create(
|
|
1421
|
-
lodash.pickBy(
|
|
1422
|
-
{
|
|
1423
|
-
projectId: settings?.getOptionalString("projectId"),
|
|
1424
|
-
keyFilename: settings?.getOptionalString("keyFilename"),
|
|
1425
|
-
host: settings?.getOptionalString("host"),
|
|
1426
|
-
port: settings?.getOptionalNumber("port"),
|
|
1427
|
-
ssl: settings?.getOptionalBoolean("ssl"),
|
|
1428
|
-
path: settings?.getOptionalString("path"),
|
|
1429
|
-
timeout: settings?.getOptionalNumber("timeout")
|
|
1430
|
-
},
|
|
1431
|
-
(value) => value !== void 0
|
|
1432
|
-
)
|
|
1433
|
-
);
|
|
1434
|
-
await FirestoreKeyStore.verifyConnection(keyStore, logger);
|
|
1435
|
-
return keyStore;
|
|
1436
|
-
}
|
|
1437
|
-
if (provider === "static") {
|
|
1438
|
-
return await StaticKeyStore.fromConfig(config);
|
|
1439
|
-
}
|
|
1440
|
-
throw new Error(`Unknown KeyStore provider: ${provider}`);
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
const TABLE = "user_info";
|
|
1445
|
-
class UserInfoDatabaseHandler {
|
|
1446
|
-
constructor(client) {
|
|
1447
|
-
this.client = client;
|
|
1448
|
-
}
|
|
1449
|
-
async addUserInfo(userInfo) {
|
|
1450
|
-
await this.client(TABLE).insert({
|
|
1451
|
-
user_entity_ref: userInfo.claims.sub,
|
|
1452
|
-
user_info: JSON.stringify(userInfo),
|
|
1453
|
-
exp: luxon.DateTime.fromSeconds(userInfo.claims.exp, {
|
|
1454
|
-
zone: "utc"
|
|
1455
|
-
}).toSQL({ includeOffset: false })
|
|
1456
|
-
}).onConflict("user_entity_ref").merge();
|
|
1457
|
-
}
|
|
1458
|
-
async getUserInfo(userEntityRef) {
|
|
1459
|
-
const info = await this.client(TABLE).where({ user_entity_ref: userEntityRef }).first();
|
|
1460
|
-
if (!info) {
|
|
1461
|
-
return void 0;
|
|
1462
|
-
}
|
|
1463
|
-
const userInfo = JSON.parse(info.user_info);
|
|
1464
|
-
return userInfo;
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
const migrationsDir = backendPluginApi.resolvePackagePath(
|
|
1469
|
-
"@backstage/plugin-auth-backend",
|
|
1470
|
-
"migrations"
|
|
1471
|
-
);
|
|
1472
|
-
class AuthDatabase {
|
|
1473
|
-
#database;
|
|
1474
|
-
#promise;
|
|
1475
|
-
static create(database) {
|
|
1476
|
-
return new AuthDatabase(database);
|
|
1477
|
-
}
|
|
1478
|
-
/** @internal */
|
|
1479
|
-
static forTesting() {
|
|
1480
|
-
const config$1 = new config.ConfigReader({
|
|
1481
|
-
backend: {
|
|
1482
|
-
database: {
|
|
1483
|
-
client: "better-sqlite3",
|
|
1484
|
-
connection: ":memory:",
|
|
1485
|
-
useNullAsDefault: true
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
});
|
|
1489
|
-
const database = backendCommon.DatabaseManager.fromConfig(config$1).forPlugin("auth");
|
|
1490
|
-
return new AuthDatabase(database);
|
|
1491
|
-
}
|
|
1492
|
-
static async runMigrations(knex) {
|
|
1493
|
-
await knex.migrate.latest({
|
|
1494
|
-
directory: migrationsDir
|
|
1495
|
-
});
|
|
1496
|
-
}
|
|
1497
|
-
constructor(database) {
|
|
1498
|
-
this.#database = database;
|
|
1499
|
-
}
|
|
1500
|
-
get() {
|
|
1501
|
-
this.#promise ??= this.#database.getClient().then(async (client) => {
|
|
1502
|
-
if (!this.#database.migrations?.skip) {
|
|
1503
|
-
await AuthDatabase.runMigrations(client);
|
|
1504
|
-
}
|
|
1505
|
-
return client;
|
|
1506
|
-
});
|
|
1507
|
-
return this.#promise;
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
const TOKEN_EXP_DEFAULT_S = 3600;
|
|
1512
|
-
const TOKEN_EXP_MIN_S = 600;
|
|
1513
|
-
const TOKEN_EXP_MAX_S = 86400;
|
|
1514
|
-
function readBackstageTokenExpiration(config$1) {
|
|
1515
|
-
const processingIntervalKey = "auth.backstageTokenExpiration";
|
|
1516
|
-
if (!config$1.has(processingIntervalKey)) {
|
|
1517
|
-
return TOKEN_EXP_DEFAULT_S;
|
|
1518
|
-
}
|
|
1519
|
-
const duration = config.readDurationFromConfig(config$1, {
|
|
1520
|
-
key: processingIntervalKey
|
|
1521
|
-
});
|
|
1522
|
-
const durationS = Math.round(types.durationToMilliseconds(duration) / 1e3);
|
|
1523
|
-
if (durationS < TOKEN_EXP_MIN_S) {
|
|
1524
|
-
return TOKEN_EXP_MIN_S;
|
|
1525
|
-
} else if (durationS > TOKEN_EXP_MAX_S) {
|
|
1526
|
-
return TOKEN_EXP_MAX_S;
|
|
1527
|
-
}
|
|
1528
|
-
return durationS;
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
const MS_IN_S = 1e3;
|
|
1532
|
-
class StaticTokenIssuer {
|
|
1533
|
-
issuer;
|
|
1534
|
-
logger;
|
|
1535
|
-
keyStore;
|
|
1536
|
-
sessionExpirationSeconds;
|
|
1537
|
-
constructor(options, keyStore) {
|
|
1538
|
-
this.issuer = options.issuer;
|
|
1539
|
-
this.logger = options.logger;
|
|
1540
|
-
this.sessionExpirationSeconds = options.sessionExpirationSeconds;
|
|
1541
|
-
this.keyStore = keyStore;
|
|
1542
|
-
}
|
|
1543
|
-
async issueToken(params) {
|
|
1544
|
-
const key = await this.getSigningKey();
|
|
1545
|
-
const iss = this.issuer;
|
|
1546
|
-
const { sub, ent, ...additionalClaims } = params.claims;
|
|
1547
|
-
const aud = "backstage";
|
|
1548
|
-
const iat = Math.floor(Date.now() / MS_IN_S);
|
|
1549
|
-
const exp = iat + this.sessionExpirationSeconds;
|
|
1550
|
-
try {
|
|
1551
|
-
catalogModel.parseEntityRef(sub);
|
|
1552
|
-
} catch (error) {
|
|
1553
|
-
throw new Error(
|
|
1554
|
-
'"sub" claim provided by the auth resolver is not a valid EntityRef.'
|
|
1555
|
-
);
|
|
1556
|
-
}
|
|
1557
|
-
this.logger.info(`Issuing token for ${sub}, with entities ${ent ?? []}`);
|
|
1558
|
-
if (!key.alg) {
|
|
1559
|
-
throw new errors.AuthenticationError("No algorithm was provided in the key");
|
|
1560
|
-
}
|
|
1561
|
-
return new jose.SignJWT({ ...additionalClaims, iss, sub, ent, aud, iat, exp }).setProtectedHeader({ alg: key.alg, kid: key.kid }).setIssuer(iss).setAudience(aud).setSubject(sub).setIssuedAt(iat).setExpirationTime(exp).sign(await jose.importJWK(key));
|
|
1562
|
-
}
|
|
1563
|
-
async getSigningKey() {
|
|
1564
|
-
const { items: keys } = await this.keyStore.listKeys();
|
|
1565
|
-
if (keys.length >= 1) {
|
|
1566
|
-
return this.keyStore.getPrivateKey(keys[0].key.kid);
|
|
1567
|
-
}
|
|
1568
|
-
throw new Error("Keystore should hold at least 1 key");
|
|
1569
|
-
}
|
|
1570
|
-
async listPublicKeys() {
|
|
1571
|
-
const { items: keys } = await this.keyStore.listKeys();
|
|
1572
|
-
return { keys: keys.map(({ key }) => key) };
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
async function createRouter(options) {
|
|
1577
|
-
const {
|
|
1578
|
-
logger,
|
|
1579
|
-
config,
|
|
1580
|
-
discovery,
|
|
1581
|
-
database,
|
|
1582
|
-
tokenFactoryAlgorithm,
|
|
1583
|
-
providerFactories = {}
|
|
1584
|
-
} = options;
|
|
1585
|
-
const { auth, httpAuth } = backendCommon.createLegacyAuthAdapters(options);
|
|
1586
|
-
const router = Router__default.default();
|
|
1587
|
-
const appUrl = config.getString("app.baseUrl");
|
|
1588
|
-
const authUrl = await discovery.getExternalBaseUrl("auth");
|
|
1589
|
-
const backstageTokenExpiration = readBackstageTokenExpiration(config);
|
|
1590
|
-
const authDb = AuthDatabase.create(database);
|
|
1591
|
-
const keyStore = await KeyStores.fromConfig(config, {
|
|
1592
|
-
logger,
|
|
1593
|
-
database: authDb
|
|
1594
|
-
});
|
|
1595
|
-
const userInfoDatabaseHandler = new UserInfoDatabaseHandler(
|
|
1596
|
-
await authDb.get()
|
|
1597
|
-
);
|
|
1598
|
-
let tokenIssuer;
|
|
1599
|
-
if (keyStore instanceof StaticKeyStore) {
|
|
1600
|
-
tokenIssuer = new StaticTokenIssuer(
|
|
1601
|
-
{
|
|
1602
|
-
logger: logger.child({ component: "token-factory" }),
|
|
1603
|
-
issuer: authUrl,
|
|
1604
|
-
sessionExpirationSeconds: backstageTokenExpiration
|
|
1605
|
-
},
|
|
1606
|
-
keyStore
|
|
1607
|
-
);
|
|
1608
|
-
} else {
|
|
1609
|
-
tokenIssuer = new TokenFactory({
|
|
1610
|
-
issuer: authUrl,
|
|
1611
|
-
keyStore,
|
|
1612
|
-
keyDurationSeconds: backstageTokenExpiration,
|
|
1613
|
-
logger: logger.child({ component: "token-factory" }),
|
|
1614
|
-
algorithm: tokenFactoryAlgorithm ?? config.getOptionalString("auth.identityTokenAlgorithm"),
|
|
1615
|
-
userInfoDatabaseHandler
|
|
1616
|
-
});
|
|
1617
|
-
}
|
|
1618
|
-
const secret = config.getOptionalString("auth.session.secret");
|
|
1619
|
-
if (secret) {
|
|
1620
|
-
router.use(cookieParser__default.default(secret));
|
|
1621
|
-
const enforceCookieSSL = authUrl.startsWith("https");
|
|
1622
|
-
const KnexSessionStore = connectSessionKnex__default.default(session__default.default);
|
|
1623
|
-
router.use(
|
|
1624
|
-
session__default.default({
|
|
1625
|
-
secret,
|
|
1626
|
-
saveUninitialized: false,
|
|
1627
|
-
resave: false,
|
|
1628
|
-
cookie: { secure: enforceCookieSSL ? "auto" : false },
|
|
1629
|
-
store: new KnexSessionStore({
|
|
1630
|
-
createtable: false,
|
|
1631
|
-
knex: await authDb.get()
|
|
1632
|
-
})
|
|
1633
|
-
})
|
|
1634
|
-
);
|
|
1635
|
-
router.use(passport__default.default.initialize());
|
|
1636
|
-
router.use(passport__default.default.session());
|
|
1637
|
-
} else {
|
|
1638
|
-
router.use(cookieParser__default.default());
|
|
1639
|
-
}
|
|
1640
|
-
router.use(express__default.default.urlencoded({ extended: false }));
|
|
1641
|
-
router.use(express__default.default.json());
|
|
1642
|
-
const providers = options.disableDefaultProviderFactories ? providerFactories : {
|
|
1643
|
-
...defaultAuthProviderFactories,
|
|
1644
|
-
...providerFactories
|
|
1645
|
-
};
|
|
1646
|
-
bindProviderRouters(router, {
|
|
1647
|
-
providers,
|
|
1648
|
-
appUrl,
|
|
1649
|
-
baseUrl: authUrl,
|
|
1650
|
-
tokenIssuer,
|
|
1651
|
-
...options,
|
|
1652
|
-
auth,
|
|
1653
|
-
httpAuth
|
|
1654
|
-
});
|
|
1655
|
-
bindOidcRouter(router, {
|
|
1656
|
-
auth,
|
|
1657
|
-
tokenIssuer,
|
|
1658
|
-
baseUrl: authUrl,
|
|
1659
|
-
userInfoDatabaseHandler
|
|
1660
|
-
});
|
|
1661
|
-
router.use("/:provider/", (req) => {
|
|
1662
|
-
const { provider } = req.params;
|
|
1663
|
-
throw new errors.NotFoundError(`Unknown auth provider '${provider}'`);
|
|
1664
|
-
});
|
|
1665
|
-
return router;
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
const authPlugin = backendPluginApi.createBackendPlugin({
|
|
1669
|
-
pluginId: "auth",
|
|
1670
|
-
register(reg) {
|
|
1671
|
-
const providers = /* @__PURE__ */ new Map();
|
|
1672
|
-
let ownershipResolver = void 0;
|
|
1673
|
-
reg.registerExtensionPoint(pluginAuthNode.authProvidersExtensionPoint, {
|
|
1674
|
-
registerProvider({ providerId, factory }) {
|
|
1675
|
-
if (providers.has(providerId)) {
|
|
1676
|
-
throw new Error(
|
|
1677
|
-
`Auth provider '${providerId}' was already registered`
|
|
1678
|
-
);
|
|
1679
|
-
}
|
|
1680
|
-
providers.set(providerId, factory);
|
|
1681
|
-
}
|
|
1682
|
-
});
|
|
1683
|
-
reg.registerExtensionPoint(pluginAuthNode.authOwnershipResolutionExtensionPoint, {
|
|
1684
|
-
setAuthOwnershipResolver(resolver) {
|
|
1685
|
-
if (ownershipResolver) {
|
|
1686
|
-
throw new Error("Auth ownership resolver is already set");
|
|
1687
|
-
}
|
|
1688
|
-
ownershipResolver = resolver;
|
|
1689
|
-
}
|
|
1690
|
-
});
|
|
1691
|
-
reg.registerInit({
|
|
1692
|
-
deps: {
|
|
1693
|
-
httpRouter: backendPluginApi.coreServices.httpRouter,
|
|
1694
|
-
logger: backendPluginApi.coreServices.logger,
|
|
1695
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
1696
|
-
database: backendPluginApi.coreServices.database,
|
|
1697
|
-
discovery: backendPluginApi.coreServices.discovery,
|
|
1698
|
-
auth: backendPluginApi.coreServices.auth,
|
|
1699
|
-
httpAuth: backendPluginApi.coreServices.httpAuth,
|
|
1700
|
-
catalogApi: alpha.catalogServiceRef
|
|
1701
|
-
},
|
|
1702
|
-
async init({
|
|
1703
|
-
httpRouter,
|
|
1704
|
-
logger,
|
|
1705
|
-
config,
|
|
1706
|
-
database,
|
|
1707
|
-
discovery,
|
|
1708
|
-
auth,
|
|
1709
|
-
httpAuth,
|
|
1710
|
-
catalogApi
|
|
1711
|
-
}) {
|
|
1712
|
-
const router = await createRouter({
|
|
1713
|
-
logger,
|
|
1714
|
-
config,
|
|
1715
|
-
database,
|
|
1716
|
-
discovery,
|
|
1717
|
-
auth,
|
|
1718
|
-
httpAuth,
|
|
1719
|
-
catalogApi,
|
|
1720
|
-
providerFactories: Object.fromEntries(providers),
|
|
1721
|
-
disableDefaultProviderFactories: true,
|
|
1722
|
-
ownershipResolver
|
|
1723
|
-
});
|
|
1724
|
-
httpRouter.addAuthPolicy({
|
|
1725
|
-
path: "/",
|
|
1726
|
-
allow: "unauthenticated"
|
|
1727
|
-
});
|
|
1728
|
-
httpRouter.use(router);
|
|
1729
|
-
}
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1732
|
-
});
|
|
1733
|
-
|
|
1734
|
-
const OAuthEnvironmentHandler = pluginAuthNode.OAuthEnvironmentHandler;
|
|
1735
|
-
|
|
1736
|
-
const readState = pluginAuthNode.decodeOAuthState;
|
|
1737
|
-
const encodeState = pluginAuthNode.encodeOAuthState;
|
|
1738
|
-
const verifyNonce = (req, providerId) => {
|
|
1739
|
-
const cookieNonce = req.cookies[`${providerId}-nonce`];
|
|
1740
|
-
const state = readState(req.query.state?.toString() ?? "");
|
|
1741
|
-
const stateNonce = state.nonce;
|
|
1742
|
-
if (!cookieNonce) {
|
|
1743
|
-
throw new Error("Auth response is missing cookie nonce");
|
|
1744
|
-
}
|
|
1745
|
-
if (stateNonce.length === 0) {
|
|
1746
|
-
throw new Error("Auth response is missing state nonce");
|
|
1747
|
-
}
|
|
1748
|
-
if (cookieNonce !== stateNonce) {
|
|
1749
|
-
throw new Error("Invalid nonce");
|
|
1750
|
-
}
|
|
1751
|
-
};
|
|
1752
|
-
const defaultCookieConfigurer = ({
|
|
1753
|
-
callbackUrl,
|
|
1754
|
-
providerId,
|
|
1755
|
-
appOrigin
|
|
1756
|
-
}) => {
|
|
1757
|
-
const { hostname: domain, pathname, protocol } = new URL(callbackUrl);
|
|
1758
|
-
const secure = protocol === "https:";
|
|
1759
|
-
let sameSite = "lax";
|
|
1760
|
-
if (new URL(appOrigin).hostname !== domain && secure) {
|
|
1761
|
-
sameSite = "none";
|
|
1762
|
-
}
|
|
1763
|
-
const path = pathname.endsWith(`${providerId}/handler/frame`) ? pathname.slice(0, -"/handler/frame".length) : `${pathname}/${providerId}`;
|
|
1764
|
-
return { domain, path, secure, sameSite };
|
|
1765
|
-
};
|
|
1766
|
-
|
|
1767
|
-
const THOUSAND_DAYS_MS = 1e3 * 24 * 60 * 60 * 1e3;
|
|
1768
|
-
const TEN_MINUTES_MS = 600 * 1e3;
|
|
1769
|
-
class OAuthAdapter {
|
|
1770
|
-
constructor(handlers, options) {
|
|
1771
|
-
this.handlers = handlers;
|
|
1772
|
-
this.options = options;
|
|
1773
|
-
this.baseCookieOptions = {
|
|
1774
|
-
httpOnly: true,
|
|
1775
|
-
sameSite: "lax"
|
|
1776
|
-
};
|
|
1777
|
-
}
|
|
1778
|
-
static fromConfig(config, handlers, options) {
|
|
1779
|
-
const { appUrl, baseUrl, isOriginAllowed } = config;
|
|
1780
|
-
const { origin: appOrigin } = new url.URL(appUrl);
|
|
1781
|
-
const cookieConfigurer = config.cookieConfigurer ?? defaultCookieConfigurer;
|
|
1782
|
-
return new OAuthAdapter(handlers, {
|
|
1783
|
-
...options,
|
|
1784
|
-
appOrigin,
|
|
1785
|
-
baseUrl,
|
|
1786
|
-
cookieConfigurer,
|
|
1787
|
-
isOriginAllowed
|
|
1788
|
-
});
|
|
1789
|
-
}
|
|
1790
|
-
baseCookieOptions;
|
|
1791
|
-
async start(req, res) {
|
|
1792
|
-
const scope = req.query.scope?.toString() ?? "";
|
|
1793
|
-
const env = req.query.env?.toString();
|
|
1794
|
-
const origin = req.query.origin?.toString();
|
|
1795
|
-
const redirectUrl = req.query.redirectUrl?.toString();
|
|
1796
|
-
const flow = req.query.flow?.toString();
|
|
1797
|
-
if (!env) {
|
|
1798
|
-
throw new errors.InputError("No env provided in request query parameters");
|
|
1799
|
-
}
|
|
1800
|
-
const cookieConfig = this.getCookieConfig(origin);
|
|
1801
|
-
const nonce = crypto__default.default.randomBytes(16).toString("base64");
|
|
1802
|
-
this.setNonceCookie(res, nonce, cookieConfig);
|
|
1803
|
-
const state = { nonce, env, origin, redirectUrl, flow };
|
|
1804
|
-
if (this.options.persistScopes) {
|
|
1805
|
-
state.scope = scope;
|
|
1806
|
-
}
|
|
1807
|
-
const forwardReq = Object.assign(req, { scope, state });
|
|
1808
|
-
const { url, status } = await this.handlers.start(
|
|
1809
|
-
forwardReq
|
|
1810
|
-
);
|
|
1811
|
-
res.statusCode = status || 302;
|
|
1812
|
-
res.setHeader("Location", url);
|
|
1813
|
-
res.setHeader("Content-Length", "0");
|
|
1814
|
-
res.end();
|
|
1815
|
-
}
|
|
1816
|
-
async frameHandler(req, res) {
|
|
1817
|
-
let appOrigin = this.options.appOrigin;
|
|
1818
|
-
try {
|
|
1819
|
-
const state = readState(req.query.state?.toString() ?? "");
|
|
1820
|
-
if (state.origin) {
|
|
1821
|
-
try {
|
|
1822
|
-
appOrigin = new url.URL(state.origin).origin;
|
|
1823
|
-
} catch {
|
|
1824
|
-
throw new errors.NotAllowedError("App origin is invalid, failed to parse");
|
|
1825
|
-
}
|
|
1826
|
-
if (!this.options.isOriginAllowed(appOrigin)) {
|
|
1827
|
-
throw new errors.NotAllowedError(`Origin '${appOrigin}' is not allowed`);
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
verifyNonce(req, this.options.providerId);
|
|
1831
|
-
const { response, refreshToken } = await this.handlers.handler(req);
|
|
1832
|
-
const cookieConfig = this.getCookieConfig(appOrigin);
|
|
1833
|
-
if (this.options.persistScopes && state.scope) {
|
|
1834
|
-
this.setGrantedScopeCookie(res, state.scope, cookieConfig);
|
|
1835
|
-
response.providerInfo.scope = state.scope;
|
|
1836
|
-
}
|
|
1837
|
-
if (refreshToken) {
|
|
1838
|
-
this.setRefreshTokenCookie(res, refreshToken, cookieConfig);
|
|
1839
|
-
}
|
|
1840
|
-
const identity = await this.populateIdentity(response.backstageIdentity);
|
|
1841
|
-
const responseObj = {
|
|
1842
|
-
type: "authorization_response",
|
|
1843
|
-
response: { ...response, backstageIdentity: identity }
|
|
1844
|
-
};
|
|
1845
|
-
if (state.flow === "redirect") {
|
|
1846
|
-
if (!state.redirectUrl) {
|
|
1847
|
-
throw new errors.InputError(
|
|
1848
|
-
"No redirectUrl provided in request query parameters"
|
|
1849
|
-
);
|
|
1850
|
-
}
|
|
1851
|
-
res.redirect(state.redirectUrl);
|
|
1852
|
-
return void 0;
|
|
1853
|
-
}
|
|
1854
|
-
return postMessageResponse(res, appOrigin, responseObj);
|
|
1855
|
-
} catch (error) {
|
|
1856
|
-
const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
1857
|
-
return postMessageResponse(res, appOrigin, {
|
|
1858
|
-
type: "authorization_response",
|
|
1859
|
-
error: { name, message }
|
|
1860
|
-
});
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
async logout(req, res) {
|
|
1864
|
-
if (!ensuresXRequestedWith(req)) {
|
|
1865
|
-
throw new errors.AuthenticationError("Invalid X-Requested-With header");
|
|
1866
|
-
}
|
|
1867
|
-
if (this.handlers.logout) {
|
|
1868
|
-
const refreshToken = this.getRefreshTokenFromCookie(req);
|
|
1869
|
-
const revokeRequest = Object.assign(req, {
|
|
1870
|
-
refreshToken
|
|
1871
|
-
});
|
|
1872
|
-
await this.handlers.logout(revokeRequest);
|
|
1873
|
-
}
|
|
1874
|
-
const origin = req.get("origin");
|
|
1875
|
-
const cookieConfig = this.getCookieConfig(origin);
|
|
1876
|
-
this.removeRefreshTokenCookie(res, cookieConfig);
|
|
1877
|
-
res.status(200).end();
|
|
1878
|
-
}
|
|
1879
|
-
async refresh(req, res) {
|
|
1880
|
-
if (!ensuresXRequestedWith(req)) {
|
|
1881
|
-
throw new errors.AuthenticationError("Invalid X-Requested-With header");
|
|
1882
|
-
}
|
|
1883
|
-
if (!this.handlers.refresh) {
|
|
1884
|
-
throw new errors.InputError(
|
|
1885
|
-
`Refresh token is not supported for provider ${this.options.providerId}`
|
|
1886
|
-
);
|
|
1887
|
-
}
|
|
1888
|
-
try {
|
|
1889
|
-
const refreshToken = this.getRefreshTokenFromCookie(req);
|
|
1890
|
-
if (!refreshToken) {
|
|
1891
|
-
throw new errors.InputError("Missing session cookie");
|
|
1892
|
-
}
|
|
1893
|
-
let scope = req.query.scope?.toString() ?? "";
|
|
1894
|
-
if (this.options.persistScopes) {
|
|
1895
|
-
scope = this.getGrantedScopeFromCookie(req);
|
|
1896
|
-
}
|
|
1897
|
-
const forwardReq = Object.assign(req, { scope, refreshToken });
|
|
1898
|
-
const { response, refreshToken: newRefreshToken } = await this.handlers.refresh(forwardReq);
|
|
1899
|
-
const backstageIdentity = await this.populateIdentity(
|
|
1900
|
-
response.backstageIdentity
|
|
1901
|
-
);
|
|
1902
|
-
if (newRefreshToken && newRefreshToken !== refreshToken) {
|
|
1903
|
-
const origin = req.get("origin");
|
|
1904
|
-
const cookieConfig = this.getCookieConfig(origin);
|
|
1905
|
-
this.setRefreshTokenCookie(res, newRefreshToken, cookieConfig);
|
|
1906
|
-
}
|
|
1907
|
-
res.status(200).json({ ...response, backstageIdentity });
|
|
1908
|
-
} catch (error) {
|
|
1909
|
-
throw new errors.AuthenticationError("Refresh failed", error);
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
/**
|
|
1913
|
-
* If the response from the OAuth provider includes a Backstage identity, we
|
|
1914
|
-
* make sure it's populated with all the information we can derive from the user ID.
|
|
1915
|
-
*/
|
|
1916
|
-
async populateIdentity(identity) {
|
|
1917
|
-
if (!identity) {
|
|
1918
|
-
return void 0;
|
|
1919
|
-
}
|
|
1920
|
-
if (!identity.token) {
|
|
1921
|
-
throw new errors.InputError(`Identity response must return a token`);
|
|
1922
|
-
}
|
|
1923
|
-
return prepareBackstageIdentityResponse(identity);
|
|
1924
|
-
}
|
|
1925
|
-
setNonceCookie = (res, nonce, cookieConfig) => {
|
|
1926
|
-
res.cookie(`${this.options.providerId}-nonce`, nonce, {
|
|
1927
|
-
maxAge: TEN_MINUTES_MS,
|
|
1928
|
-
...this.baseCookieOptions,
|
|
1929
|
-
...cookieConfig,
|
|
1930
|
-
path: `${cookieConfig.path}/handler`
|
|
1931
|
-
});
|
|
1932
|
-
};
|
|
1933
|
-
setGrantedScopeCookie = (res, scope, cookieConfig) => {
|
|
1934
|
-
res.cookie(`${this.options.providerId}-granted-scope`, scope, {
|
|
1935
|
-
maxAge: THOUSAND_DAYS_MS,
|
|
1936
|
-
...this.baseCookieOptions,
|
|
1937
|
-
...cookieConfig
|
|
1938
|
-
});
|
|
1939
|
-
};
|
|
1940
|
-
getRefreshTokenFromCookie = (req) => {
|
|
1941
|
-
return req.cookies[`${this.options.providerId}-refresh-token`];
|
|
1942
|
-
};
|
|
1943
|
-
getGrantedScopeFromCookie = (req) => {
|
|
1944
|
-
return req.cookies[`${this.options.providerId}-granted-scope`];
|
|
1945
|
-
};
|
|
1946
|
-
setRefreshTokenCookie = (res, refreshToken, cookieConfig) => {
|
|
1947
|
-
res.cookie(`${this.options.providerId}-refresh-token`, refreshToken, {
|
|
1948
|
-
maxAge: THOUSAND_DAYS_MS,
|
|
1949
|
-
...this.baseCookieOptions,
|
|
1950
|
-
...cookieConfig
|
|
1951
|
-
});
|
|
1952
|
-
};
|
|
1953
|
-
removeRefreshTokenCookie = (res, cookieConfig) => {
|
|
1954
|
-
res.cookie(`${this.options.providerId}-refresh-token`, "", {
|
|
1955
|
-
maxAge: 0,
|
|
1956
|
-
...this.baseCookieOptions,
|
|
1957
|
-
...cookieConfig
|
|
1958
|
-
});
|
|
1959
|
-
};
|
|
1960
|
-
getCookieConfig = (origin) => {
|
|
1961
|
-
return this.options.cookieConfigurer({
|
|
1962
|
-
providerId: this.options.providerId,
|
|
1963
|
-
baseUrl: this.options.baseUrl,
|
|
1964
|
-
callbackUrl: this.options.callbackUrl,
|
|
1965
|
-
appOrigin: origin ?? this.options.appOrigin
|
|
1966
|
-
});
|
|
1967
|
-
};
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
exports.CatalogIdentityClient = CatalogIdentityClient;
|
|
1971
|
-
exports.OAuthAdapter = OAuthAdapter;
|
|
1972
|
-
exports.OAuthEnvironmentHandler = OAuthEnvironmentHandler;
|
|
1973
|
-
exports.createAuthProviderIntegration = createAuthProviderIntegration;
|
|
1974
|
-
exports.createOriginFilter = createOriginFilter;
|
|
1975
|
-
exports.createRouter = createRouter;
|
|
1976
|
-
exports.default = authPlugin;
|
|
1977
|
-
exports.defaultAuthProviderFactories = defaultAuthProviderFactories;
|
|
1978
|
-
exports.encodeState = encodeState;
|
|
1979
|
-
exports.ensuresXRequestedWith = ensuresXRequestedWith;
|
|
1980
|
-
exports.getDefaultOwnershipEntityRefs = getDefaultOwnershipEntityRefs;
|
|
1981
|
-
exports.postMessageResponse = postMessageResponse;
|
|
1982
|
-
exports.prepareBackstageIdentityResponse = prepareBackstageIdentityResponse;
|
|
1983
|
-
exports.providers = providers;
|
|
1984
|
-
exports.readState = readState;
|
|
1985
|
-
exports.verifyNonce = verifyNonce;
|
|
5
|
+
var authPlugin = require('./authPlugin.cjs.js');
|
|
6
|
+
var router = require('./service/router.cjs.js');
|
|
7
|
+
var providers = require('./providers/providers.cjs.js');
|
|
8
|
+
var router$1 = require('./providers/router.cjs.js');
|
|
9
|
+
var createAuthProviderIntegration = require('./providers/createAuthProviderIntegration.cjs.js');
|
|
10
|
+
var prepareBackstageIdentityResponse = require('./providers/prepareBackstageIdentityResponse.cjs.js');
|
|
11
|
+
var authFlowHelpers = require('./lib/flow/authFlowHelpers.cjs.js');
|
|
12
|
+
var OAuthEnvironmentHandler = require('./lib/oauth/OAuthEnvironmentHandler.cjs.js');
|
|
13
|
+
var OAuthAdapter = require('./lib/oauth/OAuthAdapter.cjs.js');
|
|
14
|
+
var helpers = require('./lib/oauth/helpers.cjs.js');
|
|
15
|
+
var CatalogIdentityClient = require('./lib/catalog/CatalogIdentityClient.cjs.js');
|
|
16
|
+
var CatalogAuthResolverContext = require('./lib/resolvers/CatalogAuthResolverContext.cjs.js');
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
exports.default = authPlugin.authPlugin;
|
|
21
|
+
exports.createRouter = router.createRouter;
|
|
22
|
+
exports.defaultAuthProviderFactories = providers.defaultAuthProviderFactories;
|
|
23
|
+
exports.providers = providers.providers;
|
|
24
|
+
exports.createOriginFilter = router$1.createOriginFilter;
|
|
25
|
+
exports.createAuthProviderIntegration = createAuthProviderIntegration.createAuthProviderIntegration;
|
|
26
|
+
exports.prepareBackstageIdentityResponse = prepareBackstageIdentityResponse.prepareBackstageIdentityResponse;
|
|
27
|
+
exports.ensuresXRequestedWith = authFlowHelpers.ensuresXRequestedWith;
|
|
28
|
+
exports.postMessageResponse = authFlowHelpers.postMessageResponse;
|
|
29
|
+
exports.OAuthEnvironmentHandler = OAuthEnvironmentHandler.OAuthEnvironmentHandler;
|
|
30
|
+
exports.OAuthAdapter = OAuthAdapter.OAuthAdapter;
|
|
31
|
+
exports.encodeState = helpers.encodeState;
|
|
32
|
+
exports.readState = helpers.readState;
|
|
33
|
+
exports.verifyNonce = helpers.verifyNonce;
|
|
34
|
+
exports.CatalogIdentityClient = CatalogIdentityClient.CatalogIdentityClient;
|
|
35
|
+
exports.getDefaultOwnershipEntityRefs = CatalogAuthResolverContext.getDefaultOwnershipEntityRefs;
|
|
1986
36
|
//# sourceMappingURL=index.cjs.js.map
|