@logto/core-kit 2.6.1 → 2.7.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/lib/openid.d.ts +49 -14
- package/lib/openid.js +66 -31
- package/lib/regex.d.ts +13 -0
- package/lib/regex.js +15 -0
- package/lib/utils/url.js +82 -0
- package/package.json +6 -6
package/lib/openid.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export declare enum ReservedResource {
|
|
|
13
13
|
*/
|
|
14
14
|
Organization = "urn:logto:resource:organizations"
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* All extended claims for ID token that are controlled by tenant configuration.
|
|
18
|
+
* This is the single source of truth for which claims can be toggled on/off in ID tokens.
|
|
19
|
+
*/
|
|
20
|
+
export declare const extendedIdTokenClaims: readonly ["custom_data", "identities", "sso_identities", "roles", "organizations", "organization_data", "organization_roles"];
|
|
21
|
+
export type ExtendedIdTokenClaim = (typeof extendedIdTokenClaims)[number];
|
|
16
22
|
/**
|
|
17
23
|
* A comprehensive list of all available user claims that can be used in SAML applications.
|
|
18
24
|
* This array serves two purposes:
|
|
@@ -26,11 +32,11 @@ export declare enum ReservedResource {
|
|
|
26
32
|
* Note: This array must include ALL possible values from `UserClaim` type.
|
|
27
33
|
* TypeScript will throw error if any value is missing.
|
|
28
34
|
*/
|
|
29
|
-
export declare const userClaimsList: readonly ["name", "given_name", "family_name", "middle_name", "nickname", "preferred_username", "profile", "picture", "website", "email", "email_verified", "gender", "birthdate", "zoneinfo", "locale", "phone_number", "phone_number_verified", "address", "updated_at", "username", "
|
|
35
|
+
export declare const userClaimsList: readonly ["name", "given_name", "family_name", "middle_name", "nickname", "preferred_username", "profile", "picture", "website", "email", "email_verified", "gender", "birthdate", "zoneinfo", "locale", "phone_number", "phone_number_verified", "address", "updated_at", "username", "created_at", "custom_data", "identities", "sso_identities", "roles", "organizations", "organization_data", "organization_roles"];
|
|
30
36
|
/**
|
|
31
37
|
* Zod guard for `UserClaim` type, using `userClaimsList` as the single source of truth
|
|
32
38
|
*/
|
|
33
|
-
export declare const userClaimGuard: z.ZodEnum<["name", "given_name", "family_name", "middle_name", "nickname", "preferred_username", "profile", "picture", "website", "email", "email_verified", "gender", "birthdate", "zoneinfo", "locale", "phone_number", "phone_number_verified", "address", "updated_at", "username", "
|
|
39
|
+
export declare const userClaimGuard: z.ZodEnum<["name", "given_name", "family_name", "middle_name", "nickname", "preferred_username", "profile", "picture", "website", "email", "email_verified", "gender", "birthdate", "zoneinfo", "locale", "phone_number", "phone_number_verified", "address", "updated_at", "username", "created_at", "custom_data", "identities", "sso_identities", "roles", "organizations", "organization_data", "organization_roles"]>;
|
|
34
40
|
export type UserClaim = z.infer<typeof userClaimGuard>;
|
|
35
41
|
/**
|
|
36
42
|
* Scopes for ID Token and Userinfo Endpoint.
|
|
@@ -39,68 +45,97 @@ export declare enum UserScope {
|
|
|
39
45
|
/**
|
|
40
46
|
* Scope for basic user info.
|
|
41
47
|
*
|
|
42
|
-
* See {@link
|
|
48
|
+
* See {@link userClaims} for mapped claims.
|
|
43
49
|
*/
|
|
44
50
|
Profile = "profile",
|
|
45
51
|
/**
|
|
46
52
|
* Scope for user email address.
|
|
47
53
|
*
|
|
48
|
-
* See {@link
|
|
54
|
+
* See {@link userClaims} for mapped claims.
|
|
49
55
|
*/
|
|
50
56
|
Email = "email",
|
|
51
57
|
/**
|
|
52
58
|
* Scope for user phone number.
|
|
53
59
|
*
|
|
54
|
-
* See {@link
|
|
60
|
+
* See {@link userClaims} for mapped claims.
|
|
55
61
|
*/
|
|
56
62
|
Phone = "phone",
|
|
57
63
|
/**
|
|
58
64
|
* Scope for user address.
|
|
59
65
|
*
|
|
60
|
-
* See {@link
|
|
66
|
+
* See {@link userClaims} for mapped claims.
|
|
61
67
|
*/
|
|
62
68
|
Address = "address",
|
|
63
69
|
/**
|
|
64
70
|
* Scope for user's custom data.
|
|
65
71
|
*
|
|
66
|
-
* See {@link
|
|
72
|
+
* See {@link userClaims} for mapped claims.
|
|
67
73
|
*/
|
|
68
74
|
CustomData = "custom_data",
|
|
69
75
|
/**
|
|
70
76
|
* Scope for user's social and SSO identity details.
|
|
71
77
|
*
|
|
72
|
-
* See {@link
|
|
78
|
+
* See {@link userClaims} for mapped claims.
|
|
73
79
|
*/
|
|
74
80
|
Identities = "identities",
|
|
75
81
|
/**
|
|
76
82
|
* Scope for user's roles.
|
|
77
83
|
*
|
|
78
|
-
* See {@link
|
|
84
|
+
* See {@link userClaims} for mapped claims.
|
|
79
85
|
*/
|
|
80
86
|
Roles = "roles",
|
|
81
87
|
/**
|
|
82
88
|
* Scope for user's organization IDs and perform organization token grant per [RFC 0001](https://github.com/logto-io/rfcs).
|
|
83
89
|
*
|
|
84
|
-
* See {@link
|
|
90
|
+
* See {@link userClaims} for mapped claims.
|
|
85
91
|
*/
|
|
86
92
|
Organizations = "urn:logto:scope:organizations",
|
|
87
93
|
/**
|
|
88
94
|
* Scope for user's organization roles per [RFC 0001](https://github.com/logto-io/rfcs).
|
|
89
95
|
*
|
|
90
|
-
* See {@link
|
|
96
|
+
* See {@link userClaims} for mapped claims.
|
|
97
|
+
*/
|
|
98
|
+
OrganizationRoles = "urn:logto:scope:organization_roles",
|
|
99
|
+
/**
|
|
100
|
+
* Scope for user's sessions.
|
|
101
|
+
*
|
|
102
|
+
* Only used for session management via account API.
|
|
103
|
+
* Not included in user claims, even when the scope is requested, as it's not meant for ID token or userinfo endpoint.
|
|
91
104
|
*/
|
|
92
|
-
|
|
105
|
+
Sessions = "urn:logto:scope:sessions"
|
|
93
106
|
}
|
|
94
107
|
/**
|
|
95
108
|
* Mapped claims that ID Token includes.
|
|
96
109
|
*
|
|
97
110
|
* @see {@link https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims | OpenID Connect Core 1.0} for standard scope - claim mapping.
|
|
111
|
+
*
|
|
112
|
+
* Note: For scopes `Roles`, `Organizations`, `OrganizationRoles`, `CustomData`, and `Identities`,
|
|
113
|
+
* the claims are configured via `extendedIdTokenClaimsByScope` and are controlled by tenant settings.
|
|
98
114
|
*/
|
|
99
115
|
export declare const idTokenClaims: Readonly<Record<UserScope, UserClaim[]>>;
|
|
100
116
|
/**
|
|
101
|
-
*
|
|
117
|
+
* Extended claims for ID token grouped by scope, controlled by tenant configuration.
|
|
118
|
+
* These claims can be enabled or disabled in the ID token via tenant settings.
|
|
119
|
+
*
|
|
120
|
+
* @see {@link extendedIdTokenClaims} for the full list of extended claims.
|
|
121
|
+
* @see {@link idTokenClaims} for base claims always included in ID token.
|
|
122
|
+
* @see {@link userClaims} for all possible claims (used by userinfo endpoint).
|
|
123
|
+
*/
|
|
124
|
+
export declare const extendedIdTokenClaimsByScope: Readonly<Partial<Record<UserScope, ExtendedIdTokenClaim[]>>>;
|
|
125
|
+
/**
|
|
126
|
+
* All possible claims for each scope, combining base ID token claims and extended claims.
|
|
127
|
+
*
|
|
128
|
+
* This mapping is used for:
|
|
129
|
+
* - OIDC provider claim configuration (to tell the provider which claims are available for each
|
|
130
|
+
* scope)
|
|
131
|
+
* - Userinfo endpoint (always returns all claims regardless of tenant configuration)
|
|
132
|
+
* - SAML application attribute mapping (to determine which scope to request based on required
|
|
133
|
+
* claims)
|
|
134
|
+
*
|
|
135
|
+
* Note: The actual claims returned in ID tokens are controlled by tenant configuration via
|
|
136
|
+
* {@link extendedIdTokenClaimsByScope}. See `getAcceptedUserClaims` in core for the filtering
|
|
137
|
+
* logic.
|
|
102
138
|
*/
|
|
103
|
-
export declare const userinfoClaims: Readonly<Record<UserScope, UserClaim[]>>;
|
|
104
139
|
export declare const userClaims: Readonly<Record<UserScope, UserClaim[]>>;
|
|
105
140
|
/**
|
|
106
141
|
* The prefix of the URN (Uniform Resource Name) for the organization in Logto.
|
package/lib/openid.js
CHANGED
|
@@ -15,6 +15,19 @@ export var ReservedResource;
|
|
|
15
15
|
*/
|
|
16
16
|
ReservedResource["Organization"] = "urn:logto:resource:organizations";
|
|
17
17
|
})(ReservedResource || (ReservedResource = {}));
|
|
18
|
+
/**
|
|
19
|
+
* All extended claims for ID token that are controlled by tenant configuration.
|
|
20
|
+
* This is the single source of truth for which claims can be toggled on/off in ID tokens.
|
|
21
|
+
*/
|
|
22
|
+
export const extendedIdTokenClaims = [
|
|
23
|
+
'custom_data',
|
|
24
|
+
'identities',
|
|
25
|
+
'sso_identities',
|
|
26
|
+
'roles',
|
|
27
|
+
'organizations',
|
|
28
|
+
'organization_data',
|
|
29
|
+
'organization_roles',
|
|
30
|
+
];
|
|
18
31
|
/**
|
|
19
32
|
* A comprehensive list of all available user claims that can be used in SAML applications.
|
|
20
33
|
* This array serves two purposes:
|
|
@@ -50,15 +63,9 @@ export const userClaimsList = [
|
|
|
50
63
|
'address',
|
|
51
64
|
'updated_at',
|
|
52
65
|
// Custom claims
|
|
53
|
-
'username',
|
|
54
|
-
'
|
|
55
|
-
|
|
56
|
-
'organization_data',
|
|
57
|
-
'organization_roles',
|
|
58
|
-
'custom_data',
|
|
59
|
-
'identities',
|
|
60
|
-
'sso_identities',
|
|
61
|
-
'created_at',
|
|
66
|
+
'username', // Planned to be migrated into OIDC standard `preferred_username`, not configurable for now.
|
|
67
|
+
'created_at', // Follows the profile scope convention (always included). Not configurable for now, may change in the future.
|
|
68
|
+
...extendedIdTokenClaims,
|
|
62
69
|
];
|
|
63
70
|
/**
|
|
64
71
|
* Zod guard for `UserClaim` type, using `userClaimsList` as the single source of truth
|
|
@@ -72,62 +79,72 @@ export var UserScope;
|
|
|
72
79
|
/**
|
|
73
80
|
* Scope for basic user info.
|
|
74
81
|
*
|
|
75
|
-
* See {@link
|
|
82
|
+
* See {@link userClaims} for mapped claims.
|
|
76
83
|
*/
|
|
77
84
|
UserScope["Profile"] = "profile";
|
|
78
85
|
/**
|
|
79
86
|
* Scope for user email address.
|
|
80
87
|
*
|
|
81
|
-
* See {@link
|
|
88
|
+
* See {@link userClaims} for mapped claims.
|
|
82
89
|
*/
|
|
83
90
|
UserScope["Email"] = "email";
|
|
84
91
|
/**
|
|
85
92
|
* Scope for user phone number.
|
|
86
93
|
*
|
|
87
|
-
* See {@link
|
|
94
|
+
* See {@link userClaims} for mapped claims.
|
|
88
95
|
*/
|
|
89
96
|
UserScope["Phone"] = "phone";
|
|
90
97
|
/**
|
|
91
98
|
* Scope for user address.
|
|
92
99
|
*
|
|
93
|
-
* See {@link
|
|
100
|
+
* See {@link userClaims} for mapped claims.
|
|
94
101
|
*/
|
|
95
102
|
UserScope["Address"] = "address";
|
|
96
103
|
/**
|
|
97
104
|
* Scope for user's custom data.
|
|
98
105
|
*
|
|
99
|
-
* See {@link
|
|
106
|
+
* See {@link userClaims} for mapped claims.
|
|
100
107
|
*/
|
|
101
108
|
UserScope["CustomData"] = "custom_data";
|
|
102
109
|
/**
|
|
103
110
|
* Scope for user's social and SSO identity details.
|
|
104
111
|
*
|
|
105
|
-
* See {@link
|
|
112
|
+
* See {@link userClaims} for mapped claims.
|
|
106
113
|
*/
|
|
107
114
|
UserScope["Identities"] = "identities";
|
|
108
115
|
/**
|
|
109
116
|
* Scope for user's roles.
|
|
110
117
|
*
|
|
111
|
-
* See {@link
|
|
118
|
+
* See {@link userClaims} for mapped claims.
|
|
112
119
|
*/
|
|
113
120
|
UserScope["Roles"] = "roles";
|
|
114
121
|
/**
|
|
115
122
|
* Scope for user's organization IDs and perform organization token grant per [RFC 0001](https://github.com/logto-io/rfcs).
|
|
116
123
|
*
|
|
117
|
-
* See {@link
|
|
124
|
+
* See {@link userClaims} for mapped claims.
|
|
118
125
|
*/
|
|
119
126
|
UserScope["Organizations"] = "urn:logto:scope:organizations";
|
|
120
127
|
/**
|
|
121
128
|
* Scope for user's organization roles per [RFC 0001](https://github.com/logto-io/rfcs).
|
|
122
129
|
*
|
|
123
|
-
* See {@link
|
|
130
|
+
* See {@link userClaims} for mapped claims.
|
|
124
131
|
*/
|
|
125
132
|
UserScope["OrganizationRoles"] = "urn:logto:scope:organization_roles";
|
|
133
|
+
/**
|
|
134
|
+
* Scope for user's sessions.
|
|
135
|
+
*
|
|
136
|
+
* Only used for session management via account API.
|
|
137
|
+
* Not included in user claims, even when the scope is requested, as it's not meant for ID token or userinfo endpoint.
|
|
138
|
+
*/
|
|
139
|
+
UserScope["Sessions"] = "urn:logto:scope:sessions";
|
|
126
140
|
})(UserScope || (UserScope = {}));
|
|
127
141
|
/**
|
|
128
142
|
* Mapped claims that ID Token includes.
|
|
129
143
|
*
|
|
130
144
|
* @see {@link https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims | OpenID Connect Core 1.0} for standard scope - claim mapping.
|
|
145
|
+
*
|
|
146
|
+
* Note: For scopes `Roles`, `Organizations`, `OrganizationRoles`, `CustomData`, and `Identities`,
|
|
147
|
+
* the claims are configured via `extendedIdTokenClaimsByScope` and are controlled by tenant settings.
|
|
131
148
|
*/
|
|
132
149
|
export const idTokenClaims = Object.freeze({
|
|
133
150
|
[UserScope.Profile]: [
|
|
@@ -153,32 +170,50 @@ export const idTokenClaims = Object.freeze({
|
|
|
153
170
|
[UserScope.Email]: ['email', 'email_verified'],
|
|
154
171
|
[UserScope.Phone]: ['phone_number', 'phone_number_verified'],
|
|
155
172
|
[UserScope.Address]: ['address'],
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
[UserScope.
|
|
173
|
+
// The following scopes have their claims controlled by tenant configuration
|
|
174
|
+
// via `extendedIdTokenClaimsByScope`. See below.
|
|
175
|
+
[UserScope.Roles]: [],
|
|
176
|
+
[UserScope.Organizations]: [],
|
|
177
|
+
[UserScope.OrganizationRoles]: [],
|
|
159
178
|
[UserScope.CustomData]: [],
|
|
160
179
|
[UserScope.Identities]: [],
|
|
180
|
+
[UserScope.Sessions]: [],
|
|
161
181
|
});
|
|
162
182
|
/**
|
|
163
|
-
*
|
|
183
|
+
* Extended claims for ID token grouped by scope, controlled by tenant configuration.
|
|
184
|
+
* These claims can be enabled or disabled in the ID token via tenant settings.
|
|
185
|
+
*
|
|
186
|
+
* @see {@link extendedIdTokenClaims} for the full list of extended claims.
|
|
187
|
+
* @see {@link idTokenClaims} for base claims always included in ID token.
|
|
188
|
+
* @see {@link userClaims} for all possible claims (used by userinfo endpoint).
|
|
164
189
|
*/
|
|
165
|
-
export const
|
|
166
|
-
[UserScope.Profile]: [],
|
|
167
|
-
[UserScope.Email]: [],
|
|
168
|
-
[UserScope.Phone]: [],
|
|
169
|
-
[UserScope.Address]: [],
|
|
170
|
-
[UserScope.Roles]: [],
|
|
171
|
-
[UserScope.Organizations]: ['organization_data'],
|
|
172
|
-
[UserScope.OrganizationRoles]: [],
|
|
190
|
+
export const extendedIdTokenClaimsByScope = Object.freeze({
|
|
173
191
|
[UserScope.CustomData]: ['custom_data'],
|
|
174
192
|
[UserScope.Identities]: ['identities', 'sso_identities'],
|
|
193
|
+
[UserScope.Roles]: ['roles'],
|
|
194
|
+
[UserScope.Organizations]: ['organizations', 'organization_data'],
|
|
195
|
+
[UserScope.OrganizationRoles]: ['organization_roles'],
|
|
175
196
|
});
|
|
197
|
+
/**
|
|
198
|
+
* All possible claims for each scope, combining base ID token claims and extended claims.
|
|
199
|
+
*
|
|
200
|
+
* This mapping is used for:
|
|
201
|
+
* - OIDC provider claim configuration (to tell the provider which claims are available for each
|
|
202
|
+
* scope)
|
|
203
|
+
* - Userinfo endpoint (always returns all claims regardless of tenant configuration)
|
|
204
|
+
* - SAML application attribute mapping (to determine which scope to request based on required
|
|
205
|
+
* claims)
|
|
206
|
+
*
|
|
207
|
+
* Note: The actual claims returned in ID tokens are controlled by tenant configuration via
|
|
208
|
+
* {@link extendedIdTokenClaimsByScope}. See `getAcceptedUserClaims` in core for the filtering
|
|
209
|
+
* logic.
|
|
210
|
+
*/
|
|
176
211
|
export const userClaims = Object.freeze(
|
|
177
212
|
// Hard to infer type directly, use `as` for a workaround.
|
|
178
213
|
// eslint-disable-next-line no-restricted-syntax
|
|
179
214
|
Object.fromEntries(Object.values(UserScope).map((current) => [
|
|
180
215
|
current,
|
|
181
|
-
[...idTokenClaims[current], ...
|
|
216
|
+
[...idTokenClaims[current], ...(extendedIdTokenClaimsByScope[current] ?? [])],
|
|
182
217
|
])));
|
|
183
218
|
/**
|
|
184
219
|
* The prefix of the URN (Uniform Resource Name) for the organization in Logto.
|
package/lib/regex.d.ts
CHANGED
|
@@ -12,3 +12,16 @@ export declare const noSpaceRegEx: RegExp;
|
|
|
12
12
|
/** Full domain that consists of at least 3 parts, e.g. foo.bar.com or example-foo.bar.com */
|
|
13
13
|
export declare const domainRegEx: RegExp;
|
|
14
14
|
export declare const numberAndAlphabetRegEx: RegExp;
|
|
15
|
+
/**
|
|
16
|
+
* Custom tenant ID validation rules.
|
|
17
|
+
* Used when creating tenants with custom IDs in private cloud regions.
|
|
18
|
+
*/
|
|
19
|
+
/** Maximum length for custom tenant ID */
|
|
20
|
+
export declare const customTenantIdMaxLength = 21;
|
|
21
|
+
/** Pattern for custom tenant ID: lowercase letters, numbers, and hyphens only */
|
|
22
|
+
export declare const customTenantIdRegEx: RegExp;
|
|
23
|
+
/**
|
|
24
|
+
* Validates a custom tenant ID format.
|
|
25
|
+
* @returns `true` if valid, `false` otherwise
|
|
26
|
+
*/
|
|
27
|
+
export declare const isValidCustomTenantId: (id: string) => boolean;
|
package/lib/regex.js
CHANGED
|
@@ -12,3 +12,18 @@ export const noSpaceRegEx = /^\S+$/;
|
|
|
12
12
|
/** Full domain that consists of at least 3 parts, e.g. foo.bar.com or example-foo.bar.com */
|
|
13
13
|
export const domainRegEx = /^[\dA-Za-z](?:[\dA-Za-z-]*[\dA-Za-z])?(?:\.[\dA-Za-z](?:[\dA-Za-z-]*[\dA-Za-z])?){2,}$/;
|
|
14
14
|
export const numberAndAlphabetRegEx = /^[\dA-Za-z]+$/;
|
|
15
|
+
/**
|
|
16
|
+
* Custom tenant ID validation rules.
|
|
17
|
+
* Used when creating tenants with custom IDs in private cloud regions.
|
|
18
|
+
*/
|
|
19
|
+
/** Maximum length for custom tenant ID */
|
|
20
|
+
export const customTenantIdMaxLength = 21;
|
|
21
|
+
/** Pattern for custom tenant ID: lowercase letters, numbers, and hyphens only */
|
|
22
|
+
export const customTenantIdRegEx = /^[\da-z-]+$/;
|
|
23
|
+
/**
|
|
24
|
+
* Validates a custom tenant ID format.
|
|
25
|
+
* @returns `true` if valid, `false` otherwise
|
|
26
|
+
*/
|
|
27
|
+
export const isValidCustomTenantId = (id) => {
|
|
28
|
+
return id.length <= customTenantIdMaxLength && customTenantIdRegEx.test(id);
|
|
29
|
+
};
|
package/lib/utils/url.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { mobileUriSchemeProtocolRegEx, webRedirectUriProtocolRegEx } from '../regex.js';
|
|
2
2
|
export const validateRedirectUrl = (url, type) => {
|
|
3
|
+
if (type === 'web' && url.includes('*')) {
|
|
4
|
+
return validateWildcardWebRedirectUrl(url);
|
|
5
|
+
}
|
|
6
|
+
if (type === 'web' && hasDotSegmentsInAbsoluteUrlPath(url)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
3
9
|
try {
|
|
4
10
|
const { protocol } = new URL(url);
|
|
5
11
|
const protocolRegEx = type === 'mobile' ? mobileUriSchemeProtocolRegEx : webRedirectUriProtocolRegEx;
|
|
@@ -9,6 +15,82 @@ export const validateRedirectUrl = (url, type) => {
|
|
|
9
15
|
return false;
|
|
10
16
|
}
|
|
11
17
|
};
|
|
18
|
+
const validateWildcardWebRedirectUrl = (url) => {
|
|
19
|
+
const schemeSeparatorIndex = url.indexOf('://');
|
|
20
|
+
if (schemeSeparatorIndex <= 0) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (hasWildcardInScheme(url, schemeSeparatorIndex)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (hasWildcardInQueryOrHash(url)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const authority = getAuthorityFromUrl(url, schemeSeparatorIndex);
|
|
30
|
+
if (!isAuthorityAllowedForWildcardWebRedirect(authority)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (hasDotSegmentsInAbsoluteUrlPath(url)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return isUrlProtocolAllowedAfterWildcardReplacement(url);
|
|
37
|
+
};
|
|
38
|
+
const hasWildcardInScheme = (url, schemeSeparatorIndex) => url.slice(0, schemeSeparatorIndex).includes('*');
|
|
39
|
+
const hasWildcardInQueryOrHash = (url) => {
|
|
40
|
+
// Disallow wildcards in query/hash to keep matching deterministic and safer.
|
|
41
|
+
const queryIndex = url.indexOf('?');
|
|
42
|
+
if (queryIndex >= 0 && url.slice(queryIndex).includes('*')) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
const hashIndex = url.indexOf('#');
|
|
46
|
+
return hashIndex >= 0 && url.slice(hashIndex).includes('*');
|
|
47
|
+
};
|
|
48
|
+
const getAuthorityFromUrl = (url, schemeSeparatorIndex) => url.slice(schemeSeparatorIndex + 3).split(/[#/?]/)[0] ?? '';
|
|
49
|
+
const hasDotSegmentsInAbsoluteUrlPath = (url) => {
|
|
50
|
+
const schemeSeparatorIndex = url.indexOf('://');
|
|
51
|
+
if (schemeSeparatorIndex <= 0) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const authority = getAuthorityFromUrl(url, schemeSeparatorIndex);
|
|
55
|
+
const afterAuthorityIndex = schemeSeparatorIndex + 3 + authority.length;
|
|
56
|
+
const rest = url.slice(afterAuthorityIndex);
|
|
57
|
+
const path = rest.split(/[#?]/)[0] ?? '';
|
|
58
|
+
if (!path) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const segments = path.split('/');
|
|
62
|
+
return segments.some((segment) => {
|
|
63
|
+
const normalized = segment.toLowerCase();
|
|
64
|
+
return segment === '.' || segment === '..' || normalized === '%2e' || normalized === '%2e%2e';
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
const isAuthorityAllowedForWildcardWebRedirect = (authority) => {
|
|
68
|
+
// Disallow credentials in authority part.
|
|
69
|
+
if (authority.includes('@')) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (authority.startsWith('[')) {
|
|
73
|
+
// IPv6 literals are not a typical use-case for wildcard redirect URIs; reject for simplicity.
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const lastColonIndex = authority.lastIndexOf(':');
|
|
77
|
+
const hasPort = lastColonIndex > -1 && authority.indexOf(':') === lastColonIndex;
|
|
78
|
+
const hostname = hasPort ? authority.slice(0, lastColonIndex) : authority;
|
|
79
|
+
// When wildcard is used in hostname, require at least one dot to avoid overly broad patterns.
|
|
80
|
+
if (hostname.includes('*') && !hostname.includes('.')) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return !(hasPort && authority.slice(lastColonIndex + 1).includes('*'));
|
|
84
|
+
};
|
|
85
|
+
const isUrlProtocolAllowedAfterWildcardReplacement = (url) => {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = new URL(url.replaceAll('*', 'wildcard'));
|
|
88
|
+
return webRedirectUriProtocolRegEx.test(parsed.protocol);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
12
94
|
export const validateUriOrigin = (url) => {
|
|
13
95
|
try {
|
|
14
96
|
return new URL(url).origin === url;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logto/core-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
5
5
|
"homepage": "https://github.com/logto-io/toolkit#readme",
|
|
6
6
|
"repository": {
|
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
"main": "./lib/index.js",
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
|
-
"default": "./lib/index.js",
|
|
16
15
|
"types": "./lib/index.d.ts",
|
|
17
|
-
"import": "./lib/index.js"
|
|
16
|
+
"import": "./lib/index.js",
|
|
17
|
+
"default": "./lib/index.js"
|
|
18
18
|
},
|
|
19
19
|
"./declaration": "./declaration/index.ts",
|
|
20
20
|
"./scss/*": "./scss/*.scss",
|
|
21
21
|
"./custom-jwt": {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
22
|
+
"types": "./lib/custom-jwt/index.d.ts",
|
|
23
|
+
"node": "./lib/custom-jwt/index.js"
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"types": "./lib/index.d.ts",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@silverhand/essentials": "^2.9.1",
|
|
37
37
|
"color": "^4.2.3",
|
|
38
38
|
"@logto/language-kit": "^1.2.0",
|
|
39
|
-
"@logto/shared": "^3.3.
|
|
39
|
+
"@logto/shared": "^3.3.1"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
42
|
"zod": "3.24.3"
|