@scalekit-sdk/node 2.2.0 → 2.2.2
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/{reference.md → REFERENCE.md} +530 -77
- package/lib/core.js +1 -1
- package/package.json +9 -3
- package/.github/dependabot.yml +0 -10
- package/.nvmrc +0 -1
- package/buf.gen.yaml +0 -20
- package/jest.config.js +0 -15
- package/src/auth.ts +0 -99
- package/src/connect.ts +0 -32
- package/src/connection.ts +0 -267
- package/src/constants/user.ts +0 -22
- package/src/core.ts +0 -139
- package/src/directory.ts +0 -431
- package/src/domain.ts +0 -273
- package/src/errors/base-exception.ts +0 -263
- package/src/errors/index.ts +0 -3
- package/src/errors/specific-exceptions.ts +0 -88
- package/src/index.ts +0 -10
- package/src/organization.ts +0 -571
- package/src/passwordless.ts +0 -139
- package/src/permission.ts +0 -310
- package/src/pkg/grpc/buf/validate/validate_pb.ts +0 -28
- package/src/pkg/grpc/google/api/annotations_pb.ts +0 -28
- package/src/pkg/grpc/google/api/field_behavior_pb.ts +0 -28
- package/src/pkg/grpc/google/api/visibility_pb.ts +0 -28
- package/src/pkg/grpc/protoc-gen-openapiv2/options/annotations_pb.ts +0 -28
- package/src/pkg/grpc/scalekit/v1/auditlogs/auditlogs_pb.ts +0 -257
- package/src/pkg/grpc/scalekit/v1/auth/auth_pb.ts +0 -836
- package/src/pkg/grpc/scalekit/v1/auth/passwordless_pb.ts +0 -264
- package/src/pkg/grpc/scalekit/v1/auth/webauthn_pb.ts +0 -794
- package/src/pkg/grpc/scalekit/v1/commons/commons_pb.ts +0 -452
- package/src/pkg/grpc/scalekit/v1/connections/connections_pb.ts +0 -2645
- package/src/pkg/grpc/scalekit/v1/directories/directories_pb.ts +0 -1393
- package/src/pkg/grpc/scalekit/v1/domains/domains_pb.ts +0 -599
- package/src/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.ts +0 -311
- package/src/pkg/grpc/scalekit/v1/options/options_pb.ts +0 -200
- package/src/pkg/grpc/scalekit/v1/organizations/organizations_pb.ts +0 -1141
- package/src/pkg/grpc/scalekit/v1/roles/roles_pb.ts +0 -1491
- package/src/pkg/grpc/scalekit/v1/sessions/sessions_pb.ts +0 -497
- package/src/pkg/grpc/scalekit/v1/users/users_pb.ts +0 -1404
- package/src/role.ts +0 -463
- package/src/scalekit.ts +0 -800
- package/src/session.ts +0 -323
- package/src/types/auth.ts +0 -73
- package/src/types/organization.ts +0 -12
- package/src/types/scalekit.ts +0 -50
- package/src/types/user.ts +0 -21
- package/src/user.ts +0 -829
- package/src/webauthn.ts +0 -99
- package/tests/README.md +0 -25
- package/tests/connection.test.ts +0 -42
- package/tests/directory.test.ts +0 -46
- package/tests/domain.test.ts +0 -293
- package/tests/organization.test.ts +0 -81
- package/tests/passwordless.test.ts +0 -108
- package/tests/permission.test.ts +0 -399
- package/tests/role.test.ts +0 -323
- package/tests/scalekit.test.ts +0 -104
- package/tests/setup.ts +0 -34
- package/tests/users.test.ts +0 -168
- package/tests/utils/test-data.ts +0 -490
- package/tsconfig.json +0 -19
package/src/scalekit.ts
DELETED
|
@@ -1,800 +0,0 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
2
|
-
import * as jose from 'jose';
|
|
3
|
-
import QueryString from 'qs';
|
|
4
|
-
import GrpcConnect from './connect';
|
|
5
|
-
import ConnectionClient from './connection';
|
|
6
|
-
import { IdTokenClaimToUserMap } from './constants/user';
|
|
7
|
-
import CoreClient from './core';
|
|
8
|
-
import DirectoryClient from './directory';
|
|
9
|
-
import DomainClient from './domain';
|
|
10
|
-
import AuthClient from './auth';
|
|
11
|
-
import OrganizationClient from './organization';
|
|
12
|
-
import PasswordlessClient from './passwordless';
|
|
13
|
-
import UserClient from './user';
|
|
14
|
-
import SessionClient from './session';
|
|
15
|
-
import RoleClient from './role';
|
|
16
|
-
import PermissionClient from './permission';
|
|
17
|
-
import WebAuthnClient from './webauthn';
|
|
18
|
-
import { IdpInitiatedLoginClaims, IdTokenClaim, User } from './types/auth';
|
|
19
|
-
import { AuthenticationOptions, AuthenticationResponse, AuthorizationUrlOptions, GrantType, LogoutUrlOptions, RefreshTokenResponse ,TokenValidationOptions } from './types/scalekit';
|
|
20
|
-
import { WebhookVerificationError, ScalekitValidateTokenFailureException } from './errors/base-exception';
|
|
21
|
-
|
|
22
|
-
const authorizeEndpoint = "oauth/authorize";
|
|
23
|
-
const logoutEndpoint = "oidc/logout";
|
|
24
|
-
const WEBHOOK_TOLERANCE_IN_SECONDS = 5 * 60; // 5 minutes
|
|
25
|
-
const WEBHOOK_SIGNATURE_VERSION = "v1";
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Main Scalekit SDK client for interacting with all Scalekit API endpoints.
|
|
29
|
-
*
|
|
30
|
-
* TIP: You can use it as a singleton object - that is you can initialize it just once and use the same client variable wherever required.
|
|
31
|
-
*
|
|
32
|
-
* This is the primary entry point for interacting with Scalekit's authentication services,
|
|
33
|
-
* including SSO, SCIM, user management, roles, permissions, and passwordless authentication.
|
|
34
|
-
*
|
|
35
|
-
* You can find the Environment URL, Client ID and Client Secret in Scalekit Dashboard -> Developers (Settings) -> API Credentials
|
|
36
|
-
*
|
|
37
|
-
* @param {string} envUrl - The Scalekit environment URL (e.g., "https://yourorg.scalekit.com" or your configured custom domain like "https://auth.yourapp.ai")
|
|
38
|
-
* @param {string} clientId - Your Scalekit client ID from the Scalekit Dashboard
|
|
39
|
-
* @param {string} clientSecret - Your Scalekit client secret from the Scalekit Dashboard
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* // Initialize the Scalekit client
|
|
43
|
-
* import { ScalekitClient } from '@scalekit-sdk/node';
|
|
44
|
-
*
|
|
45
|
-
* const scalekitClient = new ScalekitClient(
|
|
46
|
-
* process.env.SCALEKIT_ENV_URL,
|
|
47
|
-
* process.env.SCALEKIT_CLIENT_ID,
|
|
48
|
-
* process.env.SCALEKIT_CLIENT_SECRET
|
|
49
|
-
* );
|
|
50
|
-
*
|
|
51
|
-
* // Access various client modules
|
|
52
|
-
* const organizations = await scalekitClient.organization.listOrganization();
|
|
53
|
-
* const users = await scalekitClient.user.listUsers();
|
|
54
|
-
*
|
|
55
|
-
* @see {@link https://docs.scalekit.com/apis/ | Scalekit API Documentation}
|
|
56
|
-
*/
|
|
57
|
-
export default class ScalekitClient {
|
|
58
|
-
private readonly coreClient: CoreClient;
|
|
59
|
-
private readonly grpcConnect: GrpcConnect;
|
|
60
|
-
readonly organization: OrganizationClient;
|
|
61
|
-
readonly connection: ConnectionClient;
|
|
62
|
-
readonly domain: DomainClient;
|
|
63
|
-
readonly directory: DirectoryClient;
|
|
64
|
-
readonly passwordless: PasswordlessClient;
|
|
65
|
-
readonly user: UserClient;
|
|
66
|
-
readonly session: SessionClient;
|
|
67
|
-
readonly role: RoleClient;
|
|
68
|
-
readonly permission: PermissionClient;
|
|
69
|
-
readonly auth: AuthClient;
|
|
70
|
-
readonly webauthn: WebAuthnClient;
|
|
71
|
-
constructor(
|
|
72
|
-
envUrl: string,
|
|
73
|
-
clientId: string,
|
|
74
|
-
clientSecret: string
|
|
75
|
-
) {
|
|
76
|
-
this.coreClient = new CoreClient(
|
|
77
|
-
envUrl,
|
|
78
|
-
clientId,
|
|
79
|
-
clientSecret
|
|
80
|
-
);
|
|
81
|
-
this.grpcConnect = new GrpcConnect(
|
|
82
|
-
this.coreClient
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
this.organization = new OrganizationClient(
|
|
86
|
-
this.grpcConnect,
|
|
87
|
-
this.coreClient
|
|
88
|
-
);
|
|
89
|
-
this.connection = new ConnectionClient(this.grpcConnect, this.coreClient);
|
|
90
|
-
this.domain = new DomainClient(this.grpcConnect, this.coreClient);
|
|
91
|
-
this.directory = new DirectoryClient(this.grpcConnect, this.coreClient);
|
|
92
|
-
this.passwordless = new PasswordlessClient(
|
|
93
|
-
this.grpcConnect,
|
|
94
|
-
this.coreClient
|
|
95
|
-
);
|
|
96
|
-
this.user = new UserClient(
|
|
97
|
-
this.grpcConnect,
|
|
98
|
-
this.coreClient
|
|
99
|
-
);
|
|
100
|
-
this.session = new SessionClient(
|
|
101
|
-
this.grpcConnect,
|
|
102
|
-
this.coreClient
|
|
103
|
-
);
|
|
104
|
-
this.role = new RoleClient(
|
|
105
|
-
this.grpcConnect,
|
|
106
|
-
this.coreClient
|
|
107
|
-
);
|
|
108
|
-
this.permission = new PermissionClient(
|
|
109
|
-
this.grpcConnect,
|
|
110
|
-
this.coreClient
|
|
111
|
-
);
|
|
112
|
-
this.auth = new AuthClient(
|
|
113
|
-
this.grpcConnect,
|
|
114
|
-
this.coreClient
|
|
115
|
-
);
|
|
116
|
-
this.webauthn = new WebAuthnClient(
|
|
117
|
-
this.grpcConnect,
|
|
118
|
-
this.coreClient
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Utility method to generate the OAuth 2.0 authorization URL to initiate the SSO authentication flow.
|
|
124
|
-
*
|
|
125
|
-
* This method doesn't make any network calls but instead generates a fully formed Authorization URL
|
|
126
|
-
* as a string that you can redirect your users to initiate authentication.
|
|
127
|
-
*
|
|
128
|
-
* @param {string} redirectUri - The URL where users will be redirected after authentication.
|
|
129
|
-
* Must match one of the redirect URIs configured in your Scalekit dashboard.
|
|
130
|
-
* @param {AuthorizationUrlOptions} [options] - Optional configuration for the authorization request
|
|
131
|
-
* @param {string[]} [options.scopes=['openid', 'profile', 'email']] - OAuth scopes to request. Default includes openid, profile, and email.
|
|
132
|
-
* @param {string} [options.state] - Opaque value to maintain state between request and callback. Used to prevent CSRF attacks.
|
|
133
|
-
* @param {string} [options.nonce] - String value used to associate a client session with an ID Token.
|
|
134
|
-
* @param {string} [options.loginHint] - Hint to the authorization server about the login identifier the user might use (e.g., email address).
|
|
135
|
-
* @param {string} [options.domainHint] - Domain hint to identify which organization's IdP to use for authentication.
|
|
136
|
-
* @param {string} [options.connectionId] - Specific SSO connection ID to use for authentication.
|
|
137
|
-
* @param {string} [options.organizationId] - Organization ID to authenticate against.
|
|
138
|
-
* @param {string} [options.provider] - Social login provider (e.g., 'google', 'github', 'microsoft').
|
|
139
|
-
* @param {string} [options.codeChallenge] - PKCE code challenge for enhanced security in public clients.
|
|
140
|
-
* @param {string} [options.codeChallengeMethod] - Method used to generate the code challenge (we support only 'S256').
|
|
141
|
-
* @param {string} [options.prompt] - Controls the authorization server's authentication behavior (e.g., 'login', 'consent', 'create').
|
|
142
|
-
*
|
|
143
|
-
* @returns {string} The complete authorization URL to redirect the user to
|
|
144
|
-
*
|
|
145
|
-
* @example
|
|
146
|
-
* // Initiate Enterprise SSO authentication for a given org_id
|
|
147
|
-
* const authUrl = scalekitClient.getAuthorizationUrl(
|
|
148
|
-
* 'https://yourapp.com/auth/callback',
|
|
149
|
-
* {
|
|
150
|
-
* state: 'random-state-value',
|
|
151
|
-
* organizationId: 'org_123456'
|
|
152
|
-
* }
|
|
153
|
-
* );
|
|
154
|
-
* // Redirect user to authUrl
|
|
155
|
-
*
|
|
156
|
-
* @example
|
|
157
|
-
* // Initiate Enterprise SSO authentication for a specific connection id
|
|
158
|
-
* // optionally, pass the loginhint to the 3rd party identity provider.
|
|
159
|
-
* const authUrl = scalekitClient.getAuthorizationUrl(
|
|
160
|
-
* 'https://yourapp.com/auth/callback',
|
|
161
|
-
* {
|
|
162
|
-
* connectionId: 'conn_abc123',
|
|
163
|
-
* loginHint: 'user@company.com'
|
|
164
|
-
* }
|
|
165
|
-
* );
|
|
166
|
-
*
|
|
167
|
-
* @example
|
|
168
|
-
* // Social login
|
|
169
|
-
* const authUrl = scalekitClient.getAuthorizationUrl(
|
|
170
|
-
* 'https://yourapp.com/auth/callback',
|
|
171
|
-
* {
|
|
172
|
-
* provider: 'google',
|
|
173
|
-
* state: 'random-state'
|
|
174
|
-
* }
|
|
175
|
-
* );
|
|
176
|
-
*
|
|
177
|
-
* @example
|
|
178
|
-
* // PKCE flow for public clients
|
|
179
|
-
* const authUrl = scalekitClient.getAuthorizationUrl(
|
|
180
|
-
* 'https://yourapp.com/auth/callback',
|
|
181
|
-
* {
|
|
182
|
-
* codeChallenge: 'your-code-challenge',
|
|
183
|
-
* codeChallengeMethod: 'S256',
|
|
184
|
-
* organizationId: 'org_123456'
|
|
185
|
-
* }
|
|
186
|
-
* );
|
|
187
|
-
*
|
|
188
|
-
* @see {@link https://docs.scalekit.com/apis/#tag/api%20auth | Authentication API Documentation}
|
|
189
|
-
* @see {@link authenticateWithCode} - Use this method to exchange the authorization code for tokens
|
|
190
|
-
*/
|
|
191
|
-
getAuthorizationUrl(
|
|
192
|
-
redirectUri: string,
|
|
193
|
-
options?: AuthorizationUrlOptions
|
|
194
|
-
): string {
|
|
195
|
-
const defaultOptions: AuthorizationUrlOptions = {
|
|
196
|
-
scopes: ["openid", "profile", "email"],
|
|
197
|
-
};
|
|
198
|
-
options = {
|
|
199
|
-
...defaultOptions,
|
|
200
|
-
...options,
|
|
201
|
-
};
|
|
202
|
-
const qs = QueryString.stringify({
|
|
203
|
-
response_type: "code",
|
|
204
|
-
client_id: this.coreClient.clientId,
|
|
205
|
-
redirect_uri: redirectUri,
|
|
206
|
-
scope: options.scopes?.join(" "),
|
|
207
|
-
...(options.state && { state: options.state }),
|
|
208
|
-
...(options.nonce && { nonce: options.nonce }),
|
|
209
|
-
...(options.loginHint && { login_hint: options.loginHint }),
|
|
210
|
-
...(options.domainHint && { domain_hint: options.domainHint }),
|
|
211
|
-
...(options.domainHint && { domain: options.domainHint }),
|
|
212
|
-
...(options.connectionId && { connection_id: options.connectionId }),
|
|
213
|
-
...(options.organizationId && {
|
|
214
|
-
organization_id: options.organizationId,
|
|
215
|
-
}),
|
|
216
|
-
...(options.codeChallenge && { code_challenge: options.codeChallenge }),
|
|
217
|
-
...(options.codeChallengeMethod && {
|
|
218
|
-
code_challenge_method: options.codeChallengeMethod,
|
|
219
|
-
}),
|
|
220
|
-
...(options.provider && { provider: options.provider }),
|
|
221
|
-
...(options.prompt && { prompt: options.prompt }),
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
return `${this.coreClient.envUrl}/${authorizeEndpoint}?${qs}`;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Exchanges an authorization code for access tokens and user information.
|
|
229
|
-
*
|
|
230
|
-
* This method completes the OAuth 2.0 authorization code flow by exchanging the code
|
|
231
|
-
* received in the callback for access tokens, ID tokens, and user profile information.
|
|
232
|
-
* Call this method in your redirect URI handler after receiving the authorization code.
|
|
233
|
-
*
|
|
234
|
-
* @param {string} code - The authorization code received in the callback URL after user authentication
|
|
235
|
-
* @param {string} redirectUri - The same redirect URI used in getAuthorizationUrl(). Must match exactly.
|
|
236
|
-
* @param {AuthenticationOptions} [options] - Optional authentication configuration
|
|
237
|
-
* @param {string} [options.codeVerifier] - PKCE code verifier to validate the code challenge (required if PKCE was used)
|
|
238
|
-
*
|
|
239
|
-
* @returns {Promise<AuthenticationResponse>} Authentication response containing:
|
|
240
|
-
* - user: User profile information (email, name, organization, etc.)
|
|
241
|
-
* - idToken: JWT ID token containing user claims
|
|
242
|
-
* - accessToken: Access token for API authorization
|
|
243
|
-
* - expiresIn: Token expiration time in seconds
|
|
244
|
-
* - refreshToken: Refresh token for obtaining new access tokens
|
|
245
|
-
*
|
|
246
|
-
* @throws {Error} When the authorization code is invalid, expired, or already used
|
|
247
|
-
* @throws {Error} When the redirect URI doesn't match the one used in authorization
|
|
248
|
-
* @throws {Error} When PKCE code verifier is invalid or missing
|
|
249
|
-
*
|
|
250
|
-
* @example
|
|
251
|
-
* // Basic code exchange (server-side flow)
|
|
252
|
-
* app.get('/auth/callback', async (req, res) => {
|
|
253
|
-
* const { code } = req.query;
|
|
254
|
-
*
|
|
255
|
-
* try {
|
|
256
|
-
* const result = await scalekitClient.authenticateWithCode(
|
|
257
|
-
* code,
|
|
258
|
-
* 'https://yourapp.com/auth/callback'
|
|
259
|
-
* );
|
|
260
|
-
*
|
|
261
|
-
* // Store tokens securely
|
|
262
|
-
* req.session.accessToken = result.accessToken;
|
|
263
|
-
* req.session.user = result.user;
|
|
264
|
-
*
|
|
265
|
-
* res.redirect('/dashboard');
|
|
266
|
-
* } catch (error) {
|
|
267
|
-
* console.error('Authentication failed:', error);
|
|
268
|
-
* res.redirect('/login?error=auth_failed');
|
|
269
|
-
* }
|
|
270
|
-
* });
|
|
271
|
-
*
|
|
272
|
-
* @example
|
|
273
|
-
* // PKCE flow (for public clients)
|
|
274
|
-
* app.get('/auth/callback', async (req, res) => {
|
|
275
|
-
* const { code } = req.query;
|
|
276
|
-
* const codeVerifier = req.session.codeVerifier; // Stored during authorization
|
|
277
|
-
*
|
|
278
|
-
* const result = await scalekitClient.authenticateWithCode(
|
|
279
|
-
* code,
|
|
280
|
-
* 'https://yourapp.com/auth/callback',
|
|
281
|
-
* { codeVerifier }
|
|
282
|
-
* );
|
|
283
|
-
*
|
|
284
|
-
* // Use result.user, result.accessToken, etc.
|
|
285
|
-
* });
|
|
286
|
-
*
|
|
287
|
-
* @see {@link getAuthorizationUrl} - Generate the authorization URL first
|
|
288
|
-
* @see {@link validateAccessToken} - Validate tokens in subsequent requests
|
|
289
|
-
*/
|
|
290
|
-
async authenticateWithCode(
|
|
291
|
-
code: string,
|
|
292
|
-
redirectUri: string,
|
|
293
|
-
options?: AuthenticationOptions
|
|
294
|
-
): Promise<AuthenticationResponse> {
|
|
295
|
-
const res = await this.coreClient.authenticate(
|
|
296
|
-
QueryString.stringify({
|
|
297
|
-
code: code,
|
|
298
|
-
redirect_uri: redirectUri,
|
|
299
|
-
grant_type: GrantType.AuthorizationCode,
|
|
300
|
-
client_id: this.coreClient.clientId,
|
|
301
|
-
client_secret: this.coreClient.clientSecret,
|
|
302
|
-
...(options?.codeVerifier && { code_verifier: options.codeVerifier }),
|
|
303
|
-
})
|
|
304
|
-
);
|
|
305
|
-
const { id_token, access_token, expires_in, refresh_token } = res.data;
|
|
306
|
-
const claims = jose.decodeJwt<IdTokenClaim>(id_token);
|
|
307
|
-
const user = <User>{};
|
|
308
|
-
for (const [k, v] of Object.entries(claims)) {
|
|
309
|
-
if (IdTokenClaimToUserMap[k]) {
|
|
310
|
-
user[IdTokenClaimToUserMap[k]] = v;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return {
|
|
315
|
-
user,
|
|
316
|
-
idToken: id_token,
|
|
317
|
-
accessToken: access_token,
|
|
318
|
-
expiresIn: expires_in,
|
|
319
|
-
refreshToken: refresh_token,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Extracts and validates claims from an IdP-initiated login token.
|
|
325
|
-
*
|
|
326
|
-
* Use this method when handling IdP-initiated SSO flows, where the authentication is
|
|
327
|
-
* initiated from the identity provider's portal rather than your application. This validates
|
|
328
|
-
* the token and returns the necessary information to initiate a new SP Initiated SSO workflow.
|
|
329
|
-
*
|
|
330
|
-
* @param {string} idpInitiatedLoginToken - The token received in the 'idp_initiated_login' query parameter
|
|
331
|
-
* @param {TokenValidationOptions} [options] - Optional token validation configuration
|
|
332
|
-
* @param {string} [options.issuer] - Expected token issuer for validation
|
|
333
|
-
* @param {string} [options.audience] - Expected token audience for validation
|
|
334
|
-
*
|
|
335
|
-
* @returns {Promise<IdpInitiatedLoginClaims>} Claims containing:
|
|
336
|
-
* - connection_id: The SSO connection identifier
|
|
337
|
-
* - organization_id: The organization identifier
|
|
338
|
-
* - login_hint: User's email or login identifier
|
|
339
|
-
* - relay_state: Optional state parameter from the IdP
|
|
340
|
-
*
|
|
341
|
-
* @throws {ScalekitValidateTokenFailureException} When token validation fails
|
|
342
|
-
*
|
|
343
|
-
* @example
|
|
344
|
-
* // Handle IdP-initiated login
|
|
345
|
-
* app.get('/auth/callback', async (req, res) => {
|
|
346
|
-
* const { idp_initiated_login } = req.query;
|
|
347
|
-
*
|
|
348
|
-
* if (idp_initiated_login) {
|
|
349
|
-
* try {
|
|
350
|
-
* const claims = await scalekitClient.getIdpInitiatedLoginClaims(idp_initiated_login);
|
|
351
|
-
*
|
|
352
|
-
* // Redirect to authorization URL with the claims
|
|
353
|
-
* const authUrl = scalekitClient.getAuthorizationUrl(
|
|
354
|
-
* 'https://yourapp.com/auth/callback',
|
|
355
|
-
* {
|
|
356
|
-
* connectionId: claims.connection_id,
|
|
357
|
-
* organizationId: claims.organization_id,
|
|
358
|
-
* loginHint: claims.login_hint,
|
|
359
|
-
* ...(claims.relay_state && { state: claims.relay_state })
|
|
360
|
-
* }
|
|
361
|
-
* );
|
|
362
|
-
*
|
|
363
|
-
* return res.redirect(authUrl);
|
|
364
|
-
* } catch (error) {
|
|
365
|
-
* console.error('IdP-initiated login failed:', error);
|
|
366
|
-
* return res.redirect('/login?error=idp_login_failed');
|
|
367
|
-
* }
|
|
368
|
-
* }
|
|
369
|
-
* // Handle normal callback flow...
|
|
370
|
-
* });
|
|
371
|
-
*
|
|
372
|
-
* @see {@link https://docs.scalekit.com/sso/guides/idp-init-sso/ | IdP-Initiated SSO Documentation}
|
|
373
|
-
* @see {@link getAuthorizationUrl} - Use the claims to construct the authorization URL
|
|
374
|
-
*/
|
|
375
|
-
async getIdpInitiatedLoginClaims(
|
|
376
|
-
idpInitiatedLoginToken: string,
|
|
377
|
-
options?: TokenValidationOptions
|
|
378
|
-
): Promise<IdpInitiatedLoginClaims> {
|
|
379
|
-
return this.validateToken<IdpInitiatedLoginClaims>(
|
|
380
|
-
idpInitiatedLoginToken,
|
|
381
|
-
options
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Validates the access token and returns a boolean result.
|
|
387
|
-
*
|
|
388
|
-
* @param {string} token The token to be validated.
|
|
389
|
-
* @param {TokenValidationOptions} options Optional validation options for issuer, audience, and scopes
|
|
390
|
-
* @return {Promise<boolean>} Returns true if the token is valid, false otherwise.
|
|
391
|
-
*/
|
|
392
|
-
async validateAccessToken(
|
|
393
|
-
token: string,
|
|
394
|
-
options?: TokenValidationOptions
|
|
395
|
-
): Promise<boolean> {
|
|
396
|
-
try {
|
|
397
|
-
await this.validateToken(token, options);
|
|
398
|
-
return true;
|
|
399
|
-
} catch (_) {
|
|
400
|
-
return false;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Returns the logout URL that can be used to log out the user.
|
|
406
|
-
* @param {LogoutUrlOptions} options Logout URL options
|
|
407
|
-
* @param {string} options.idTokenHint The ID Token previously issued to the client
|
|
408
|
-
* @param {string} options.postLogoutRedirectUri URL to redirect after logout
|
|
409
|
-
* @param {string} options.state Opaque value to maintain state between request and callback
|
|
410
|
-
* @returns {string} The logout URL
|
|
411
|
-
*
|
|
412
|
-
* @example
|
|
413
|
-
* const scalekit = new Scalekit(envUrl, clientId, clientSecret);
|
|
414
|
-
* const logoutUrl = scalekit.getLogoutUrl({
|
|
415
|
-
* postLogoutRedirectUri: 'https://example.com',
|
|
416
|
-
* state: 'some-state'
|
|
417
|
-
* });
|
|
418
|
-
*/
|
|
419
|
-
getLogoutUrl(options?: LogoutUrlOptions): string {
|
|
420
|
-
const qs = QueryString.stringify({
|
|
421
|
-
...(options?.idTokenHint && { id_token_hint: options.idTokenHint }),
|
|
422
|
-
...(options?.postLogoutRedirectUri && {
|
|
423
|
-
post_logout_redirect_uri: options.postLogoutRedirectUri,
|
|
424
|
-
}),
|
|
425
|
-
...(options?.state && { state: options.state }),
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
return `${this.coreClient.envUrl}/${logoutEndpoint}${qs ? `?${qs}` : ""}`;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Verifies the authenticity and integrity of webhook payloads from Scalekit.
|
|
433
|
-
*
|
|
434
|
-
* Use this method to validate webhook requests from Scalekit by verifying the HMAC signature.
|
|
435
|
-
* This ensures the webhook was sent by Scalekit and hasn't been tampered with. The method
|
|
436
|
-
* checks the signature and timestamp to prevent replay attacks (5-minute tolerance window).
|
|
437
|
-
*
|
|
438
|
-
* @param {string} secret - Your webhook signing secret from the Scalekit dashboard (format: 'whsec_...')
|
|
439
|
-
* @param {Record<string, string>} headers - The HTTP headers from the webhook request
|
|
440
|
-
* @param {string} payload - The raw webhook request body as a string
|
|
441
|
-
*
|
|
442
|
-
* @returns {boolean} Returns true if the webhook signature is valid
|
|
443
|
-
*
|
|
444
|
-
* @throws {WebhookVerificationError} When required headers are missing
|
|
445
|
-
* @throws {WebhookVerificationError} When the secret format is invalid
|
|
446
|
-
* @throws {WebhookVerificationError} When the signature doesn't match
|
|
447
|
-
* @throws {WebhookVerificationError} When the timestamp is too old (>5 minutes) or in the future
|
|
448
|
-
*
|
|
449
|
-
* @example
|
|
450
|
-
* // Express.js webhook handler
|
|
451
|
-
* app.post('/webhooks/scalekit', express.raw({ type: 'application/json' }), (req, res) => {
|
|
452
|
-
* const secret = process.env.SCALEKIT_WEBHOOK_SECRET;
|
|
453
|
-
* const headers = req.headers;
|
|
454
|
-
* const payload = req.body.toString();
|
|
455
|
-
*
|
|
456
|
-
* try {
|
|
457
|
-
* const isValid = scalekitClient.verifyWebhookPayload(secret, headers, payload);
|
|
458
|
-
*
|
|
459
|
-
* if (isValid) {
|
|
460
|
-
* const event = JSON.parse(payload);
|
|
461
|
-
*
|
|
462
|
-
* // Process the webhook event
|
|
463
|
-
* switch (event.type) {
|
|
464
|
-
* case 'user.created':
|
|
465
|
-
* console.log('New user created:', event.data);
|
|
466
|
-
* break;
|
|
467
|
-
* case 'connection.enabled':
|
|
468
|
-
* console.log('Connection enabled:', event.data);
|
|
469
|
-
* break;
|
|
470
|
-
* }
|
|
471
|
-
*
|
|
472
|
-
* res.status(200).send('Webhook received');
|
|
473
|
-
* }
|
|
474
|
-
* } catch (error) {
|
|
475
|
-
* console.error('Webhook verification failed:', error);
|
|
476
|
-
* res.status(400).send('Invalid webhook signature');
|
|
477
|
-
* }
|
|
478
|
-
* });
|
|
479
|
-
*
|
|
480
|
-
* @see {@link https://docs.scalekit.com/reference/webhooks/overview/ | Webhook Documentation}
|
|
481
|
-
* @see {@link verifyInterceptorPayload} - Similar method for interceptor payloads
|
|
482
|
-
*/
|
|
483
|
-
verifyWebhookPayload(
|
|
484
|
-
secret: string,
|
|
485
|
-
headers: Record<string, string>,
|
|
486
|
-
payload: string
|
|
487
|
-
): boolean {
|
|
488
|
-
const webhookId = headers["webhook-id"];
|
|
489
|
-
const webhookTimestamp = headers["webhook-timestamp"];
|
|
490
|
-
const webhookSignature = headers["webhook-signature"];
|
|
491
|
-
|
|
492
|
-
return this.verifyPayloadSignature(
|
|
493
|
-
secret,
|
|
494
|
-
webhookId,
|
|
495
|
-
webhookTimestamp,
|
|
496
|
-
webhookSignature,
|
|
497
|
-
payload
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Verifies the authenticity and integrity of interceptor payloads from Scalekit.
|
|
503
|
-
*
|
|
504
|
-
* Use this method to validate HTTP interceptor requests from Scalekit by verifying the HMAC signature.
|
|
505
|
-
* This ensures the interceptor payload was sent by Scalekit and hasn't been tampered with. The method
|
|
506
|
-
* checks the signature and timestamp to prevent replay attacks (5-minute tolerance window)
|
|
507
|
-
*
|
|
508
|
-
* @param {string} secret Your interceptor signing secret that you can copy from Scalekit Dashboard
|
|
509
|
-
* @param {Record<string, string>} headers The HTTP headers from the interceptor request
|
|
510
|
-
* @param {string} payload The raw interceptor request body as a string
|
|
511
|
-
* @return {boolean} Returns true if the interceptor payload is valid.
|
|
512
|
-
*/
|
|
513
|
-
verifyInterceptorPayload(
|
|
514
|
-
secret: string,
|
|
515
|
-
headers: Record<string, string>,
|
|
516
|
-
payload: string
|
|
517
|
-
): boolean {
|
|
518
|
-
const interceptorId = headers["interceptor-id"];
|
|
519
|
-
const interceptorTimestamp = headers["interceptor-timestamp"];
|
|
520
|
-
const interceptorSignature = headers["interceptor-signature"];
|
|
521
|
-
|
|
522
|
-
return this.verifyPayloadSignature(
|
|
523
|
-
secret,
|
|
524
|
-
interceptorId,
|
|
525
|
-
interceptorTimestamp,
|
|
526
|
-
interceptorSignature,
|
|
527
|
-
payload
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Common payload signature verification logic
|
|
533
|
-
*
|
|
534
|
-
* @param {string} secret The secret
|
|
535
|
-
* @param {string} id The webhook/interceptor id
|
|
536
|
-
* @param {string} timestamp The timestamp
|
|
537
|
-
* @param {string} signature The signature
|
|
538
|
-
* @param {string} payload The payload
|
|
539
|
-
* @return {boolean} Returns true if the payload signature is valid.
|
|
540
|
-
*/
|
|
541
|
-
private verifyPayloadSignature(
|
|
542
|
-
secret: string,
|
|
543
|
-
id: string,
|
|
544
|
-
timestamp: string,
|
|
545
|
-
signature: string,
|
|
546
|
-
payload: string
|
|
547
|
-
): boolean {
|
|
548
|
-
if (!id || !timestamp || !signature) {
|
|
549
|
-
throw new WebhookVerificationError("Missing required headers");
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const secretParts = secret.split("_");
|
|
553
|
-
if (secretParts.length < 2) {
|
|
554
|
-
throw new WebhookVerificationError("Invalid secret");
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
try {
|
|
558
|
-
const timestampDate = this.verifyTimestamp(timestamp);
|
|
559
|
-
const data = `${id}.${Math.floor(
|
|
560
|
-
timestampDate.getTime() / 1000
|
|
561
|
-
)}.${payload}`;
|
|
562
|
-
const secretBytes = Buffer.from(secretParts[1], "base64");
|
|
563
|
-
const computedSignature = this.computeSignature(secretBytes, data);
|
|
564
|
-
const receivedSignatures = signature.split(" ");
|
|
565
|
-
|
|
566
|
-
for (const versionedSignature of receivedSignatures) {
|
|
567
|
-
const [version, receivedSignature] = versionedSignature.split(",");
|
|
568
|
-
if (version !== WEBHOOK_SIGNATURE_VERSION) {
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
if (
|
|
572
|
-
crypto.timingSafeEqual(
|
|
573
|
-
Buffer.from(receivedSignature, "base64"),
|
|
574
|
-
Buffer.from(computedSignature, "base64")
|
|
575
|
-
)
|
|
576
|
-
) {
|
|
577
|
-
return true;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
throw new WebhookVerificationError("Invalid Signature");
|
|
582
|
-
} catch (error) {
|
|
583
|
-
if (error instanceof WebhookVerificationError) {
|
|
584
|
-
throw error;
|
|
585
|
-
}
|
|
586
|
-
throw new WebhookVerificationError("Invalid Signature");
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Validates a token and returns the claims as json payload if valid.
|
|
592
|
-
* Supports issuer, audience, and scope validation.
|
|
593
|
-
*
|
|
594
|
-
* @param {string} token The token to be validated
|
|
595
|
-
* @param {TokenValidationOptions} options Optional validation options for issuer, audience, and scopes
|
|
596
|
-
* @return {Promise<T>} Returns the token payload if valid
|
|
597
|
-
* @throws {ScalekitValidateTokenFailureException} If token is invalid or missing required scopes
|
|
598
|
-
*/
|
|
599
|
-
async validateToken<T>(
|
|
600
|
-
token: string,
|
|
601
|
-
options?: TokenValidationOptions
|
|
602
|
-
): Promise<T> {
|
|
603
|
-
await this.coreClient.getJwks();
|
|
604
|
-
const jwks = jose.createLocalJWKSet({
|
|
605
|
-
keys: this.coreClient.keys,
|
|
606
|
-
});
|
|
607
|
-
try {
|
|
608
|
-
const { payload } = await jose.jwtVerify<T>(token, jwks, {
|
|
609
|
-
...(options?.issuer && { issuer: options.issuer }),
|
|
610
|
-
...(options?.audience && { audience: options.audience }),
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
if (options?.requiredScopes && options.requiredScopes.length > 0) {
|
|
614
|
-
this.verifyScopes(token, options.requiredScopes);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
return payload;
|
|
618
|
-
} catch (error) {
|
|
619
|
-
throw new ScalekitValidateTokenFailureException(error);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Verify that the token contains the required scopes
|
|
625
|
-
*
|
|
626
|
-
* @param {string} token The token to verify
|
|
627
|
-
* @param {string[]} requiredScopes The scopes that must be present in the token
|
|
628
|
-
* @return {boolean} Returns true if all required scopes are present
|
|
629
|
-
* @throws {ScalekitValidateTokenFailureException} If required scopes are missing, with details about which scopes are missing
|
|
630
|
-
*/
|
|
631
|
-
verifyScopes(token: string, requiredScopes: string[]): boolean {
|
|
632
|
-
const payload = jose.decodeJwt(token);
|
|
633
|
-
const scopes = this.extractScopesFromPayload(payload);
|
|
634
|
-
|
|
635
|
-
const missingScopes = requiredScopes.filter(
|
|
636
|
-
(scope) => !scopes.includes(scope)
|
|
637
|
-
);
|
|
638
|
-
|
|
639
|
-
if (missingScopes.length > 0) {
|
|
640
|
-
throw new ScalekitValidateTokenFailureException(
|
|
641
|
-
`Token missing required scopes: ${missingScopes.join(", ")}`
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
return true;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Extract scopes from token payload
|
|
650
|
-
*
|
|
651
|
-
* @param {any} payload The token payload
|
|
652
|
-
* @return {string[]} Array of scopes found in the token
|
|
653
|
-
*/
|
|
654
|
-
private extractScopesFromPayload(payload: Record<string, any>): string[] {
|
|
655
|
-
const scopes = payload.scopes;
|
|
656
|
-
return Array.isArray(scopes)
|
|
657
|
-
? scopes.filter((scope) => !!scope.trim?.())
|
|
658
|
-
: [];
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* Verify the timestamp
|
|
663
|
-
*
|
|
664
|
-
* @param {string} timestampStr The timestamp string
|
|
665
|
-
* @return {Date} Returns the timestamp
|
|
666
|
-
*/
|
|
667
|
-
private verifyTimestamp(timestampStr: string): Date {
|
|
668
|
-
const now = Math.floor(Date.now() / 1000);
|
|
669
|
-
const timestamp = parseInt(timestampStr, 10);
|
|
670
|
-
if (isNaN(timestamp)) {
|
|
671
|
-
throw new WebhookVerificationError("Invalid Signature Headers");
|
|
672
|
-
}
|
|
673
|
-
if (now - timestamp > WEBHOOK_TOLERANCE_IN_SECONDS) {
|
|
674
|
-
throw new WebhookVerificationError("Message timestamp too old");
|
|
675
|
-
}
|
|
676
|
-
if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) {
|
|
677
|
-
throw new WebhookVerificationError("Message timestamp too new");
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
return new Date(timestamp * 1000);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* Compute the signature
|
|
685
|
-
*
|
|
686
|
-
* @param {Buffer} secretBytes The secret bytes
|
|
687
|
-
* @param {string} data The data to be signed
|
|
688
|
-
* @return {string} Returns the signature
|
|
689
|
-
*/
|
|
690
|
-
private computeSignature(secretBytes: Buffer, data: string): string {
|
|
691
|
-
return crypto
|
|
692
|
-
.createHmac("sha256", secretBytes)
|
|
693
|
-
.update(data)
|
|
694
|
-
.digest("base64");
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Obtains a new access token using a refresh token.
|
|
699
|
-
*
|
|
700
|
-
* Use this method to get a new access token when the current one expires, without requiring
|
|
701
|
-
* the user to re-authenticate. This implements the OAuth 2.0 refresh token grant type.
|
|
702
|
-
* The method returns both a new access token and a new refresh token (token rotation).
|
|
703
|
-
*
|
|
704
|
-
* @param {string} refreshToken - The refresh token obtained from a previous authentication
|
|
705
|
-
*
|
|
706
|
-
* @returns {Promise<RefreshTokenResponse>} Response containing:
|
|
707
|
-
* - accessToken: New access token for API authorization
|
|
708
|
-
* - refreshToken: New refresh token (the old one is invalidated)
|
|
709
|
-
*
|
|
710
|
-
* @throws {Error} When the refresh token is missing
|
|
711
|
-
* @throws {Error} When the refresh token is invalid, expired, or revoked
|
|
712
|
-
* @throws {Error} When the authentication server response is invalid
|
|
713
|
-
*
|
|
714
|
-
* @example
|
|
715
|
-
* // Refresh tokens before they expire
|
|
716
|
-
* async function refreshUserToken(userId) {
|
|
717
|
-
* try {
|
|
718
|
-
* const oldRefreshToken = await getStoredRefreshToken(userId);
|
|
719
|
-
*
|
|
720
|
-
* const result = await scalekitClient.refreshAccessToken(oldRefreshToken);
|
|
721
|
-
*
|
|
722
|
-
* // Store the new tokens (old refresh token is now invalid)
|
|
723
|
-
* await storeTokens(userId, {
|
|
724
|
-
* accessToken: result.accessToken,
|
|
725
|
-
* refreshToken: result.refreshToken
|
|
726
|
-
* });
|
|
727
|
-
*
|
|
728
|
-
* return result.accessToken;
|
|
729
|
-
* } catch (error) {
|
|
730
|
-
* console.error('Token refresh failed:', error);
|
|
731
|
-
* // Redirect user to login
|
|
732
|
-
* throw new Error('Please log in again');
|
|
733
|
-
* }
|
|
734
|
-
* }
|
|
735
|
-
*
|
|
736
|
-
* @example
|
|
737
|
-
* // Automatic token refresh middleware
|
|
738
|
-
* app.use(async (req, res, next) => {
|
|
739
|
-
* const accessToken = req.session.accessToken;
|
|
740
|
-
* const refreshToken = req.session.refreshToken;
|
|
741
|
-
*
|
|
742
|
-
* // Check if access token is expired (decode JWT and check exp claim)
|
|
743
|
-
* if (isTokenExpired(accessToken) && refreshToken) {
|
|
744
|
-
* try {
|
|
745
|
-
* const result = await scalekitClient.refreshAccessToken(refreshToken);
|
|
746
|
-
* req.session.accessToken = result.accessToken;
|
|
747
|
-
* req.session.refreshToken = result.refreshToken;
|
|
748
|
-
* } catch (error) {
|
|
749
|
-
* return res.redirect('/login');
|
|
750
|
-
* }
|
|
751
|
-
* }
|
|
752
|
-
* next();
|
|
753
|
-
* });
|
|
754
|
-
*
|
|
755
|
-
*/
|
|
756
|
-
async refreshAccessToken(
|
|
757
|
-
refreshToken: string
|
|
758
|
-
): Promise<RefreshTokenResponse> {
|
|
759
|
-
if (!refreshToken) {
|
|
760
|
-
throw new Error("Refresh token is required");
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
let res;
|
|
764
|
-
try {
|
|
765
|
-
res = await this.coreClient.authenticate(
|
|
766
|
-
QueryString.stringify({
|
|
767
|
-
grant_type: GrantType.RefreshToken,
|
|
768
|
-
client_id: this.coreClient.clientId,
|
|
769
|
-
client_secret: this.coreClient.clientSecret,
|
|
770
|
-
refresh_token: refreshToken,
|
|
771
|
-
})
|
|
772
|
-
);
|
|
773
|
-
} catch (error) {
|
|
774
|
-
throw new Error(
|
|
775
|
-
`Failed to refresh token: ${
|
|
776
|
-
error instanceof Error ? error.message : "Unknown error"
|
|
777
|
-
}`
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (!res || !res.data) {
|
|
782
|
-
throw new Error("Invalid response from authentication server");
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
const { access_token, refresh_token } = res.data;
|
|
786
|
-
|
|
787
|
-
// Validate that all required properties exist
|
|
788
|
-
if (!access_token) {
|
|
789
|
-
throw new Error("Missing access_token in authentication response");
|
|
790
|
-
}
|
|
791
|
-
if (!refresh_token) {
|
|
792
|
-
throw new Error("Missing refresh_token in authentication response");
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
return {
|
|
796
|
-
accessToken: access_token,
|
|
797
|
-
refreshToken: refresh_token,
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
}
|