@korajs/auth 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -28
- package/dist/chunk-7OXBRSJL.js +203 -0
- package/dist/chunk-7OXBRSJL.js.map +1 -0
- package/dist/create-org-session-RsDj9cl4.d.cts +722 -0
- package/dist/create-org-session-RsDj9cl4.d.ts +722 -0
- package/dist/index.cjs +1146 -586
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -118
- package/dist/index.d.ts +67 -118
- package/dist/index.js +654 -258
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +297 -183
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +25 -209
- package/dist/react.d.ts +25 -209
- package/dist/react.js +134 -209
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +1611 -730
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +415 -224
- package/dist/server.d.ts +415 -224
- package/dist/server.js +1565 -704
- package/dist/server.js.map +1 -1
- package/dist/svelte.cjs +512 -0
- package/dist/svelte.cjs.map +1 -0
- package/dist/svelte.d.cts +100 -0
- package/dist/svelte.d.ts +100 -0
- package/dist/svelte.js +278 -0
- package/dist/svelte.js.map +1 -0
- package/dist/vue.cjs +565 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.d.cts +136 -0
- package/dist/vue.d.ts +136 -0
- package/dist/vue.js +338 -0
- package/dist/vue.js.map +1 -0
- package/package.json +40 -6
- package/src/svelte/AuthProvider.svelte +37 -0
- package/src/svelte/OrgProvider.svelte +22 -0
- package/dist/org-client-BVTLKcIk.d.cts +0 -355
- package/dist/org-client-BVTLKcIk.d.ts +0 -355
package/dist/server.js
CHANGED
|
@@ -635,7 +635,7 @@ var BuiltInAuthRoutes = class {
|
|
|
635
635
|
const userStore = this.userStore;
|
|
636
636
|
return {
|
|
637
637
|
async authenticate(token) {
|
|
638
|
-
const payload = tokenManager.
|
|
638
|
+
const payload = await tokenManager.validateTokenWithRevocation(token);
|
|
639
639
|
if (payload === null || payload.type !== "access") {
|
|
640
640
|
return null;
|
|
641
641
|
}
|
|
@@ -661,6 +661,9 @@ var BuiltInAuthRoutes = class {
|
|
|
661
661
|
}
|
|
662
662
|
};
|
|
663
663
|
|
|
664
|
+
// src/provider/built-in/quickstart-server.ts
|
|
665
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
666
|
+
|
|
664
667
|
// src/tokens/token-manager.ts
|
|
665
668
|
import { randomBytes as randomBytes2, randomUUID } from "crypto";
|
|
666
669
|
|
|
@@ -773,7 +776,7 @@ var InMemoryTokenRevocationStore = class {
|
|
|
773
776
|
/**
|
|
774
777
|
* Check if a device has been revoked.
|
|
775
778
|
*/
|
|
776
|
-
isDeviceRevoked(deviceId) {
|
|
779
|
+
async isDeviceRevoked(deviceId) {
|
|
777
780
|
return this.revokedDevices.has(deviceId);
|
|
778
781
|
}
|
|
779
782
|
/**
|
|
@@ -966,7 +969,7 @@ var TokenManager = class {
|
|
|
966
969
|
* Validate a token and check it against the revocation store.
|
|
967
970
|
*
|
|
968
971
|
* Like {@link validateToken}, but also checks whether the token's `jti` has been
|
|
969
|
-
* revoked. Requires a revocation store to be configured.
|
|
972
|
+
* revoked or belongs to a revoked device. Requires a revocation store to be configured.
|
|
970
973
|
*
|
|
971
974
|
* @param token - The JWT string to validate
|
|
972
975
|
* @returns The decoded {@link TokenPayload} if valid and not revoked, or null otherwise
|
|
@@ -981,6 +984,10 @@ var TokenManager = class {
|
|
|
981
984
|
if (revoked) {
|
|
982
985
|
return null;
|
|
983
986
|
}
|
|
987
|
+
const deviceRevoked = await this.revocationStore.isDeviceRevoked(payload.dev);
|
|
988
|
+
if (deviceRevoked) {
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
984
991
|
}
|
|
985
992
|
return payload;
|
|
986
993
|
}
|
|
@@ -1047,217 +1054,1000 @@ var TokenManager = class {
|
|
|
1047
1054
|
}
|
|
1048
1055
|
};
|
|
1049
1056
|
|
|
1050
|
-
// src/provider/
|
|
1057
|
+
// src/provider/oauth/linked-identity-store.ts
|
|
1058
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1051
1059
|
import { KoraError as KoraError2 } from "@korajs/core";
|
|
1052
|
-
var
|
|
1060
|
+
var DuplicateLinkedIdentityError = class extends KoraError2 {
|
|
1061
|
+
constructor(provider) {
|
|
1062
|
+
super(`This ${provider} account is already linked.`, "DUPLICATE_LINKED_IDENTITY", {
|
|
1063
|
+
provider
|
|
1064
|
+
});
|
|
1065
|
+
this.name = "DuplicateLinkedIdentityError";
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
var InMemoryLinkedIdentityStore = class {
|
|
1069
|
+
identitiesById = /* @__PURE__ */ new Map();
|
|
1070
|
+
identityIdByProvider = /* @__PURE__ */ new Map();
|
|
1071
|
+
identityIdsByUser = /* @__PURE__ */ new Map();
|
|
1072
|
+
identityIdByUserProvider = /* @__PURE__ */ new Map();
|
|
1073
|
+
async findByProvider(provider, providerUserId) {
|
|
1074
|
+
const id = this.identityIdByProvider.get(providerIdentityKey(provider, providerUserId));
|
|
1075
|
+
return id ? this.identitiesById.get(id) ?? null : null;
|
|
1076
|
+
}
|
|
1077
|
+
async findByUser(userId) {
|
|
1078
|
+
const ids = this.identityIdsByUser.get(userId);
|
|
1079
|
+
if (!ids) return [];
|
|
1080
|
+
return [...ids].map((id) => this.identitiesById.get(id)).filter((identity) => identity !== void 0);
|
|
1081
|
+
}
|
|
1082
|
+
async create(params) {
|
|
1083
|
+
const providerKey = providerIdentityKey(params.provider, params.providerUserId);
|
|
1084
|
+
const userProviderKeyValue = userProviderKey(params.userId, params.provider);
|
|
1085
|
+
if (this.identityIdByProvider.has(providerKey) || this.identityIdByUserProvider.has(userProviderKeyValue)) {
|
|
1086
|
+
throw new DuplicateLinkedIdentityError(params.provider);
|
|
1087
|
+
}
|
|
1088
|
+
const identity = {
|
|
1089
|
+
id: randomUUID2(),
|
|
1090
|
+
userId: params.userId,
|
|
1091
|
+
provider: params.provider,
|
|
1092
|
+
providerUserId: params.providerUserId,
|
|
1093
|
+
email: params.email,
|
|
1094
|
+
linkedAt: Date.now()
|
|
1095
|
+
};
|
|
1096
|
+
this.identitiesById.set(identity.id, identity);
|
|
1097
|
+
this.identityIdByProvider.set(providerKey, identity.id);
|
|
1098
|
+
this.identityIdByUserProvider.set(userProviderKeyValue, identity.id);
|
|
1099
|
+
const userIdentities = this.identityIdsByUser.get(params.userId) ?? /* @__PURE__ */ new Set();
|
|
1100
|
+
userIdentities.add(identity.id);
|
|
1101
|
+
this.identityIdsByUser.set(params.userId, userIdentities);
|
|
1102
|
+
return identity;
|
|
1103
|
+
}
|
|
1104
|
+
async delete(userId, provider) {
|
|
1105
|
+
const userProviderKeyValue = userProviderKey(userId, provider);
|
|
1106
|
+
const id = this.identityIdByUserProvider.get(userProviderKeyValue);
|
|
1107
|
+
if (!id) return;
|
|
1108
|
+
const identity = this.identitiesById.get(id);
|
|
1109
|
+
this.identitiesById.delete(id);
|
|
1110
|
+
this.identityIdByUserProvider.delete(userProviderKeyValue);
|
|
1111
|
+
if (identity) {
|
|
1112
|
+
this.identityIdByProvider.delete(
|
|
1113
|
+
providerIdentityKey(identity.provider, identity.providerUserId)
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
const userIdentities = this.identityIdsByUser.get(userId);
|
|
1117
|
+
userIdentities?.delete(id);
|
|
1118
|
+
if (userIdentities?.size === 0) {
|
|
1119
|
+
this.identityIdsByUser.delete(userId);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
function providerIdentityKey(provider, providerUserId) {
|
|
1124
|
+
return `${provider}:${providerUserId}`;
|
|
1125
|
+
}
|
|
1126
|
+
function userProviderKey(userId, provider) {
|
|
1127
|
+
return `${userId}:${provider}`;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/provider/oauth/oauth-types.ts
|
|
1131
|
+
import { KoraError as KoraError3 } from "@korajs/core";
|
|
1132
|
+
var OAuthError = class extends KoraError3 {
|
|
1053
1133
|
constructor(message, code, context) {
|
|
1054
1134
|
super(message, code, context);
|
|
1055
|
-
this.name = "
|
|
1135
|
+
this.name = "OAuthError";
|
|
1056
1136
|
}
|
|
1057
1137
|
};
|
|
1058
|
-
var
|
|
1138
|
+
var OAuthStateMismatchError = class extends OAuthError {
|
|
1059
1139
|
constructor() {
|
|
1060
|
-
super("
|
|
1140
|
+
super("OAuth state parameter does not match. Possible CSRF attack.", "OAUTH_STATE_MISMATCH");
|
|
1061
1141
|
}
|
|
1062
1142
|
};
|
|
1063
|
-
var
|
|
1064
|
-
constructor() {
|
|
1065
|
-
super(
|
|
1143
|
+
var OAuthCodeExchangeError = class extends OAuthError {
|
|
1144
|
+
constructor(details) {
|
|
1145
|
+
super(
|
|
1146
|
+
`Failed to exchange authorization code for tokens.${details ? ` ${details}` : ""}`,
|
|
1147
|
+
"OAUTH_CODE_EXCHANGE_FAILED",
|
|
1148
|
+
details ? { details } : void 0
|
|
1149
|
+
);
|
|
1066
1150
|
}
|
|
1067
1151
|
};
|
|
1068
|
-
var
|
|
1069
|
-
constructor() {
|
|
1070
|
-
super(
|
|
1152
|
+
var OAuthUserInfoError = class extends OAuthError {
|
|
1153
|
+
constructor(details) {
|
|
1154
|
+
super(
|
|
1155
|
+
`Failed to fetch user info from OAuth provider.${details ? ` ${details}` : ""}`,
|
|
1156
|
+
"OAUTH_USER_INFO_FAILED",
|
|
1157
|
+
details ? { details } : void 0
|
|
1158
|
+
);
|
|
1071
1159
|
}
|
|
1072
1160
|
};
|
|
1073
|
-
var
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
async get(token) {
|
|
1079
|
-
return this.tokens.get(token) ?? null;
|
|
1161
|
+
var OAuthProviderNotFoundError = class extends OAuthError {
|
|
1162
|
+
constructor(provider) {
|
|
1163
|
+
super(`OAuth provider "${provider}" is not configured.`, "OAUTH_PROVIDER_NOT_FOUND", {
|
|
1164
|
+
provider
|
|
1165
|
+
});
|
|
1080
1166
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
// src/provider/oauth/oauth-flow.ts
|
|
1170
|
+
var DEFAULT_STATE_TTL_MS = 10 * 60 * 1e3;
|
|
1171
|
+
var PKCE_VERIFIER_BYTES = 32;
|
|
1172
|
+
var InMemoryOAuthStateStore = class {
|
|
1173
|
+
states = /* @__PURE__ */ new Map();
|
|
1174
|
+
async store(state) {
|
|
1175
|
+
this.states.set(state.state, state);
|
|
1086
1176
|
}
|
|
1087
|
-
async
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
return count;
|
|
1177
|
+
async consume(stateValue) {
|
|
1178
|
+
const state = this.states.get(stateValue);
|
|
1179
|
+
if (!state) return null;
|
|
1180
|
+
this.states.delete(stateValue);
|
|
1181
|
+
if (Date.now() > state.expiresAt) return null;
|
|
1182
|
+
return state;
|
|
1096
1183
|
}
|
|
1097
1184
|
async cleanExpired() {
|
|
1098
1185
|
const now = Date.now();
|
|
1099
1186
|
let count = 0;
|
|
1100
|
-
for (const [key,
|
|
1101
|
-
if (now >
|
|
1102
|
-
this.
|
|
1187
|
+
for (const [key, state] of this.states) {
|
|
1188
|
+
if (now > state.expiresAt) {
|
|
1189
|
+
this.states.delete(key);
|
|
1103
1190
|
count++;
|
|
1104
1191
|
}
|
|
1105
1192
|
}
|
|
1106
1193
|
return count;
|
|
1107
1194
|
}
|
|
1108
1195
|
};
|
|
1109
|
-
var
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
userStore;
|
|
1115
|
-
resetStore;
|
|
1116
|
-
tokenTtlMs;
|
|
1117
|
-
maxRequestsPerEmail;
|
|
1118
|
-
onResetRequested;
|
|
1196
|
+
var OAuthManager = class {
|
|
1197
|
+
providers = /* @__PURE__ */ new Map();
|
|
1198
|
+
stateStore;
|
|
1199
|
+
stateTtlMs;
|
|
1200
|
+
fetchFn;
|
|
1119
1201
|
constructor(config) {
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
this.
|
|
1124
|
-
this.
|
|
1202
|
+
for (const provider of config.providers) {
|
|
1203
|
+
this.providers.set(provider.providerId, provider);
|
|
1204
|
+
}
|
|
1205
|
+
this.stateStore = config.stateStore ?? new InMemoryOAuthStateStore();
|
|
1206
|
+
this.stateTtlMs = config.stateTtlMs ?? DEFAULT_STATE_TTL_MS;
|
|
1207
|
+
this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
1125
1208
|
}
|
|
1126
1209
|
/**
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
* If a callback is configured, invokes it with the token.
|
|
1131
|
-
* In development mode (no callback), returns the token in the response.
|
|
1210
|
+
* Generate an authorization URL for the user to visit.
|
|
1211
|
+
* Returns the URL and the state parameter for CSRF validation.
|
|
1132
1212
|
*/
|
|
1133
|
-
async
|
|
1134
|
-
const
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
data: {
|
|
1139
|
-
message: "If an account with that email exists, a password reset link has been sent."
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
};
|
|
1143
|
-
const user = await this.userStore.findByEmail(normalizedEmail);
|
|
1144
|
-
if (!user) {
|
|
1145
|
-
return successResponse;
|
|
1146
|
-
}
|
|
1147
|
-
const activeCount = await this.resetStore.countActiveForEmail(normalizedEmail);
|
|
1148
|
-
if (activeCount >= this.maxRequestsPerEmail) {
|
|
1149
|
-
return successResponse;
|
|
1150
|
-
}
|
|
1151
|
-
const token = generateSecureToken();
|
|
1213
|
+
async getAuthorizationUrl(providerId, metadata) {
|
|
1214
|
+
const provider = this.getProvider(providerId);
|
|
1215
|
+
const state = generateState();
|
|
1216
|
+
const codeVerifier = provider.pkce ? generateCodeVerifier() : void 0;
|
|
1217
|
+
const codeChallenge = codeVerifier ? await createCodeChallenge(codeVerifier) : void 0;
|
|
1152
1218
|
const now = Date.now();
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1219
|
+
const oauthState = {
|
|
1220
|
+
state,
|
|
1221
|
+
provider: providerId,
|
|
1222
|
+
redirectUri: provider.redirectUri,
|
|
1157
1223
|
createdAt: now,
|
|
1158
|
-
expiresAt: now + this.
|
|
1159
|
-
|
|
1224
|
+
expiresAt: now + this.stateTtlMs,
|
|
1225
|
+
metadata,
|
|
1226
|
+
codeVerifier
|
|
1160
1227
|
};
|
|
1161
|
-
await this.
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1228
|
+
await this.stateStore.store(oauthState);
|
|
1229
|
+
const params = new URLSearchParams({
|
|
1230
|
+
client_id: provider.clientId,
|
|
1231
|
+
redirect_uri: provider.redirectUri,
|
|
1232
|
+
response_type: "code",
|
|
1233
|
+
scope: provider.scopes.join(" "),
|
|
1234
|
+
state
|
|
1235
|
+
});
|
|
1236
|
+
if (codeChallenge) {
|
|
1237
|
+
params.set("code_challenge", codeChallenge);
|
|
1238
|
+
params.set("code_challenge_method", "S256");
|
|
1170
1239
|
}
|
|
1171
|
-
|
|
1240
|
+
const url = `${provider.authorizationUrl}?${params.toString()}`;
|
|
1241
|
+
return { url, state };
|
|
1172
1242
|
}
|
|
1173
1243
|
/**
|
|
1174
|
-
*
|
|
1244
|
+
* Handle the OAuth callback after the user authorizes.
|
|
1245
|
+
* Validates the state parameter, exchanges the code for tokens,
|
|
1246
|
+
* and fetches user info.
|
|
1247
|
+
*
|
|
1248
|
+
* @param providerId - The OAuth provider
|
|
1249
|
+
* @param code - The authorization code from the callback
|
|
1250
|
+
* @param state - The state parameter from the callback
|
|
1251
|
+
* @returns Tokens and user info from the provider
|
|
1175
1252
|
*/
|
|
1176
|
-
async
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
if (newPassword.length > MAX_PASSWORD_LENGTH2) {
|
|
1184
|
-
return {
|
|
1185
|
-
status: 400,
|
|
1186
|
-
body: { error: `Password must be at most ${MAX_PASSWORD_LENGTH2} characters.` }
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
const resetToken = await this.resetStore.get(token);
|
|
1190
|
-
if (!resetToken || resetToken.consumed) {
|
|
1191
|
-
return { status: 404, body: { error: "Password reset token not found or already used." } };
|
|
1253
|
+
async handleCallback(providerId, code, state) {
|
|
1254
|
+
const provider = this.getProvider(providerId);
|
|
1255
|
+
const oauthState = await this.stateStore.consume(state);
|
|
1256
|
+
if (!oauthState || oauthState.provider !== providerId) {
|
|
1257
|
+
throw new OAuthStateMismatchError();
|
|
1192
1258
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1259
|
+
const tokens = await this.exchangeCodeForTokens(provider, code, oauthState);
|
|
1260
|
+
const userInfo = await this.fetchUserInfo(provider, tokens.accessToken);
|
|
1261
|
+
return { tokens, userInfo, stateMetadata: oauthState.metadata };
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Get a registered provider by ID.
|
|
1265
|
+
*/
|
|
1266
|
+
getProvider(providerId) {
|
|
1267
|
+
const provider = this.providers.get(providerId);
|
|
1268
|
+
if (!provider) {
|
|
1269
|
+
throw new OAuthProviderNotFoundError(providerId);
|
|
1196
1270
|
}
|
|
1197
|
-
|
|
1198
|
-
const hashed = await hashPassword(newPassword);
|
|
1199
|
-
await this.userStore.updatePassword(resetToken.userId, hashed.hash, hashed.salt);
|
|
1200
|
-
return { status: 200, body: { data: { message: "Password has been reset successfully." } } };
|
|
1271
|
+
return provider;
|
|
1201
1272
|
}
|
|
1202
1273
|
/**
|
|
1203
|
-
*
|
|
1274
|
+
* List all registered provider IDs.
|
|
1204
1275
|
*/
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1276
|
+
getProviderIds() {
|
|
1277
|
+
return [...this.providers.keys()];
|
|
1278
|
+
}
|
|
1279
|
+
// --- Private ---
|
|
1280
|
+
async exchangeCodeForTokens(provider, code, oauthState) {
|
|
1281
|
+
const body = new URLSearchParams({
|
|
1282
|
+
grant_type: "authorization_code",
|
|
1283
|
+
code,
|
|
1284
|
+
redirect_uri: provider.redirectUri,
|
|
1285
|
+
client_id: provider.clientId
|
|
1286
|
+
});
|
|
1287
|
+
if (provider.clientSecret) {
|
|
1288
|
+
body.set("client_secret", provider.clientSecret);
|
|
1211
1289
|
}
|
|
1212
|
-
if (
|
|
1213
|
-
|
|
1214
|
-
status: 400,
|
|
1215
|
-
body: { error: `New password must be at most ${MAX_PASSWORD_LENGTH2} characters.` }
|
|
1216
|
-
};
|
|
1290
|
+
if (oauthState.codeVerifier) {
|
|
1291
|
+
body.set("code_verifier", oauthState.codeVerifier);
|
|
1217
1292
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1293
|
+
let response;
|
|
1294
|
+
try {
|
|
1295
|
+
response = await this.fetchFn(provider.tokenUrl, {
|
|
1296
|
+
method: "POST",
|
|
1297
|
+
headers: {
|
|
1298
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1299
|
+
Accept: "application/json"
|
|
1300
|
+
},
|
|
1301
|
+
body: body.toString()
|
|
1302
|
+
});
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
throw new OAuthCodeExchangeError(err instanceof Error ? err.message : "Network error");
|
|
1221
1305
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1306
|
+
if (!response.ok) {
|
|
1307
|
+
let details = `HTTP ${response.status}`;
|
|
1308
|
+
try {
|
|
1309
|
+
const errorBody = await response.text();
|
|
1310
|
+
details += `: ${errorBody}`;
|
|
1311
|
+
} catch {
|
|
1312
|
+
}
|
|
1313
|
+
throw new OAuthCodeExchangeError(details);
|
|
1314
|
+
}
|
|
1315
|
+
const data = await response.json();
|
|
1316
|
+
return {
|
|
1317
|
+
accessToken: data.access_token,
|
|
1318
|
+
tokenType: data.token_type ?? "Bearer",
|
|
1319
|
+
expiresIn: data.expires_in,
|
|
1320
|
+
refreshToken: data.refresh_token,
|
|
1321
|
+
idToken: data.id_token,
|
|
1322
|
+
scope: data.scope
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
async fetchUserInfo(provider, accessToken) {
|
|
1326
|
+
let response;
|
|
1327
|
+
try {
|
|
1328
|
+
response = await this.fetchFn(provider.userInfoUrl, {
|
|
1329
|
+
headers: {
|
|
1330
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1331
|
+
Accept: "application/json"
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
throw new OAuthUserInfoError(err instanceof Error ? err.message : "Network error");
|
|
1336
|
+
}
|
|
1337
|
+
if (!response.ok) {
|
|
1338
|
+
throw new OAuthUserInfoError(`HTTP ${response.status}`);
|
|
1339
|
+
}
|
|
1340
|
+
const profile = await response.json();
|
|
1341
|
+
return normalizeUserInfo(provider.providerId, profile);
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
function normalizeUserInfo(providerId, profile) {
|
|
1345
|
+
switch (providerId) {
|
|
1346
|
+
case "google":
|
|
1347
|
+
return {
|
|
1348
|
+
providerId: profile.sub,
|
|
1349
|
+
provider: "google",
|
|
1350
|
+
email: profile.email ?? null,
|
|
1351
|
+
emailVerified: profile.email_verified ?? false,
|
|
1352
|
+
name: profile.name ?? null,
|
|
1353
|
+
avatarUrl: profile.picture ?? null,
|
|
1354
|
+
rawProfile: profile
|
|
1355
|
+
};
|
|
1356
|
+
case "github":
|
|
1357
|
+
return {
|
|
1358
|
+
providerId: String(profile.id),
|
|
1359
|
+
provider: "github",
|
|
1360
|
+
email: profile.email ?? null,
|
|
1361
|
+
emailVerified: false,
|
|
1362
|
+
// GitHub doesn't confirm in the profile response
|
|
1363
|
+
name: profile.name ?? profile.login ?? null,
|
|
1364
|
+
avatarUrl: profile.avatar_url ?? null,
|
|
1365
|
+
rawProfile: profile
|
|
1366
|
+
};
|
|
1367
|
+
case "microsoft":
|
|
1368
|
+
return {
|
|
1369
|
+
providerId: profile.id,
|
|
1370
|
+
provider: "microsoft",
|
|
1371
|
+
email: profile.mail ?? profile.userPrincipalName ?? null,
|
|
1372
|
+
emailVerified: false,
|
|
1373
|
+
name: profile.displayName ?? null,
|
|
1374
|
+
avatarUrl: null,
|
|
1375
|
+
rawProfile: profile
|
|
1376
|
+
};
|
|
1377
|
+
default:
|
|
1378
|
+
return {
|
|
1379
|
+
providerId: String(profile.id ?? profile.sub ?? ""),
|
|
1380
|
+
provider: providerId,
|
|
1381
|
+
email: profile.email ?? null,
|
|
1382
|
+
emailVerified: profile.email_verified ?? false,
|
|
1383
|
+
name: profile.name ?? null,
|
|
1384
|
+
avatarUrl: profile.picture ?? profile.avatar_url ?? null,
|
|
1385
|
+
rawProfile: profile
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
function googleProvider(config) {
|
|
1390
|
+
return {
|
|
1391
|
+
providerId: "google",
|
|
1392
|
+
clientId: config.clientId,
|
|
1393
|
+
clientSecret: config.clientSecret,
|
|
1394
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
1395
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
1396
|
+
userInfoUrl: "https://openidconnect.googleapis.com/v1/userinfo",
|
|
1397
|
+
scopes: config.scopes ?? ["openid", "email", "profile"],
|
|
1398
|
+
redirectUri: config.redirectUri,
|
|
1399
|
+
pkce: config.pkce
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
function githubProvider(config) {
|
|
1403
|
+
return {
|
|
1404
|
+
providerId: "github",
|
|
1405
|
+
clientId: config.clientId,
|
|
1406
|
+
clientSecret: config.clientSecret,
|
|
1407
|
+
authorizationUrl: "https://github.com/login/oauth/authorize",
|
|
1408
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
1409
|
+
userInfoUrl: "https://api.github.com/user",
|
|
1410
|
+
scopes: config.scopes ?? ["read:user", "user:email"],
|
|
1411
|
+
redirectUri: config.redirectUri,
|
|
1412
|
+
pkce: config.pkce
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
function microsoftProvider(config) {
|
|
1416
|
+
const tenant = config.tenantId ?? "common";
|
|
1417
|
+
return {
|
|
1418
|
+
providerId: "microsoft",
|
|
1419
|
+
clientId: config.clientId,
|
|
1420
|
+
clientSecret: config.clientSecret,
|
|
1421
|
+
authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
|
|
1422
|
+
tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
|
1423
|
+
userInfoUrl: "https://graph.microsoft.com/v1.0/me",
|
|
1424
|
+
scopes: config.scopes ?? ["openid", "email", "profile", "User.Read"],
|
|
1425
|
+
redirectUri: config.redirectUri,
|
|
1426
|
+
pkce: config.pkce
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function generateState() {
|
|
1430
|
+
const bytes = new Uint8Array(32);
|
|
1431
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
1432
|
+
return toBase64Url2(bytes);
|
|
1433
|
+
}
|
|
1434
|
+
function generateCodeVerifier() {
|
|
1435
|
+
const bytes = new Uint8Array(PKCE_VERIFIER_BYTES);
|
|
1436
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
1437
|
+
return toBase64Url2(bytes);
|
|
1438
|
+
}
|
|
1439
|
+
async function createCodeChallenge(codeVerifier) {
|
|
1440
|
+
const data = new TextEncoder().encode(codeVerifier);
|
|
1441
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
1442
|
+
return toBase64Url2(new Uint8Array(digest));
|
|
1443
|
+
}
|
|
1444
|
+
function toBase64Url2(bytes) {
|
|
1445
|
+
let binary = "";
|
|
1446
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1447
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1448
|
+
}
|
|
1449
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// src/provider/built-in/user-store.ts
|
|
1453
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1454
|
+
import { KoraError as KoraError4 } from "@korajs/core";
|
|
1455
|
+
var DuplicateEmailError = class extends KoraError4 {
|
|
1456
|
+
constructor() {
|
|
1457
|
+
super("A user with this email already exists.", "DUPLICATE_EMAIL");
|
|
1458
|
+
this.name = "DuplicateEmailError";
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
var InMemoryUserStore = class {
|
|
1462
|
+
/** Users indexed by ID */
|
|
1463
|
+
usersById = /* @__PURE__ */ new Map();
|
|
1464
|
+
/** Users indexed by email (lowercase) for fast lookup */
|
|
1465
|
+
usersByEmail = /* @__PURE__ */ new Map();
|
|
1466
|
+
/** Devices indexed by device ID */
|
|
1467
|
+
devicesById = /* @__PURE__ */ new Map();
|
|
1468
|
+
/** Device IDs indexed by user ID for fast listing */
|
|
1469
|
+
devicesByUserId = /* @__PURE__ */ new Map();
|
|
1470
|
+
/**
|
|
1471
|
+
* Create a new user account.
|
|
1472
|
+
*
|
|
1473
|
+
* @param params - User creation parameters
|
|
1474
|
+
* @param params.email - The user's email address (must be unique, case-insensitive)
|
|
1475
|
+
* @param params.passwordHash - Hex-encoded PBKDF2 derived key
|
|
1476
|
+
* @param params.salt - Hex-encoded salt used during hashing
|
|
1477
|
+
* @param params.name - The user's display name
|
|
1478
|
+
* @returns The created user (without sensitive credential fields)
|
|
1479
|
+
* @throws {DuplicateEmailError} If a user with the same email already exists
|
|
1480
|
+
*/
|
|
1481
|
+
async createUser(params) {
|
|
1482
|
+
const normalizedEmail = params.email.toLowerCase();
|
|
1483
|
+
if (this.usersByEmail.has(normalizedEmail)) {
|
|
1484
|
+
throw new DuplicateEmailError();
|
|
1485
|
+
}
|
|
1486
|
+
const now = Date.now();
|
|
1487
|
+
const id = randomUUID3();
|
|
1488
|
+
const storedUser = {
|
|
1489
|
+
id,
|
|
1490
|
+
email: normalizedEmail,
|
|
1491
|
+
name: params.name,
|
|
1492
|
+
emailVerified: false,
|
|
1493
|
+
createdAt: now,
|
|
1494
|
+
passwordHash: params.passwordHash,
|
|
1495
|
+
salt: params.salt
|
|
1496
|
+
};
|
|
1497
|
+
this.usersById.set(id, storedUser);
|
|
1498
|
+
this.usersByEmail.set(normalizedEmail, storedUser);
|
|
1499
|
+
return toAuthUser(storedUser);
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Find a user by email address.
|
|
1503
|
+
*
|
|
1504
|
+
* @param email - The email to search for (case-insensitive)
|
|
1505
|
+
* @returns The stored user record including credentials, or null if not found
|
|
1506
|
+
*/
|
|
1507
|
+
async findByEmail(email) {
|
|
1508
|
+
return this.usersByEmail.get(email.toLowerCase()) ?? null;
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Find a user by ID.
|
|
1512
|
+
*
|
|
1513
|
+
* @param id - The user ID to search for
|
|
1514
|
+
* @returns The stored user record including credentials, or null if not found
|
|
1515
|
+
*/
|
|
1516
|
+
async findById(id) {
|
|
1517
|
+
return this.usersById.get(id) ?? null;
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Register a device for a user.
|
|
1521
|
+
*
|
|
1522
|
+
* If a device with the same ID already exists and is not revoked, it is
|
|
1523
|
+
* returned as-is (idempotent registration). If it was previously revoked,
|
|
1524
|
+
* it is re-activated with updated details.
|
|
1525
|
+
*
|
|
1526
|
+
* @param params - Device registration parameters
|
|
1527
|
+
* @param params.id - Unique device identifier
|
|
1528
|
+
* @param params.userId - ID of the user who owns the device
|
|
1529
|
+
* @param params.publicKey - Base64url-encoded device public key or thumbprint
|
|
1530
|
+
* @param params.name - Human-readable device name
|
|
1531
|
+
* @returns The registered device record
|
|
1532
|
+
*/
|
|
1533
|
+
async registerDevice(params) {
|
|
1534
|
+
const existing = this.devicesById.get(params.id);
|
|
1535
|
+
if (existing !== void 0 && !existing.revoked) {
|
|
1536
|
+
return existing;
|
|
1537
|
+
}
|
|
1538
|
+
const now = Date.now();
|
|
1539
|
+
const device = {
|
|
1540
|
+
id: params.id,
|
|
1541
|
+
userId: params.userId,
|
|
1542
|
+
publicKey: params.publicKey,
|
|
1543
|
+
name: params.name,
|
|
1544
|
+
revoked: false,
|
|
1545
|
+
createdAt: now,
|
|
1546
|
+
lastSeenAt: now
|
|
1547
|
+
};
|
|
1548
|
+
this.devicesById.set(params.id, device);
|
|
1549
|
+
let userDevices = this.devicesByUserId.get(params.userId);
|
|
1550
|
+
if (userDevices === void 0) {
|
|
1551
|
+
userDevices = /* @__PURE__ */ new Set();
|
|
1552
|
+
this.devicesByUserId.set(params.userId, userDevices);
|
|
1553
|
+
}
|
|
1554
|
+
userDevices.add(params.id);
|
|
1555
|
+
return device;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Find a device by its ID.
|
|
1559
|
+
*
|
|
1560
|
+
* @param deviceId - The device ID to search for
|
|
1561
|
+
* @returns The device record, or null if not found
|
|
1562
|
+
*/
|
|
1563
|
+
async findDevice(deviceId) {
|
|
1564
|
+
return this.devicesById.get(deviceId) ?? null;
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* List all devices registered for a user.
|
|
1568
|
+
*
|
|
1569
|
+
* @param userId - The user ID whose devices to list
|
|
1570
|
+
* @returns Array of device records (includes revoked devices)
|
|
1571
|
+
*/
|
|
1572
|
+
async listDevices(userId) {
|
|
1573
|
+
const deviceIds = this.devicesByUserId.get(userId);
|
|
1574
|
+
if (deviceIds === void 0) {
|
|
1575
|
+
return [];
|
|
1576
|
+
}
|
|
1577
|
+
const devices = [];
|
|
1578
|
+
for (const deviceId of deviceIds) {
|
|
1579
|
+
const device = this.devicesById.get(deviceId);
|
|
1580
|
+
if (device !== void 0) {
|
|
1581
|
+
devices.push(device);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return devices;
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Revoke a device, preventing it from being used for authentication.
|
|
1588
|
+
*
|
|
1589
|
+
* This is a soft revoke — the device record remains but is marked as revoked.
|
|
1590
|
+
* If the device does not exist, this is a no-op.
|
|
1591
|
+
*
|
|
1592
|
+
* @param deviceId - The ID of the device to revoke
|
|
1593
|
+
*/
|
|
1594
|
+
async revokeDevice(deviceId) {
|
|
1595
|
+
const device = this.devicesById.get(deviceId);
|
|
1596
|
+
if (device !== void 0) {
|
|
1597
|
+
device.revoked = true;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Set a user's email verification status.
|
|
1602
|
+
*
|
|
1603
|
+
* @param userId - The user whose email to verify
|
|
1604
|
+
* @param verified - Whether the email is verified
|
|
1605
|
+
*/
|
|
1606
|
+
async setEmailVerified(userId, verified) {
|
|
1607
|
+
const user = this.usersById.get(userId);
|
|
1608
|
+
if (!user) return;
|
|
1609
|
+
const updated = { ...user, emailVerified: verified };
|
|
1610
|
+
this.usersById.set(userId, updated);
|
|
1611
|
+
this.usersByEmail.set(user.email, updated);
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Update a user's password hash and salt.
|
|
1615
|
+
*
|
|
1616
|
+
* @param userId - The user whose password to update
|
|
1617
|
+
* @param passwordHash - New hex-encoded PBKDF2 derived key
|
|
1618
|
+
* @param salt - New hex-encoded salt
|
|
1619
|
+
*/
|
|
1620
|
+
async updatePassword(userId, passwordHash, salt) {
|
|
1621
|
+
const user = this.usersById.get(userId);
|
|
1622
|
+
if (!user) return;
|
|
1623
|
+
const updated = { ...user, passwordHash, salt };
|
|
1624
|
+
this.usersById.set(userId, updated);
|
|
1625
|
+
this.usersByEmail.set(user.email, updated);
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* List all users. For admin/development use.
|
|
1629
|
+
*/
|
|
1630
|
+
async listAll() {
|
|
1631
|
+
return [...this.usersById.values()];
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Update a stored user record.
|
|
1635
|
+
*/
|
|
1636
|
+
async update(user) {
|
|
1637
|
+
const existing = this.usersById.get(user.id);
|
|
1638
|
+
if (!existing) return;
|
|
1639
|
+
if (existing.email !== user.email) {
|
|
1640
|
+
this.usersByEmail.delete(existing.email);
|
|
1641
|
+
this.usersByEmail.set(user.email, user);
|
|
1642
|
+
} else {
|
|
1643
|
+
this.usersByEmail.set(user.email, user);
|
|
1644
|
+
}
|
|
1645
|
+
this.usersById.set(user.id, user);
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Delete a user and all associated devices.
|
|
1649
|
+
*/
|
|
1650
|
+
async delete(userId) {
|
|
1651
|
+
const user = this.usersById.get(userId);
|
|
1652
|
+
if (!user) return;
|
|
1653
|
+
this.usersById.delete(userId);
|
|
1654
|
+
this.usersByEmail.delete(user.email);
|
|
1655
|
+
const deviceIds = this.devicesByUserId.get(userId);
|
|
1656
|
+
if (deviceIds) {
|
|
1657
|
+
for (const deviceId of deviceIds) {
|
|
1658
|
+
this.devicesById.delete(deviceId);
|
|
1659
|
+
}
|
|
1660
|
+
this.devicesByUserId.delete(userId);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Update the last-seen timestamp for a device.
|
|
1665
|
+
*
|
|
1666
|
+
* Called when a device authenticates or syncs to track activity.
|
|
1667
|
+
* If the device does not exist, this is a no-op.
|
|
1668
|
+
*
|
|
1669
|
+
* @param deviceId - The ID of the device to update
|
|
1670
|
+
*/
|
|
1671
|
+
async touchDevice(deviceId) {
|
|
1672
|
+
const device = this.devicesById.get(deviceId);
|
|
1673
|
+
if (device !== void 0) {
|
|
1674
|
+
device.lastSeenAt = Date.now();
|
|
1226
1675
|
}
|
|
1227
|
-
const hashed = await hashPassword(newPassword);
|
|
1228
|
-
await this.userStore.updatePassword(userId, hashed.hash, hashed.salt);
|
|
1229
|
-
return { status: 200, body: { data: { message: "Password changed successfully." } } };
|
|
1230
1676
|
}
|
|
1231
1677
|
};
|
|
1232
|
-
function
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1678
|
+
function toAuthUser(stored) {
|
|
1679
|
+
return {
|
|
1680
|
+
id: stored.id,
|
|
1681
|
+
email: stored.email,
|
|
1682
|
+
name: stored.name,
|
|
1683
|
+
emailVerified: stored.emailVerified,
|
|
1684
|
+
createdAt: stored.createdAt
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/provider/built-in/quickstart-server.ts
|
|
1689
|
+
function createKoraAuthServer(options = {}) {
|
|
1690
|
+
const userStore = options.userStore ?? new InMemoryUserStore();
|
|
1691
|
+
const tokenManager = options.tokenManager ?? createDefaultTokenManager(options);
|
|
1692
|
+
const routes = new BuiltInAuthRoutes({
|
|
1693
|
+
userStore,
|
|
1694
|
+
tokenManager,
|
|
1695
|
+
challengeStore: options.challengeStore,
|
|
1696
|
+
rateLimiter: options.rateLimiter
|
|
1697
|
+
});
|
|
1698
|
+
const oauth = options.oauth ? createOAuthRuntime(options.oauth) : void 0;
|
|
1699
|
+
const path = normalizePath(options.path ?? "/auth");
|
|
1700
|
+
return {
|
|
1701
|
+
routes,
|
|
1702
|
+
userStore,
|
|
1703
|
+
tokenManager,
|
|
1704
|
+
oauth: oauth?.manager,
|
|
1705
|
+
linkedIdentityStore: oauth?.linkedIdentityStore,
|
|
1706
|
+
auth: routes.toSyncAuthProvider(),
|
|
1707
|
+
handleRequest(request) {
|
|
1708
|
+
return handleAuthRequest(routes, path, request, oauth, userStore, tokenManager);
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
function createDefaultTokenManager(options) {
|
|
1713
|
+
const secret = options.jwtSecret ?? readEnvSecret();
|
|
1714
|
+
if (!secret && isProduction()) {
|
|
1715
|
+
throw new Error(
|
|
1716
|
+
"createKoraAuthServer requires jwtSecret in production. Set KORA_AUTH_SECRET or pass jwtSecret."
|
|
1717
|
+
);
|
|
1238
1718
|
}
|
|
1239
|
-
return
|
|
1719
|
+
return new TokenManager({
|
|
1720
|
+
secret: secret ?? TokenManager.generateSecret(),
|
|
1721
|
+
revocationStore: new InMemoryTokenRevocationStore(),
|
|
1722
|
+
...options.tokenManagerOptions
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
function createOAuthRuntime(config) {
|
|
1726
|
+
return {
|
|
1727
|
+
manager: new OAuthManager(config),
|
|
1728
|
+
linkedIdentityStore: config.linkedIdentityStore ?? new InMemoryLinkedIdentityStore(),
|
|
1729
|
+
createNewUsers: config.createNewUsers ?? true,
|
|
1730
|
+
autoLinkVerifiedEmail: config.autoLinkVerifiedEmail ?? false,
|
|
1731
|
+
allowUnlinkLastIdentity: config.allowUnlinkLastIdentity ?? false
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
async function handleAuthRequest(routes, pathPrefix, request, oauth, userStore, tokenManager) {
|
|
1735
|
+
const path = normalizePath(request.path);
|
|
1736
|
+
const relativePath = path === pathPrefix ? "/" : path.slice(pathPrefix.length);
|
|
1737
|
+
const method = request.method.toUpperCase();
|
|
1738
|
+
const body = isRecord(request.body) ? request.body : {};
|
|
1739
|
+
const token = extractBearerToken(request.headers);
|
|
1740
|
+
if (path !== pathPrefix && !path.startsWith(`${pathPrefix}/`)) {
|
|
1741
|
+
return notFound();
|
|
1742
|
+
}
|
|
1743
|
+
if (relativePath.startsWith("/oauth/")) {
|
|
1744
|
+
return handleOAuthRequest({
|
|
1745
|
+
oauth,
|
|
1746
|
+
userStore,
|
|
1747
|
+
tokenManager,
|
|
1748
|
+
relativePath,
|
|
1749
|
+
method,
|
|
1750
|
+
body,
|
|
1751
|
+
query: request.query,
|
|
1752
|
+
token
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
if (method === "POST" && relativePath === "/signup") {
|
|
1756
|
+
return routes.handleSignUp(body, request.ip);
|
|
1757
|
+
}
|
|
1758
|
+
if (method === "POST" && relativePath === "/signin") {
|
|
1759
|
+
return routes.handleSignIn(body, request.ip);
|
|
1760
|
+
}
|
|
1761
|
+
if (method === "POST" && relativePath === "/refresh") {
|
|
1762
|
+
return routes.handleRefresh(body);
|
|
1763
|
+
}
|
|
1764
|
+
if (method === "POST" && relativePath === "/signout") {
|
|
1765
|
+
return routes.handleSignOut(token, body);
|
|
1766
|
+
}
|
|
1767
|
+
if (method === "GET" && relativePath === "/me") {
|
|
1768
|
+
return routes.handleGetMe(token);
|
|
1769
|
+
}
|
|
1770
|
+
if (method === "GET" && relativePath === "/devices") {
|
|
1771
|
+
return routes.handleListDevices(token);
|
|
1772
|
+
}
|
|
1773
|
+
if (method === "POST" && relativePath === "/device/register") {
|
|
1774
|
+
return routes.handleDeviceRegister(token, body);
|
|
1775
|
+
}
|
|
1776
|
+
if (method === "POST" && relativePath === "/device/challenge") {
|
|
1777
|
+
const deviceId = typeof body.deviceId === "string" ? body.deviceId : "";
|
|
1778
|
+
return routes.handleDeviceChallenge(token, deviceId);
|
|
1779
|
+
}
|
|
1780
|
+
if (method === "POST" && relativePath === "/device/verify") {
|
|
1781
|
+
return routes.handleDeviceVerify(body);
|
|
1782
|
+
}
|
|
1783
|
+
if (method === "DELETE" && relativePath.startsWith("/device/")) {
|
|
1784
|
+
return routes.handleRevokeDevice(token, relativePath.slice("/device/".length));
|
|
1785
|
+
}
|
|
1786
|
+
return notFound();
|
|
1787
|
+
}
|
|
1788
|
+
async function handleOAuthRequest(params) {
|
|
1789
|
+
const { oauth, userStore, tokenManager, relativePath, method, body, query, token } = params;
|
|
1790
|
+
if (!oauth) {
|
|
1791
|
+
return notFound();
|
|
1792
|
+
}
|
|
1793
|
+
try {
|
|
1794
|
+
if (method === "GET" && relativePath === "/oauth/links") {
|
|
1795
|
+
const authUser = await requireAuthUser(tokenManager, userStore, token);
|
|
1796
|
+
if ("status" in authUser) return authUser;
|
|
1797
|
+
const identities = await oauth.linkedIdentityStore.findByUser(authUser.id);
|
|
1798
|
+
return { status: 200, body: { data: identities } };
|
|
1799
|
+
}
|
|
1800
|
+
const match = /^\/oauth\/([^/]+)(?:\/(callback|link))?$/.exec(relativePath);
|
|
1801
|
+
if (!match) {
|
|
1802
|
+
return notFound();
|
|
1803
|
+
}
|
|
1804
|
+
const provider = decodeURIComponent(match[1]);
|
|
1805
|
+
const action = match[2];
|
|
1806
|
+
if (method === "GET" && !action) {
|
|
1807
|
+
const { url, state } = await oauth.manager.getAuthorizationUrl(
|
|
1808
|
+
provider,
|
|
1809
|
+
metadataFromQuery(query)
|
|
1810
|
+
);
|
|
1811
|
+
return { status: 200, body: { data: { url, state } } };
|
|
1812
|
+
}
|
|
1813
|
+
if ((method === "GET" || method === "POST") && action === "callback") {
|
|
1814
|
+
const code = readString(method === "GET" ? queryValue(query, "code") : body.code);
|
|
1815
|
+
const state = readString(method === "GET" ? queryValue(query, "state") : body.state);
|
|
1816
|
+
if (!code || !state) {
|
|
1817
|
+
return { status: 400, body: { error: "OAuth callback requires code and state." } };
|
|
1818
|
+
}
|
|
1819
|
+
return completeOAuthSignIn({
|
|
1820
|
+
oauth,
|
|
1821
|
+
userStore,
|
|
1822
|
+
tokenManager,
|
|
1823
|
+
provider,
|
|
1824
|
+
code,
|
|
1825
|
+
state,
|
|
1826
|
+
deviceId: readString(body.deviceId),
|
|
1827
|
+
devicePublicKey: readString(body.devicePublicKey)
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
if (method === "POST" && action === "link") {
|
|
1831
|
+
const authUser = await requireAuthUser(tokenManager, userStore, token);
|
|
1832
|
+
if ("status" in authUser) return authUser;
|
|
1833
|
+
const code = readString(body.code);
|
|
1834
|
+
const state = readString(body.state);
|
|
1835
|
+
if (!code || !state) {
|
|
1836
|
+
return { status: 400, body: { error: "OAuth linking requires code and state." } };
|
|
1837
|
+
}
|
|
1838
|
+
return linkOAuthIdentity(oauth, authUser.id, provider, code, state);
|
|
1839
|
+
}
|
|
1840
|
+
if (method === "DELETE" && action === "link") {
|
|
1841
|
+
const authUser = await requireAuthUser(tokenManager, userStore, token);
|
|
1842
|
+
if ("status" in authUser) return authUser;
|
|
1843
|
+
const identities = await oauth.linkedIdentityStore.findByUser(authUser.id);
|
|
1844
|
+
if (!oauth.allowUnlinkLastIdentity && identities.length <= 1) {
|
|
1845
|
+
return {
|
|
1846
|
+
status: 409,
|
|
1847
|
+
body: {
|
|
1848
|
+
error: "Cannot unlink the last OAuth identity unless allowUnlinkLastIdentity is enabled."
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
await oauth.linkedIdentityStore.delete(authUser.id, provider);
|
|
1853
|
+
return { status: 200, body: { data: { ok: true } } };
|
|
1854
|
+
}
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
return oauthErrorResponse(error);
|
|
1857
|
+
}
|
|
1858
|
+
return notFound();
|
|
1859
|
+
}
|
|
1860
|
+
async function completeOAuthSignIn(params) {
|
|
1861
|
+
const { oauth, userStore, tokenManager, provider, code, state, deviceId, devicePublicKey } = params;
|
|
1862
|
+
const { userInfo, stateMetadata } = await oauth.manager.handleCallback(provider, code, state);
|
|
1863
|
+
const linkedIdentity = await oauth.linkedIdentityStore.findByProvider(
|
|
1864
|
+
userInfo.provider,
|
|
1865
|
+
userInfo.providerId
|
|
1866
|
+
);
|
|
1867
|
+
let user;
|
|
1868
|
+
let identity;
|
|
1869
|
+
if (linkedIdentity) {
|
|
1870
|
+
const storedUser = await userStore.findById(linkedIdentity.userId);
|
|
1871
|
+
if (!storedUser) {
|
|
1872
|
+
return { status: 409, body: { error: "Linked OAuth account has no matching user." } };
|
|
1873
|
+
}
|
|
1874
|
+
user = toAuthUser2(storedUser);
|
|
1875
|
+
identity = linkedIdentity;
|
|
1876
|
+
} else {
|
|
1877
|
+
const resolved = await resolveOAuthUser(userStore, oauth, userInfo);
|
|
1878
|
+
if ("status" in resolved) return resolved;
|
|
1879
|
+
user = resolved;
|
|
1880
|
+
identity = await oauth.linkedIdentityStore.create({
|
|
1881
|
+
userId: user.id,
|
|
1882
|
+
provider: userInfo.provider,
|
|
1883
|
+
providerUserId: userInfo.providerId,
|
|
1884
|
+
email: userInfo.email
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
const resolvedDeviceId = deviceId ?? readString(stateMetadata?.deviceId) ?? `device-${user.id}`;
|
|
1888
|
+
await userStore.registerDevice({
|
|
1889
|
+
id: resolvedDeviceId,
|
|
1890
|
+
userId: user.id,
|
|
1891
|
+
publicKey: devicePublicKey ?? readString(stateMetadata?.devicePublicKey) ?? "",
|
|
1892
|
+
name: deviceId ? "Device" : "Browser"
|
|
1893
|
+
});
|
|
1894
|
+
const tokens = tokenManager.issueTokens(user.id, resolvedDeviceId);
|
|
1895
|
+
return { status: 200, body: { data: { user, tokens, identity } } };
|
|
1896
|
+
}
|
|
1897
|
+
async function resolveOAuthUser(userStore, oauth, userInfo) {
|
|
1898
|
+
if (!userInfo.email) {
|
|
1899
|
+
return { status: 400, body: { error: "OAuth provider did not return an email address." } };
|
|
1900
|
+
}
|
|
1901
|
+
const existingUser = await userStore.findByEmail(userInfo.email);
|
|
1902
|
+
if (existingUser) {
|
|
1903
|
+
if (oauth.autoLinkVerifiedEmail && userInfo.emailVerified) {
|
|
1904
|
+
return toAuthUser2(existingUser);
|
|
1905
|
+
}
|
|
1906
|
+
return {
|
|
1907
|
+
status: 409,
|
|
1908
|
+
body: { error: "OAuth account is not linked. Sign in and link this provider first." }
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
if (!oauth.createNewUsers) {
|
|
1912
|
+
return { status: 403, body: { error: "OAuth sign-up is disabled for this application." } };
|
|
1913
|
+
}
|
|
1914
|
+
const credential = await hashPassword(randomUUID4());
|
|
1915
|
+
const user = await userStore.createUser({
|
|
1916
|
+
email: userInfo.email,
|
|
1917
|
+
passwordHash: credential.hash,
|
|
1918
|
+
salt: credential.salt,
|
|
1919
|
+
name: userInfo.name ?? userInfo.email.split("@")[0] ?? userInfo.email
|
|
1920
|
+
});
|
|
1921
|
+
if (userInfo.emailVerified) {
|
|
1922
|
+
await userStore.setEmailVerified(user.id, true);
|
|
1923
|
+
return { ...user, emailVerified: true };
|
|
1924
|
+
}
|
|
1925
|
+
return user;
|
|
1926
|
+
}
|
|
1927
|
+
async function linkOAuthIdentity(oauth, userId, provider, code, state) {
|
|
1928
|
+
const { userInfo } = await oauth.manager.handleCallback(provider, code, state);
|
|
1929
|
+
const existing = await oauth.linkedIdentityStore.findByProvider(
|
|
1930
|
+
userInfo.provider,
|
|
1931
|
+
userInfo.providerId
|
|
1932
|
+
);
|
|
1933
|
+
if (existing && existing.userId !== userId) {
|
|
1934
|
+
return { status: 409, body: { error: "This OAuth account is already linked to another user." } };
|
|
1935
|
+
}
|
|
1936
|
+
if (existing) {
|
|
1937
|
+
return { status: 200, body: { data: existing } };
|
|
1938
|
+
}
|
|
1939
|
+
const identity = await oauth.linkedIdentityStore.create({
|
|
1940
|
+
userId,
|
|
1941
|
+
provider: userInfo.provider,
|
|
1942
|
+
providerUserId: userInfo.providerId,
|
|
1943
|
+
email: userInfo.email
|
|
1944
|
+
});
|
|
1945
|
+
return { status: 201, body: { data: identity } };
|
|
1946
|
+
}
|
|
1947
|
+
async function requireAuthUser(tokenManager, userStore, token) {
|
|
1948
|
+
if (!token) {
|
|
1949
|
+
return { status: 401, body: { error: "Authorization token required." } };
|
|
1950
|
+
}
|
|
1951
|
+
const payload = await tokenManager.validateToken(token);
|
|
1952
|
+
if (!payload || payload.type !== "access") {
|
|
1953
|
+
return { status: 401, body: { error: "Invalid or expired token." } };
|
|
1954
|
+
}
|
|
1955
|
+
const user = await userStore.findById(payload.sub);
|
|
1956
|
+
if (!user) {
|
|
1957
|
+
return { status: 401, body: { error: "User not found." } };
|
|
1958
|
+
}
|
|
1959
|
+
return toAuthUser2(user);
|
|
1960
|
+
}
|
|
1961
|
+
function extractBearerToken(headers) {
|
|
1962
|
+
const authorization = headers?.authorization ?? headers?.Authorization;
|
|
1963
|
+
const value = Array.isArray(authorization) ? authorization[0] : authorization;
|
|
1964
|
+
if (!value?.startsWith("Bearer ")) {
|
|
1965
|
+
return "";
|
|
1966
|
+
}
|
|
1967
|
+
return value.slice("Bearer ".length).trim();
|
|
1968
|
+
}
|
|
1969
|
+
function normalizePath(path) {
|
|
1970
|
+
const withoutQuery = path.split("?")[0] || "/";
|
|
1971
|
+
const normalized = withoutQuery.startsWith("/") ? withoutQuery : `/${withoutQuery}`;
|
|
1972
|
+
return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
|
|
1973
|
+
}
|
|
1974
|
+
function metadataFromQuery(query) {
|
|
1975
|
+
if (!query) return void 0;
|
|
1976
|
+
const metadata = {};
|
|
1977
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1978
|
+
if (key === "code" || key === "state") continue;
|
|
1979
|
+
if (value !== void 0) {
|
|
1980
|
+
metadata[key] = value;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
return Object.keys(metadata).length > 0 ? metadata : void 0;
|
|
1984
|
+
}
|
|
1985
|
+
function queryValue(query, key) {
|
|
1986
|
+
return query?.[key];
|
|
1987
|
+
}
|
|
1988
|
+
function readString(value) {
|
|
1989
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
1990
|
+
if (Array.isArray(value) && typeof value[0] === "string" && value[0].length > 0) {
|
|
1991
|
+
return value[0];
|
|
1992
|
+
}
|
|
1993
|
+
return void 0;
|
|
1994
|
+
}
|
|
1995
|
+
function oauthErrorResponse(error) {
|
|
1996
|
+
if (error instanceof DuplicateLinkedIdentityError) {
|
|
1997
|
+
return { status: 409, body: { error: error.message } };
|
|
1998
|
+
}
|
|
1999
|
+
if (error instanceof OAuthError) {
|
|
2000
|
+
const status = error.code === "OAUTH_PROVIDER_NOT_FOUND" ? 404 : 400;
|
|
2001
|
+
return { status, body: { error: error.message } };
|
|
2002
|
+
}
|
|
2003
|
+
throw error;
|
|
2004
|
+
}
|
|
2005
|
+
function toAuthUser2(user) {
|
|
2006
|
+
return {
|
|
2007
|
+
id: user.id,
|
|
2008
|
+
email: user.email,
|
|
2009
|
+
name: user.name,
|
|
2010
|
+
emailVerified: user.emailVerified,
|
|
2011
|
+
createdAt: user.createdAt
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
function isRecord(value) {
|
|
2015
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2016
|
+
}
|
|
2017
|
+
function notFound() {
|
|
2018
|
+
return { status: 404, body: { error: "Not found" } };
|
|
2019
|
+
}
|
|
2020
|
+
function readEnvSecret() {
|
|
2021
|
+
return typeof process !== "undefined" ? process.env.KORA_AUTH_SECRET : void 0;
|
|
2022
|
+
}
|
|
2023
|
+
function isProduction() {
|
|
2024
|
+
return typeof process !== "undefined" && process.env.NODE_ENV === "production";
|
|
1240
2025
|
}
|
|
1241
2026
|
|
|
1242
|
-
// src/provider/built-in/
|
|
1243
|
-
import { KoraError as
|
|
1244
|
-
var
|
|
2027
|
+
// src/provider/built-in/password-reset.ts
|
|
2028
|
+
import { KoraError as KoraError5 } from "@korajs/core";
|
|
2029
|
+
var PasswordResetError = class extends KoraError5 {
|
|
1245
2030
|
constructor(message, code, context) {
|
|
1246
2031
|
super(message, code, context);
|
|
1247
|
-
this.name = "
|
|
2032
|
+
this.name = "PasswordResetError";
|
|
1248
2033
|
}
|
|
1249
2034
|
};
|
|
1250
|
-
var
|
|
2035
|
+
var ResetTokenExpiredError = class extends PasswordResetError {
|
|
1251
2036
|
constructor() {
|
|
1252
|
-
super("
|
|
2037
|
+
super("Password reset token has expired.", "RESET_TOKEN_EXPIRED");
|
|
1253
2038
|
}
|
|
1254
2039
|
};
|
|
1255
|
-
var
|
|
2040
|
+
var ResetTokenNotFoundError = class extends PasswordResetError {
|
|
1256
2041
|
constructor() {
|
|
1257
|
-
super("
|
|
2042
|
+
super("Password reset token not found or already used.", "RESET_TOKEN_NOT_FOUND");
|
|
1258
2043
|
}
|
|
1259
2044
|
};
|
|
1260
|
-
var
|
|
2045
|
+
var ResetRateLimitedError = class extends PasswordResetError {
|
|
2046
|
+
constructor() {
|
|
2047
|
+
super("Too many password reset requests. Please try again later.", "RESET_RATE_LIMITED");
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
var InMemoryPasswordResetStore = class {
|
|
1261
2051
|
tokens = /* @__PURE__ */ new Map();
|
|
1262
2052
|
async store(token) {
|
|
1263
2053
|
this.tokens.set(token.token, token);
|
|
@@ -1271,11 +2061,11 @@ var InMemoryEmailVerificationStore = class {
|
|
|
1271
2061
|
this.tokens.set(token, { ...entry, consumed: true });
|
|
1272
2062
|
}
|
|
1273
2063
|
}
|
|
1274
|
-
async
|
|
2064
|
+
async countActiveForEmail(email) {
|
|
1275
2065
|
const now = Date.now();
|
|
1276
2066
|
let count = 0;
|
|
1277
2067
|
for (const token of this.tokens.values()) {
|
|
1278
|
-
if (token.
|
|
2068
|
+
if (token.email === email && !token.consumed && now < token.expiresAt) {
|
|
1279
2069
|
count++;
|
|
1280
2070
|
}
|
|
1281
2071
|
}
|
|
@@ -1293,97 +2083,130 @@ var InMemoryEmailVerificationStore = class {
|
|
|
1293
2083
|
return count;
|
|
1294
2084
|
}
|
|
1295
2085
|
};
|
|
1296
|
-
var
|
|
1297
|
-
var
|
|
1298
|
-
var
|
|
2086
|
+
var DEFAULT_TOKEN_TTL_MS = 60 * 60 * 1e3;
|
|
2087
|
+
var DEFAULT_MAX_REQUESTS = 3;
|
|
2088
|
+
var MIN_PASSWORD_LENGTH2 = 8;
|
|
2089
|
+
var MAX_PASSWORD_LENGTH2 = 128;
|
|
2090
|
+
var PasswordResetManager = class {
|
|
1299
2091
|
userStore;
|
|
1300
|
-
|
|
2092
|
+
resetStore;
|
|
1301
2093
|
tokenTtlMs;
|
|
1302
|
-
|
|
1303
|
-
|
|
2094
|
+
maxRequestsPerEmail;
|
|
2095
|
+
onResetRequested;
|
|
1304
2096
|
constructor(config) {
|
|
1305
2097
|
this.userStore = config.userStore;
|
|
1306
|
-
this.
|
|
1307
|
-
this.tokenTtlMs = config.tokenTtlMs ??
|
|
1308
|
-
this.
|
|
1309
|
-
this.
|
|
2098
|
+
this.resetStore = config.resetStore ?? new InMemoryPasswordResetStore();
|
|
2099
|
+
this.tokenTtlMs = config.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
|
|
2100
|
+
this.maxRequestsPerEmail = config.maxRequestsPerEmail ?? DEFAULT_MAX_REQUESTS;
|
|
2101
|
+
this.onResetRequested = config.onResetRequested;
|
|
1310
2102
|
}
|
|
1311
2103
|
/**
|
|
1312
|
-
*
|
|
1313
|
-
*
|
|
2104
|
+
* Request a password reset for an email.
|
|
2105
|
+
* Always returns success to prevent email enumeration.
|
|
2106
|
+
*
|
|
2107
|
+
* If a callback is configured, invokes it with the token.
|
|
2108
|
+
* In development mode (no callback), returns the token in the response.
|
|
1314
2109
|
*/
|
|
1315
|
-
async
|
|
2110
|
+
async requestReset(email) {
|
|
1316
2111
|
const normalizedEmail = email.toLowerCase().trim();
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
2112
|
+
const successResponse = {
|
|
2113
|
+
status: 200,
|
|
2114
|
+
body: {
|
|
2115
|
+
data: {
|
|
2116
|
+
message: "If an account with that email exists, a password reset link has been sent."
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
const user = await this.userStore.findByEmail(normalizedEmail);
|
|
2121
|
+
if (!user) {
|
|
2122
|
+
return successResponse;
|
|
1323
2123
|
}
|
|
1324
|
-
const
|
|
2124
|
+
const activeCount = await this.resetStore.countActiveForEmail(normalizedEmail);
|
|
2125
|
+
if (activeCount >= this.maxRequestsPerEmail) {
|
|
2126
|
+
return successResponse;
|
|
2127
|
+
}
|
|
2128
|
+
const token = generateSecureToken();
|
|
1325
2129
|
const now = Date.now();
|
|
1326
|
-
const
|
|
2130
|
+
const resetToken = {
|
|
1327
2131
|
token,
|
|
1328
|
-
userId,
|
|
2132
|
+
userId: user.id,
|
|
1329
2133
|
email: normalizedEmail,
|
|
1330
2134
|
createdAt: now,
|
|
1331
2135
|
expiresAt: now + this.tokenTtlMs,
|
|
1332
2136
|
consumed: false
|
|
1333
2137
|
};
|
|
1334
|
-
await this.
|
|
1335
|
-
if (this.
|
|
2138
|
+
await this.resetStore.store(resetToken);
|
|
2139
|
+
if (this.onResetRequested) {
|
|
1336
2140
|
try {
|
|
1337
|
-
await this.
|
|
2141
|
+
await this.onResetRequested(normalizedEmail, token, resetToken.expiresAt);
|
|
1338
2142
|
} catch {
|
|
1339
2143
|
}
|
|
1340
2144
|
}
|
|
1341
|
-
|
|
1342
|
-
message: "
|
|
1343
|
-
};
|
|
1344
|
-
if (!this.onVerificationRequired) {
|
|
1345
|
-
responseData.token = token;
|
|
2145
|
+
if (!this.onResetRequested) {
|
|
2146
|
+
successResponse.body = { data: { message: "Password reset token generated.", token } };
|
|
1346
2147
|
}
|
|
1347
|
-
return
|
|
2148
|
+
return successResponse;
|
|
1348
2149
|
}
|
|
1349
2150
|
/**
|
|
1350
|
-
*
|
|
2151
|
+
* Consume a reset token and set a new password.
|
|
1351
2152
|
*/
|
|
1352
|
-
async
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
2153
|
+
async resetPassword(token, newPassword) {
|
|
2154
|
+
if (typeof newPassword !== "string" || newPassword.length < MIN_PASSWORD_LENGTH2) {
|
|
2155
|
+
return {
|
|
2156
|
+
status: 400,
|
|
2157
|
+
body: { error: `Password must be at least ${MIN_PASSWORD_LENGTH2} characters.` }
|
|
2158
|
+
};
|
|
1356
2159
|
}
|
|
1357
|
-
if (
|
|
1358
|
-
|
|
1359
|
-
|
|
2160
|
+
if (newPassword.length > MAX_PASSWORD_LENGTH2) {
|
|
2161
|
+
return {
|
|
2162
|
+
status: 400,
|
|
2163
|
+
body: { error: `Password must be at most ${MAX_PASSWORD_LENGTH2} characters.` }
|
|
2164
|
+
};
|
|
1360
2165
|
}
|
|
1361
|
-
await this.
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
};
|
|
2166
|
+
const resetToken = await this.resetStore.get(token);
|
|
2167
|
+
if (!resetToken || resetToken.consumed) {
|
|
2168
|
+
return { status: 404, body: { error: "Password reset token not found or already used." } };
|
|
2169
|
+
}
|
|
2170
|
+
if (Date.now() > resetToken.expiresAt) {
|
|
2171
|
+
await this.resetStore.consume(token);
|
|
2172
|
+
return { status: 410, body: { error: "Password reset token has expired." } };
|
|
2173
|
+
}
|
|
2174
|
+
await this.resetStore.consume(token);
|
|
2175
|
+
const hashed = await hashPassword(newPassword);
|
|
2176
|
+
await this.userStore.updatePassword(resetToken.userId, hashed.hash, hashed.salt);
|
|
2177
|
+
return { status: 200, body: { data: { message: "Password has been reset successfully." } } };
|
|
1373
2178
|
}
|
|
1374
2179
|
/**
|
|
1375
|
-
*
|
|
1376
|
-
* Delegates to sendVerification with rate limiting.
|
|
2180
|
+
* Change password for an authenticated user (requires current password verification).
|
|
1377
2181
|
*/
|
|
1378
|
-
async
|
|
2182
|
+
async changePassword(userId, currentPassword, newPassword) {
|
|
2183
|
+
if (typeof newPassword !== "string" || newPassword.length < MIN_PASSWORD_LENGTH2) {
|
|
2184
|
+
return {
|
|
2185
|
+
status: 400,
|
|
2186
|
+
body: { error: `New password must be at least ${MIN_PASSWORD_LENGTH2} characters.` }
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
if (newPassword.length > MAX_PASSWORD_LENGTH2) {
|
|
2190
|
+
return {
|
|
2191
|
+
status: 400,
|
|
2192
|
+
body: { error: `New password must be at most ${MAX_PASSWORD_LENGTH2} characters.` }
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
1379
2195
|
const user = await this.userStore.findById(userId);
|
|
1380
2196
|
if (!user) {
|
|
1381
2197
|
return { status: 404, body: { error: "User not found." } };
|
|
1382
2198
|
}
|
|
1383
|
-
|
|
2199
|
+
const { verifyPassword: verifyPassword2 } = await import("./password-hash-QRBG6BNJ.js");
|
|
2200
|
+
const isValid = await verifyPassword2(currentPassword, user.passwordHash, user.salt);
|
|
2201
|
+
if (!isValid) {
|
|
2202
|
+
return { status: 401, body: { error: "Current password is incorrect." } };
|
|
2203
|
+
}
|
|
2204
|
+
const hashed = await hashPassword(newPassword);
|
|
2205
|
+
await this.userStore.updatePassword(userId, hashed.hash, hashed.salt);
|
|
2206
|
+
return { status: 200, body: { data: { message: "Password changed successfully." } } };
|
|
1384
2207
|
}
|
|
1385
2208
|
};
|
|
1386
|
-
function
|
|
2209
|
+
function generateSecureToken() {
|
|
1387
2210
|
const bytes = new Uint8Array(32);
|
|
1388
2211
|
globalThis.crypto.getRandomValues(bytes);
|
|
1389
2212
|
let binary = "";
|
|
@@ -1393,244 +2216,162 @@ function generateSecureToken2() {
|
|
|
1393
2216
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1394
2217
|
}
|
|
1395
2218
|
|
|
1396
|
-
// src/provider/built-in/
|
|
1397
|
-
import {
|
|
1398
|
-
|
|
1399
|
-
|
|
2219
|
+
// src/provider/built-in/email-verification.ts
|
|
2220
|
+
import { KoraError as KoraError6 } from "@korajs/core";
|
|
2221
|
+
var EmailVerificationError = class extends KoraError6 {
|
|
2222
|
+
constructor(message, code, context) {
|
|
2223
|
+
super(message, code, context);
|
|
2224
|
+
this.name = "EmailVerificationError";
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
var VerificationTokenExpiredError = class extends EmailVerificationError {
|
|
1400
2228
|
constructor() {
|
|
1401
|
-
super("
|
|
1402
|
-
this.name = "DuplicateEmailError";
|
|
2229
|
+
super("Email verification token has expired.", "VERIFICATION_TOKEN_EXPIRED");
|
|
1403
2230
|
}
|
|
1404
2231
|
};
|
|
1405
|
-
var
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
/** Users indexed by email (lowercase) for fast lookup */
|
|
1409
|
-
usersByEmail = /* @__PURE__ */ new Map();
|
|
1410
|
-
/** Devices indexed by device ID */
|
|
1411
|
-
devicesById = /* @__PURE__ */ new Map();
|
|
1412
|
-
/** Device IDs indexed by user ID for fast listing */
|
|
1413
|
-
devicesByUserId = /* @__PURE__ */ new Map();
|
|
1414
|
-
/**
|
|
1415
|
-
* Create a new user account.
|
|
1416
|
-
*
|
|
1417
|
-
* @param params - User creation parameters
|
|
1418
|
-
* @param params.email - The user's email address (must be unique, case-insensitive)
|
|
1419
|
-
* @param params.passwordHash - Hex-encoded PBKDF2 derived key
|
|
1420
|
-
* @param params.salt - Hex-encoded salt used during hashing
|
|
1421
|
-
* @param params.name - The user's display name
|
|
1422
|
-
* @returns The created user (without sensitive credential fields)
|
|
1423
|
-
* @throws {DuplicateEmailError} If a user with the same email already exists
|
|
1424
|
-
*/
|
|
1425
|
-
async createUser(params) {
|
|
1426
|
-
const normalizedEmail = params.email.toLowerCase();
|
|
1427
|
-
if (this.usersByEmail.has(normalizedEmail)) {
|
|
1428
|
-
throw new DuplicateEmailError();
|
|
1429
|
-
}
|
|
1430
|
-
const now = Date.now();
|
|
1431
|
-
const id = randomUUID2();
|
|
1432
|
-
const storedUser = {
|
|
1433
|
-
id,
|
|
1434
|
-
email: normalizedEmail,
|
|
1435
|
-
name: params.name,
|
|
1436
|
-
emailVerified: false,
|
|
1437
|
-
createdAt: now,
|
|
1438
|
-
passwordHash: params.passwordHash,
|
|
1439
|
-
salt: params.salt
|
|
1440
|
-
};
|
|
1441
|
-
this.usersById.set(id, storedUser);
|
|
1442
|
-
this.usersByEmail.set(normalizedEmail, storedUser);
|
|
1443
|
-
return toAuthUser(storedUser);
|
|
2232
|
+
var VerificationTokenNotFoundError = class extends EmailVerificationError {
|
|
2233
|
+
constructor() {
|
|
2234
|
+
super("Email verification token not found or already used.", "VERIFICATION_TOKEN_NOT_FOUND");
|
|
1444
2235
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
*/
|
|
1451
|
-
async findByEmail(email) {
|
|
1452
|
-
return this.usersByEmail.get(email.toLowerCase()) ?? null;
|
|
2236
|
+
};
|
|
2237
|
+
var InMemoryEmailVerificationStore = class {
|
|
2238
|
+
tokens = /* @__PURE__ */ new Map();
|
|
2239
|
+
async store(token) {
|
|
2240
|
+
this.tokens.set(token.token, token);
|
|
1453
2241
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
*
|
|
1457
|
-
* @param id - The user ID to search for
|
|
1458
|
-
* @returns The stored user record including credentials, or null if not found
|
|
1459
|
-
*/
|
|
1460
|
-
async findById(id) {
|
|
1461
|
-
return this.usersById.get(id) ?? null;
|
|
2242
|
+
async get(token) {
|
|
2243
|
+
return this.tokens.get(token) ?? null;
|
|
1462
2244
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
* returned as-is (idempotent registration). If it was previously revoked,
|
|
1468
|
-
* it is re-activated with updated details.
|
|
1469
|
-
*
|
|
1470
|
-
* @param params - Device registration parameters
|
|
1471
|
-
* @param params.id - Unique device identifier
|
|
1472
|
-
* @param params.userId - ID of the user who owns the device
|
|
1473
|
-
* @param params.publicKey - Base64url-encoded device public key or thumbprint
|
|
1474
|
-
* @param params.name - Human-readable device name
|
|
1475
|
-
* @returns The registered device record
|
|
1476
|
-
*/
|
|
1477
|
-
async registerDevice(params) {
|
|
1478
|
-
const existing = this.devicesById.get(params.id);
|
|
1479
|
-
if (existing !== void 0 && !existing.revoked) {
|
|
1480
|
-
return existing;
|
|
1481
|
-
}
|
|
1482
|
-
const now = Date.now();
|
|
1483
|
-
const device = {
|
|
1484
|
-
id: params.id,
|
|
1485
|
-
userId: params.userId,
|
|
1486
|
-
publicKey: params.publicKey,
|
|
1487
|
-
name: params.name,
|
|
1488
|
-
revoked: false,
|
|
1489
|
-
createdAt: now,
|
|
1490
|
-
lastSeenAt: now
|
|
1491
|
-
};
|
|
1492
|
-
this.devicesById.set(params.id, device);
|
|
1493
|
-
let userDevices = this.devicesByUserId.get(params.userId);
|
|
1494
|
-
if (userDevices === void 0) {
|
|
1495
|
-
userDevices = /* @__PURE__ */ new Set();
|
|
1496
|
-
this.devicesByUserId.set(params.userId, userDevices);
|
|
2245
|
+
async consume(token) {
|
|
2246
|
+
const entry = this.tokens.get(token);
|
|
2247
|
+
if (entry) {
|
|
2248
|
+
this.tokens.set(token, { ...entry, consumed: true });
|
|
1497
2249
|
}
|
|
1498
|
-
userDevices.add(params.id);
|
|
1499
|
-
return device;
|
|
1500
2250
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
async findDevice(deviceId) {
|
|
1508
|
-
return this.devicesById.get(deviceId) ?? null;
|
|
1509
|
-
}
|
|
1510
|
-
/**
|
|
1511
|
-
* List all devices registered for a user.
|
|
1512
|
-
*
|
|
1513
|
-
* @param userId - The user ID whose devices to list
|
|
1514
|
-
* @returns Array of device records (includes revoked devices)
|
|
1515
|
-
*/
|
|
1516
|
-
async listDevices(userId) {
|
|
1517
|
-
const deviceIds = this.devicesByUserId.get(userId);
|
|
1518
|
-
if (deviceIds === void 0) {
|
|
1519
|
-
return [];
|
|
1520
|
-
}
|
|
1521
|
-
const devices = [];
|
|
1522
|
-
for (const deviceId of deviceIds) {
|
|
1523
|
-
const device = this.devicesById.get(deviceId);
|
|
1524
|
-
if (device !== void 0) {
|
|
1525
|
-
devices.push(device);
|
|
2251
|
+
async countActiveForUser(userId) {
|
|
2252
|
+
const now = Date.now();
|
|
2253
|
+
let count = 0;
|
|
2254
|
+
for (const token of this.tokens.values()) {
|
|
2255
|
+
if (token.userId === userId && !token.consumed && now < token.expiresAt) {
|
|
2256
|
+
count++;
|
|
1526
2257
|
}
|
|
1527
2258
|
}
|
|
1528
|
-
return
|
|
2259
|
+
return count;
|
|
1529
2260
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
async revokeDevice(deviceId) {
|
|
1539
|
-
const device = this.devicesById.get(deviceId);
|
|
1540
|
-
if (device !== void 0) {
|
|
1541
|
-
device.revoked = true;
|
|
2261
|
+
async cleanExpired() {
|
|
2262
|
+
const now = Date.now();
|
|
2263
|
+
let count = 0;
|
|
2264
|
+
for (const [key, token] of this.tokens) {
|
|
2265
|
+
if (now > token.expiresAt) {
|
|
2266
|
+
this.tokens.delete(key);
|
|
2267
|
+
count++;
|
|
2268
|
+
}
|
|
1542
2269
|
}
|
|
2270
|
+
return count;
|
|
1543
2271
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
this.
|
|
1555
|
-
this.
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
*
|
|
1560
|
-
* @param userId - The user whose password to update
|
|
1561
|
-
* @param passwordHash - New hex-encoded PBKDF2 derived key
|
|
1562
|
-
* @param salt - New hex-encoded salt
|
|
1563
|
-
*/
|
|
1564
|
-
async updatePassword(userId, passwordHash, salt) {
|
|
1565
|
-
const user = this.usersById.get(userId);
|
|
1566
|
-
if (!user) return;
|
|
1567
|
-
const updated = { ...user, passwordHash, salt };
|
|
1568
|
-
this.usersById.set(userId, updated);
|
|
1569
|
-
this.usersByEmail.set(user.email, updated);
|
|
1570
|
-
}
|
|
1571
|
-
/**
|
|
1572
|
-
* List all users. For admin/development use.
|
|
1573
|
-
*/
|
|
1574
|
-
async listAll() {
|
|
1575
|
-
return [...this.usersById.values()];
|
|
2272
|
+
};
|
|
2273
|
+
var DEFAULT_TOKEN_TTL_MS2 = 24 * 60 * 60 * 1e3;
|
|
2274
|
+
var DEFAULT_MAX_REQUESTS2 = 3;
|
|
2275
|
+
var EmailVerificationManager = class {
|
|
2276
|
+
userStore;
|
|
2277
|
+
verificationStore;
|
|
2278
|
+
tokenTtlMs;
|
|
2279
|
+
maxRequestsPerUser;
|
|
2280
|
+
onVerificationRequired;
|
|
2281
|
+
constructor(config) {
|
|
2282
|
+
this.userStore = config.userStore;
|
|
2283
|
+
this.verificationStore = config.verificationStore ?? new InMemoryEmailVerificationStore();
|
|
2284
|
+
this.tokenTtlMs = config.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS2;
|
|
2285
|
+
this.maxRequestsPerUser = config.maxRequestsPerUser ?? DEFAULT_MAX_REQUESTS2;
|
|
2286
|
+
this.onVerificationRequired = config.onVerificationRequired;
|
|
1576
2287
|
}
|
|
1577
2288
|
/**
|
|
1578
|
-
*
|
|
2289
|
+
* Send a verification email for a user.
|
|
2290
|
+
* Rate-limited to prevent abuse.
|
|
1579
2291
|
*/
|
|
1580
|
-
async
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1583
|
-
if (
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
2292
|
+
async sendVerification(userId, email) {
|
|
2293
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
2294
|
+
const activeCount = await this.verificationStore.countActiveForUser(userId);
|
|
2295
|
+
if (activeCount >= this.maxRequestsPerUser) {
|
|
2296
|
+
return {
|
|
2297
|
+
status: 429,
|
|
2298
|
+
body: { error: "Too many verification requests. Please try again later." }
|
|
2299
|
+
};
|
|
1588
2300
|
}
|
|
1589
|
-
|
|
2301
|
+
const token = generateSecureToken2();
|
|
2302
|
+
const now = Date.now();
|
|
2303
|
+
const verificationToken = {
|
|
2304
|
+
token,
|
|
2305
|
+
userId,
|
|
2306
|
+
email: normalizedEmail,
|
|
2307
|
+
createdAt: now,
|
|
2308
|
+
expiresAt: now + this.tokenTtlMs,
|
|
2309
|
+
consumed: false
|
|
2310
|
+
};
|
|
2311
|
+
await this.verificationStore.store(verificationToken);
|
|
2312
|
+
if (this.onVerificationRequired) {
|
|
2313
|
+
try {
|
|
2314
|
+
await this.onVerificationRequired(normalizedEmail, token, verificationToken.expiresAt);
|
|
2315
|
+
} catch {
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
const responseData = {
|
|
2319
|
+
message: "Verification email sent."
|
|
2320
|
+
};
|
|
2321
|
+
if (!this.onVerificationRequired) {
|
|
2322
|
+
responseData.token = token;
|
|
2323
|
+
}
|
|
2324
|
+
return { status: 200, body: { data: responseData } };
|
|
1590
2325
|
}
|
|
1591
2326
|
/**
|
|
1592
|
-
*
|
|
2327
|
+
* Verify an email using a verification token.
|
|
1593
2328
|
*/
|
|
1594
|
-
async
|
|
1595
|
-
const
|
|
1596
|
-
if (!
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
this.devicesById.delete(deviceId);
|
|
1603
|
-
}
|
|
1604
|
-
this.devicesByUserId.delete(userId);
|
|
2329
|
+
async verifyEmail(token) {
|
|
2330
|
+
const verificationToken = await this.verificationStore.get(token);
|
|
2331
|
+
if (!verificationToken || verificationToken.consumed) {
|
|
2332
|
+
return { status: 404, body: { error: "Verification token not found or already used." } };
|
|
2333
|
+
}
|
|
2334
|
+
if (Date.now() > verificationToken.expiresAt) {
|
|
2335
|
+
await this.verificationStore.consume(token);
|
|
2336
|
+
return { status: 410, body: { error: "Verification token has expired." } };
|
|
1605
2337
|
}
|
|
2338
|
+
await this.verificationStore.consume(token);
|
|
2339
|
+
await this.userStore.setEmailVerified(verificationToken.userId, true);
|
|
2340
|
+
return {
|
|
2341
|
+
status: 200,
|
|
2342
|
+
body: {
|
|
2343
|
+
data: {
|
|
2344
|
+
message: "Email verified successfully.",
|
|
2345
|
+
userId: verificationToken.userId,
|
|
2346
|
+
email: verificationToken.email
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
1606
2350
|
}
|
|
1607
2351
|
/**
|
|
1608
|
-
*
|
|
1609
|
-
*
|
|
1610
|
-
* Called when a device authenticates or syncs to track activity.
|
|
1611
|
-
* If the device does not exist, this is a no-op.
|
|
1612
|
-
*
|
|
1613
|
-
* @param deviceId - The ID of the device to update
|
|
2352
|
+
* Resend verification email for a user.
|
|
2353
|
+
* Delegates to sendVerification with rate limiting.
|
|
1614
2354
|
*/
|
|
1615
|
-
async
|
|
1616
|
-
const
|
|
1617
|
-
if (
|
|
1618
|
-
|
|
2355
|
+
async resendVerification(userId) {
|
|
2356
|
+
const user = await this.userStore.findById(userId);
|
|
2357
|
+
if (!user) {
|
|
2358
|
+
return { status: 404, body: { error: "User not found." } };
|
|
1619
2359
|
}
|
|
2360
|
+
return this.sendVerification(userId, user.email);
|
|
1620
2361
|
}
|
|
1621
2362
|
};
|
|
1622
|
-
function
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
2363
|
+
function generateSecureToken2() {
|
|
2364
|
+
const bytes = new Uint8Array(32);
|
|
2365
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
2366
|
+
let binary = "";
|
|
2367
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2368
|
+
binary += String.fromCharCode(bytes[i]);
|
|
2369
|
+
}
|
|
2370
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1630
2371
|
}
|
|
1631
2372
|
|
|
1632
2373
|
// src/provider/built-in/sqlite-user-store.ts
|
|
1633
|
-
import { randomUUID as
|
|
2374
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
1634
2375
|
var SqliteUserStore = class {
|
|
1635
2376
|
db;
|
|
1636
2377
|
constructor(db) {
|
|
@@ -1667,7 +2408,7 @@ var SqliteUserStore = class {
|
|
|
1667
2408
|
async createUser(params) {
|
|
1668
2409
|
const normalizedEmail = params.email.toLowerCase();
|
|
1669
2410
|
const now = Date.now();
|
|
1670
|
-
const id =
|
|
2411
|
+
const id = randomUUID5();
|
|
1671
2412
|
try {
|
|
1672
2413
|
this.db.prepare(`
|
|
1673
2414
|
INSERT INTO auth_users (id, email, name, email_verified, created_at, password_hash, salt)
|
|
@@ -1795,7 +2536,7 @@ function rowToDevice(row) {
|
|
|
1795
2536
|
}
|
|
1796
2537
|
|
|
1797
2538
|
// src/provider/built-in/postgres-user-store.ts
|
|
1798
|
-
import { randomUUID as
|
|
2539
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
1799
2540
|
var PostgresUserStore = class {
|
|
1800
2541
|
sql;
|
|
1801
2542
|
ready;
|
|
@@ -1837,7 +2578,7 @@ var PostgresUserStore = class {
|
|
|
1837
2578
|
await this.ready;
|
|
1838
2579
|
const normalizedEmail = params.email.toLowerCase();
|
|
1839
2580
|
const now = Date.now();
|
|
1840
|
-
const id =
|
|
2581
|
+
const id = randomUUID6();
|
|
1841
2582
|
try {
|
|
1842
2583
|
await this.sql`
|
|
1843
2584
|
INSERT INTO auth_users (id, email, name, email_verified, created_at, password_hash, salt)
|
|
@@ -2073,8 +2814,8 @@ var BuiltInProvider = class {
|
|
|
2073
2814
|
};
|
|
2074
2815
|
|
|
2075
2816
|
// src/provider/external/external-jwt-provider.ts
|
|
2076
|
-
import { KoraError as
|
|
2077
|
-
var ExternalAuthOperationNotSupportedError = class extends
|
|
2817
|
+
import { KoraError as KoraError7 } from "@korajs/core";
|
|
2818
|
+
var ExternalAuthOperationNotSupportedError = class extends KoraError7 {
|
|
2078
2819
|
constructor(operation, provider) {
|
|
2079
2820
|
super(
|
|
2080
2821
|
`The "${operation}" operation is not supported by the external auth provider "${provider}". Perform this operation through your external auth provider's SDK or dashboard instead.`,
|
|
@@ -2084,7 +2825,7 @@ var ExternalAuthOperationNotSupportedError = class extends KoraError5 {
|
|
|
2084
2825
|
this.name = "ExternalAuthOperationNotSupportedError";
|
|
2085
2826
|
}
|
|
2086
2827
|
};
|
|
2087
|
-
var ExternalTokenValidationError = class extends
|
|
2828
|
+
var ExternalTokenValidationError = class extends KoraError7 {
|
|
2088
2829
|
constructor(reason, context) {
|
|
2089
2830
|
super(`External token validation failed: ${reason}`, "AUTH_EXTERNAL_TOKEN_INVALID", context);
|
|
2090
2831
|
this.name = "ExternalTokenValidationError";
|
|
@@ -2377,8 +3118,8 @@ function createSupabaseAdapter(config) {
|
|
|
2377
3118
|
|
|
2378
3119
|
// src/passkey/passkey-server.ts
|
|
2379
3120
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
2380
|
-
import { KoraError as
|
|
2381
|
-
var PasskeyVerificationError = class extends
|
|
3121
|
+
import { KoraError as KoraError8 } from "@korajs/core";
|
|
3122
|
+
var PasskeyVerificationError = class extends KoraError8 {
|
|
2382
3123
|
constructor(message, context) {
|
|
2383
3124
|
super(message, "PASSKEY_VERIFICATION_ERROR", context);
|
|
2384
3125
|
this.name = "PasskeyVerificationError";
|
|
@@ -2704,7 +3445,7 @@ function copyComponentToP1363(component, target, targetOffset, componentLength)
|
|
|
2704
3445
|
}
|
|
2705
3446
|
|
|
2706
3447
|
// src/org/org-types.ts
|
|
2707
|
-
import { KoraError as
|
|
3448
|
+
import { KoraError as KoraError9 } from "@korajs/core";
|
|
2708
3449
|
var ORG_ROLES = ["owner", "admin", "member", "viewer", "billing"];
|
|
2709
3450
|
var ROLE_HIERARCHY = {
|
|
2710
3451
|
viewer: 10,
|
|
@@ -2717,7 +3458,7 @@ function hasRoleLevel(userRole, requiredRole) {
|
|
|
2717
3458
|
return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[requiredRole];
|
|
2718
3459
|
}
|
|
2719
3460
|
var INVITATION_STATUSES = ["pending", "accepted", "revoked", "expired"];
|
|
2720
|
-
var OrgError = class extends
|
|
3461
|
+
var OrgError = class extends KoraError9 {
|
|
2721
3462
|
constructor(message, code, context) {
|
|
2722
3463
|
super(message, code, context);
|
|
2723
3464
|
this.name = "OrgError";
|
|
@@ -3509,7 +4250,7 @@ function slugify(name) {
|
|
|
3509
4250
|
}
|
|
3510
4251
|
|
|
3511
4252
|
// src/rbac/rbac-types.ts
|
|
3512
|
-
import { KoraError as
|
|
4253
|
+
import { KoraError as KoraError10 } from "@korajs/core";
|
|
3513
4254
|
function parsePermission(permission) {
|
|
3514
4255
|
const colonIndex = permission.indexOf(":");
|
|
3515
4256
|
if (colonIndex === -1) {
|
|
@@ -3553,7 +4294,7 @@ var BUILT_IN_ROLES = [
|
|
|
3553
4294
|
permissions: ["*:*"]
|
|
3554
4295
|
}
|
|
3555
4296
|
];
|
|
3556
|
-
var RbacError = class extends
|
|
4297
|
+
var RbacError = class extends KoraError10 {
|
|
3557
4298
|
constructor(message, code, context) {
|
|
3558
4299
|
super(message, code, context);
|
|
3559
4300
|
this.name = "RbacError";
|
|
@@ -3872,302 +4613,409 @@ var OrgScopeResolver = class {
|
|
|
3872
4613
|
}
|
|
3873
4614
|
};
|
|
3874
4615
|
|
|
3875
|
-
// src/provider/oauth/oauth-
|
|
3876
|
-
import {
|
|
3877
|
-
var
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
this.
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
var OAuthStateMismatchError = class extends OAuthError {
|
|
3884
|
-
constructor() {
|
|
3885
|
-
super("OAuth state parameter does not match. Possible CSRF attack.", "OAUTH_STATE_MISMATCH");
|
|
3886
|
-
}
|
|
3887
|
-
};
|
|
3888
|
-
var OAuthCodeExchangeError = class extends OAuthError {
|
|
3889
|
-
constructor(details) {
|
|
3890
|
-
super(
|
|
3891
|
-
`Failed to exchange authorization code for tokens.${details ? ` ${details}` : ""}`,
|
|
3892
|
-
"OAUTH_CODE_EXCHANGE_FAILED",
|
|
3893
|
-
details ? { details } : void 0
|
|
3894
|
-
);
|
|
4616
|
+
// src/provider/oauth/sqlite-oauth-store.ts
|
|
4617
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
4618
|
+
var SqliteOAuthStateStore = class {
|
|
4619
|
+
db;
|
|
4620
|
+
constructor(db) {
|
|
4621
|
+
this.db = db;
|
|
4622
|
+
this.db.pragma("journal_mode = WAL");
|
|
4623
|
+
this.ensureTables();
|
|
3895
4624
|
}
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
4625
|
+
async store(state) {
|
|
4626
|
+
this.db.prepare(`
|
|
4627
|
+
INSERT OR REPLACE INTO auth_oauth_states
|
|
4628
|
+
(state, provider, redirect_uri, created_at, expires_at, metadata_json, code_verifier)
|
|
4629
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
4630
|
+
`).run(
|
|
4631
|
+
state.state,
|
|
4632
|
+
state.provider,
|
|
4633
|
+
state.redirectUri,
|
|
4634
|
+
state.createdAt,
|
|
4635
|
+
state.expiresAt,
|
|
4636
|
+
state.metadata ? JSON.stringify(state.metadata) : null,
|
|
4637
|
+
state.codeVerifier ?? null
|
|
3903
4638
|
);
|
|
3904
4639
|
}
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
4640
|
+
async consume(stateValue) {
|
|
4641
|
+
const consumeInTransaction = this.db.transaction(() => {
|
|
4642
|
+
const row = this.db.prepare("SELECT * FROM auth_oauth_states WHERE state = ?").get(stateValue);
|
|
4643
|
+
if (!row) return null;
|
|
4644
|
+
this.db.prepare("DELETE FROM auth_oauth_states WHERE state = ?").run(stateValue);
|
|
4645
|
+
if (Date.now() > row.expires_at) return null;
|
|
4646
|
+
return rowToOAuthState(row);
|
|
3910
4647
|
});
|
|
4648
|
+
return consumeInTransaction();
|
|
3911
4649
|
}
|
|
3912
|
-
|
|
4650
|
+
async cleanExpired() {
|
|
4651
|
+
const result = this.db.prepare("DELETE FROM auth_oauth_states WHERE expires_at < ?").run(Date.now());
|
|
4652
|
+
return result.changes ?? 0;
|
|
4653
|
+
}
|
|
4654
|
+
ensureTables() {
|
|
4655
|
+
this.db.exec(`
|
|
4656
|
+
CREATE TABLE IF NOT EXISTS auth_oauth_states (
|
|
4657
|
+
state TEXT PRIMARY KEY,
|
|
4658
|
+
provider TEXT NOT NULL,
|
|
4659
|
+
redirect_uri TEXT NOT NULL,
|
|
4660
|
+
created_at INTEGER NOT NULL,
|
|
4661
|
+
expires_at INTEGER NOT NULL,
|
|
4662
|
+
metadata_json TEXT,
|
|
4663
|
+
code_verifier TEXT
|
|
4664
|
+
);
|
|
3913
4665
|
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
states = /* @__PURE__ */ new Map();
|
|
3918
|
-
async store(state) {
|
|
3919
|
-
this.states.set(state.state, state);
|
|
4666
|
+
CREATE INDEX IF NOT EXISTS idx_auth_oauth_states_expires_at
|
|
4667
|
+
ON auth_oauth_states(expires_at);
|
|
4668
|
+
`);
|
|
3920
4669
|
}
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
4670
|
+
};
|
|
4671
|
+
var SqliteLinkedIdentityStore = class {
|
|
4672
|
+
db;
|
|
4673
|
+
constructor(db) {
|
|
4674
|
+
this.db = db;
|
|
4675
|
+
this.db.pragma("journal_mode = WAL");
|
|
4676
|
+
this.ensureTables();
|
|
3927
4677
|
}
|
|
3928
|
-
async
|
|
3929
|
-
const
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
4678
|
+
async findByProvider(provider, providerUserId) {
|
|
4679
|
+
const row = this.db.prepare(`
|
|
4680
|
+
SELECT * FROM auth_linked_identities
|
|
4681
|
+
WHERE provider = ? AND provider_user_id = ?
|
|
4682
|
+
`).get(provider, providerUserId);
|
|
4683
|
+
return row ? rowToLinkedIdentity(row) : null;
|
|
4684
|
+
}
|
|
4685
|
+
async findByUser(userId) {
|
|
4686
|
+
const rows = this.db.prepare(`
|
|
4687
|
+
SELECT * FROM auth_linked_identities
|
|
4688
|
+
WHERE user_id = ?
|
|
4689
|
+
ORDER BY linked_at ASC
|
|
4690
|
+
`).all(userId);
|
|
4691
|
+
return rows.map(rowToLinkedIdentity);
|
|
4692
|
+
}
|
|
4693
|
+
async create(params) {
|
|
4694
|
+
const identity = {
|
|
4695
|
+
id: randomUUID7(),
|
|
4696
|
+
userId: params.userId,
|
|
4697
|
+
provider: params.provider,
|
|
4698
|
+
providerUserId: params.providerUserId,
|
|
4699
|
+
email: params.email,
|
|
4700
|
+
linkedAt: Date.now()
|
|
4701
|
+
};
|
|
4702
|
+
try {
|
|
4703
|
+
this.db.prepare(`
|
|
4704
|
+
INSERT INTO auth_linked_identities
|
|
4705
|
+
(id, user_id, provider, provider_user_id, email, linked_at)
|
|
4706
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
4707
|
+
`).run(
|
|
4708
|
+
identity.id,
|
|
4709
|
+
identity.userId,
|
|
4710
|
+
identity.provider,
|
|
4711
|
+
identity.providerUserId,
|
|
4712
|
+
identity.email,
|
|
4713
|
+
identity.linkedAt
|
|
4714
|
+
);
|
|
4715
|
+
} catch (error) {
|
|
4716
|
+
if (error instanceof Error && error.message.includes("UNIQUE constraint failed")) {
|
|
4717
|
+
throw new DuplicateLinkedIdentityError(params.provider);
|
|
3935
4718
|
}
|
|
4719
|
+
throw error;
|
|
3936
4720
|
}
|
|
3937
|
-
return
|
|
4721
|
+
return identity;
|
|
4722
|
+
}
|
|
4723
|
+
async delete(userId, provider) {
|
|
4724
|
+
this.db.prepare("DELETE FROM auth_linked_identities WHERE user_id = ? AND provider = ?").run(userId, provider);
|
|
4725
|
+
}
|
|
4726
|
+
ensureTables() {
|
|
4727
|
+
this.db.exec(`
|
|
4728
|
+
CREATE TABLE IF NOT EXISTS auth_linked_identities (
|
|
4729
|
+
id TEXT PRIMARY KEY,
|
|
4730
|
+
user_id TEXT NOT NULL,
|
|
4731
|
+
provider TEXT NOT NULL,
|
|
4732
|
+
provider_user_id TEXT NOT NULL,
|
|
4733
|
+
email TEXT,
|
|
4734
|
+
linked_at INTEGER NOT NULL,
|
|
4735
|
+
UNIQUE(provider, provider_user_id),
|
|
4736
|
+
UNIQUE(user_id, provider)
|
|
4737
|
+
);
|
|
4738
|
+
|
|
4739
|
+
CREATE INDEX IF NOT EXISTS idx_auth_linked_identities_user_id
|
|
4740
|
+
ON auth_linked_identities(user_id);
|
|
4741
|
+
`);
|
|
3938
4742
|
}
|
|
3939
4743
|
};
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
4744
|
+
async function createSqliteOAuthStateStore(options) {
|
|
4745
|
+
const Database = await loadBetterSqlite32();
|
|
4746
|
+
const db = new Database(options.filename);
|
|
4747
|
+
return new SqliteOAuthStateStore(db);
|
|
4748
|
+
}
|
|
4749
|
+
async function createSqliteLinkedIdentityStore(options) {
|
|
4750
|
+
const Database = await loadBetterSqlite32();
|
|
4751
|
+
const db = new Database(options.filename);
|
|
4752
|
+
return new SqliteLinkedIdentityStore(db);
|
|
4753
|
+
}
|
|
4754
|
+
async function createSqliteOAuthStores(options) {
|
|
4755
|
+
const Database = await loadBetterSqlite32();
|
|
4756
|
+
const db = new Database(options.filename);
|
|
4757
|
+
return {
|
|
4758
|
+
stateStore: new SqliteOAuthStateStore(db),
|
|
4759
|
+
linkedIdentityStore: new SqliteLinkedIdentityStore(db)
|
|
4760
|
+
};
|
|
4761
|
+
}
|
|
4762
|
+
async function loadBetterSqlite32() {
|
|
4763
|
+
try {
|
|
4764
|
+
const { createRequire } = await import("module");
|
|
4765
|
+
const require2 = createRequire(import.meta.url);
|
|
4766
|
+
return require2("better-sqlite3");
|
|
4767
|
+
} catch {
|
|
4768
|
+
throw new Error(
|
|
4769
|
+
'SQLite OAuth stores require the "better-sqlite3" package. Install it in your project dependencies.'
|
|
4770
|
+
);
|
|
3952
4771
|
}
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
4772
|
+
}
|
|
4773
|
+
function rowToOAuthState(row) {
|
|
4774
|
+
return {
|
|
4775
|
+
state: row.state,
|
|
4776
|
+
provider: row.provider,
|
|
4777
|
+
redirectUri: row.redirect_uri,
|
|
4778
|
+
createdAt: row.created_at,
|
|
4779
|
+
expiresAt: row.expires_at,
|
|
4780
|
+
metadata: parseMetadata(row.metadata_json),
|
|
4781
|
+
codeVerifier: row.code_verifier ?? void 0
|
|
4782
|
+
};
|
|
4783
|
+
}
|
|
4784
|
+
function parseMetadata(value) {
|
|
4785
|
+
if (!value) return void 0;
|
|
4786
|
+
const parsed = JSON.parse(value);
|
|
4787
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
4788
|
+
}
|
|
4789
|
+
function rowToLinkedIdentity(row) {
|
|
4790
|
+
return {
|
|
4791
|
+
id: row.id,
|
|
4792
|
+
userId: row.user_id,
|
|
4793
|
+
provider: row.provider,
|
|
4794
|
+
providerUserId: row.provider_user_id,
|
|
4795
|
+
email: row.email,
|
|
4796
|
+
linkedAt: row.linked_at
|
|
4797
|
+
};
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
// src/provider/oauth/postgres-oauth-store.ts
|
|
4801
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
4802
|
+
var PostgresOAuthStateStore = class {
|
|
4803
|
+
sql;
|
|
4804
|
+
ready;
|
|
4805
|
+
constructor(sql) {
|
|
4806
|
+
this.sql = sql;
|
|
4807
|
+
this.ready = this.ensureTables();
|
|
4808
|
+
}
|
|
4809
|
+
async store(state) {
|
|
4810
|
+
await this.ready;
|
|
4811
|
+
await this.sql`
|
|
4812
|
+
INSERT INTO auth_oauth_states
|
|
4813
|
+
(state, provider, redirect_uri, created_at, expires_at, metadata_json, code_verifier)
|
|
4814
|
+
VALUES (
|
|
4815
|
+
${state.state},
|
|
4816
|
+
${state.provider},
|
|
4817
|
+
${state.redirectUri},
|
|
4818
|
+
${state.createdAt},
|
|
4819
|
+
${state.expiresAt},
|
|
4820
|
+
${state.metadata ? JSON.stringify(state.metadata) : null},
|
|
4821
|
+
${state.codeVerifier ?? null}
|
|
4822
|
+
)
|
|
4823
|
+
ON CONFLICT (state) DO UPDATE SET
|
|
4824
|
+
provider = EXCLUDED.provider,
|
|
4825
|
+
redirect_uri = EXCLUDED.redirect_uri,
|
|
4826
|
+
created_at = EXCLUDED.created_at,
|
|
4827
|
+
expires_at = EXCLUDED.expires_at,
|
|
4828
|
+
metadata_json = EXCLUDED.metadata_json,
|
|
4829
|
+
code_verifier = EXCLUDED.code_verifier
|
|
4830
|
+
`;
|
|
4831
|
+
}
|
|
4832
|
+
async consume(stateValue) {
|
|
4833
|
+
await this.ready;
|
|
4834
|
+
return this.sql.begin(async (tx) => {
|
|
4835
|
+
const rows = await tx`
|
|
4836
|
+
DELETE FROM auth_oauth_states
|
|
4837
|
+
WHERE state = ${stateValue}
|
|
4838
|
+
RETURNING *
|
|
4839
|
+
`;
|
|
4840
|
+
const row = rows[0];
|
|
4841
|
+
if (!row || Date.now() > Number(row.expires_at)) {
|
|
4842
|
+
return null;
|
|
4843
|
+
}
|
|
4844
|
+
return rowToOAuthState2(row);
|
|
3976
4845
|
});
|
|
3977
|
-
const url = `${provider.authorizationUrl}?${params.toString()}`;
|
|
3978
|
-
return { url, state };
|
|
3979
4846
|
}
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
* @returns Tokens and user info from the provider
|
|
3989
|
-
*/
|
|
3990
|
-
async handleCallback(providerId, code, state) {
|
|
3991
|
-
const provider = this.getProvider(providerId);
|
|
3992
|
-
const oauthState = await this.stateStore.consume(state);
|
|
3993
|
-
if (!oauthState || oauthState.provider !== providerId) {
|
|
3994
|
-
throw new OAuthStateMismatchError();
|
|
3995
|
-
}
|
|
3996
|
-
const tokens = await this.exchangeCodeForTokens(provider, code);
|
|
3997
|
-
const userInfo = await this.fetchUserInfo(provider, tokens.accessToken);
|
|
3998
|
-
return { tokens, userInfo, stateMetadata: oauthState.metadata };
|
|
4847
|
+
async cleanExpired() {
|
|
4848
|
+
await this.ready;
|
|
4849
|
+
const rows = await this.sql`
|
|
4850
|
+
DELETE FROM auth_oauth_states
|
|
4851
|
+
WHERE expires_at < ${Date.now()}
|
|
4852
|
+
RETURNING state
|
|
4853
|
+
`;
|
|
4854
|
+
return rows.length;
|
|
3999
4855
|
}
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4856
|
+
async ensureTables() {
|
|
4857
|
+
await this.sql`
|
|
4858
|
+
CREATE TABLE IF NOT EXISTS auth_oauth_states (
|
|
4859
|
+
state TEXT PRIMARY KEY,
|
|
4860
|
+
provider TEXT NOT NULL,
|
|
4861
|
+
redirect_uri TEXT NOT NULL,
|
|
4862
|
+
created_at BIGINT NOT NULL,
|
|
4863
|
+
expires_at BIGINT NOT NULL,
|
|
4864
|
+
metadata_json TEXT,
|
|
4865
|
+
code_verifier TEXT
|
|
4866
|
+
)
|
|
4867
|
+
`;
|
|
4868
|
+
await this.sql`
|
|
4869
|
+
CREATE INDEX IF NOT EXISTS idx_auth_oauth_states_expires_at
|
|
4870
|
+
ON auth_oauth_states(expires_at)
|
|
4871
|
+
`;
|
|
4009
4872
|
}
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4873
|
+
};
|
|
4874
|
+
var PostgresLinkedIdentityStore = class {
|
|
4875
|
+
sql;
|
|
4876
|
+
ready;
|
|
4877
|
+
constructor(sql) {
|
|
4878
|
+
this.sql = sql;
|
|
4879
|
+
this.ready = this.ensureTables();
|
|
4015
4880
|
}
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
const
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4881
|
+
async findByProvider(provider, providerUserId) {
|
|
4882
|
+
await this.ready;
|
|
4883
|
+
const rows = await this.sql`
|
|
4884
|
+
SELECT * FROM auth_linked_identities
|
|
4885
|
+
WHERE provider = ${provider} AND provider_user_id = ${providerUserId}
|
|
4886
|
+
`;
|
|
4887
|
+
return rows[0] ? rowToLinkedIdentity2(rows[0]) : null;
|
|
4888
|
+
}
|
|
4889
|
+
async findByUser(userId) {
|
|
4890
|
+
await this.ready;
|
|
4891
|
+
const rows = await this.sql`
|
|
4892
|
+
SELECT * FROM auth_linked_identities
|
|
4893
|
+
WHERE user_id = ${userId}
|
|
4894
|
+
ORDER BY linked_at ASC
|
|
4895
|
+
`;
|
|
4896
|
+
return rows.map(rowToLinkedIdentity2);
|
|
4897
|
+
}
|
|
4898
|
+
async create(params) {
|
|
4899
|
+
await this.ready;
|
|
4900
|
+
const identity = {
|
|
4901
|
+
id: randomUUID8(),
|
|
4902
|
+
userId: params.userId,
|
|
4903
|
+
provider: params.provider,
|
|
4904
|
+
providerUserId: params.providerUserId,
|
|
4905
|
+
email: params.email,
|
|
4906
|
+
linkedAt: Date.now()
|
|
4907
|
+
};
|
|
4026
4908
|
try {
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
details += `: ${errorBody}`;
|
|
4043
|
-
} catch {
|
|
4909
|
+
await this.sql`
|
|
4910
|
+
INSERT INTO auth_linked_identities
|
|
4911
|
+
(id, user_id, provider, provider_user_id, email, linked_at)
|
|
4912
|
+
VALUES (
|
|
4913
|
+
${identity.id},
|
|
4914
|
+
${identity.userId},
|
|
4915
|
+
${identity.provider},
|
|
4916
|
+
${identity.providerUserId},
|
|
4917
|
+
${identity.email},
|
|
4918
|
+
${identity.linkedAt}
|
|
4919
|
+
)
|
|
4920
|
+
`;
|
|
4921
|
+
} catch (error) {
|
|
4922
|
+
if (isUniqueViolation(error)) {
|
|
4923
|
+
throw new DuplicateLinkedIdentityError(params.provider);
|
|
4044
4924
|
}
|
|
4045
|
-
throw
|
|
4925
|
+
throw error;
|
|
4046
4926
|
}
|
|
4047
|
-
|
|
4048
|
-
return {
|
|
4049
|
-
accessToken: data.access_token,
|
|
4050
|
-
tokenType: data.token_type ?? "Bearer",
|
|
4051
|
-
expiresIn: data.expires_in,
|
|
4052
|
-
refreshToken: data.refresh_token,
|
|
4053
|
-
idToken: data.id_token,
|
|
4054
|
-
scope: data.scope
|
|
4055
|
-
};
|
|
4927
|
+
return identity;
|
|
4056
4928
|
}
|
|
4057
|
-
async
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
Accept: "application/json"
|
|
4064
|
-
}
|
|
4065
|
-
});
|
|
4066
|
-
} catch (err) {
|
|
4067
|
-
throw new OAuthUserInfoError(err instanceof Error ? err.message : "Network error");
|
|
4068
|
-
}
|
|
4069
|
-
if (!response.ok) {
|
|
4070
|
-
throw new OAuthUserInfoError(`HTTP ${response.status}`);
|
|
4071
|
-
}
|
|
4072
|
-
const profile = await response.json();
|
|
4073
|
-
return normalizeUserInfo(provider.providerId, profile);
|
|
4929
|
+
async delete(userId, provider) {
|
|
4930
|
+
await this.ready;
|
|
4931
|
+
await this.sql`
|
|
4932
|
+
DELETE FROM auth_linked_identities
|
|
4933
|
+
WHERE user_id = ${userId} AND provider = ${provider}
|
|
4934
|
+
`;
|
|
4074
4935
|
}
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
email: profile.email ?? null,
|
|
4093
|
-
emailVerified: false,
|
|
4094
|
-
// GitHub doesn't confirm in the profile response
|
|
4095
|
-
name: profile.name ?? profile.login ?? null,
|
|
4096
|
-
avatarUrl: profile.avatar_url ?? null,
|
|
4097
|
-
rawProfile: profile
|
|
4098
|
-
};
|
|
4099
|
-
case "microsoft":
|
|
4100
|
-
return {
|
|
4101
|
-
providerId: profile.id,
|
|
4102
|
-
provider: "microsoft",
|
|
4103
|
-
email: profile.mail ?? profile.userPrincipalName ?? null,
|
|
4104
|
-
emailVerified: false,
|
|
4105
|
-
name: profile.displayName ?? null,
|
|
4106
|
-
avatarUrl: null,
|
|
4107
|
-
rawProfile: profile
|
|
4108
|
-
};
|
|
4109
|
-
default:
|
|
4110
|
-
return {
|
|
4111
|
-
providerId: String(profile.id ?? profile.sub ?? ""),
|
|
4112
|
-
provider: providerId,
|
|
4113
|
-
email: profile.email ?? null,
|
|
4114
|
-
emailVerified: profile.email_verified ?? false,
|
|
4115
|
-
name: profile.name ?? null,
|
|
4116
|
-
avatarUrl: profile.picture ?? profile.avatar_url ?? null,
|
|
4117
|
-
rawProfile: profile
|
|
4118
|
-
};
|
|
4936
|
+
async ensureTables() {
|
|
4937
|
+
await this.sql`
|
|
4938
|
+
CREATE TABLE IF NOT EXISTS auth_linked_identities (
|
|
4939
|
+
id TEXT PRIMARY KEY,
|
|
4940
|
+
user_id TEXT NOT NULL,
|
|
4941
|
+
provider TEXT NOT NULL,
|
|
4942
|
+
provider_user_id TEXT NOT NULL,
|
|
4943
|
+
email TEXT,
|
|
4944
|
+
linked_at BIGINT NOT NULL,
|
|
4945
|
+
UNIQUE(provider, provider_user_id),
|
|
4946
|
+
UNIQUE(user_id, provider)
|
|
4947
|
+
)
|
|
4948
|
+
`;
|
|
4949
|
+
await this.sql`
|
|
4950
|
+
CREATE INDEX IF NOT EXISTS idx_auth_linked_identities_user_id
|
|
4951
|
+
ON auth_linked_identities(user_id)
|
|
4952
|
+
`;
|
|
4119
4953
|
}
|
|
4954
|
+
};
|
|
4955
|
+
async function createPostgresOAuthStateStore(options) {
|
|
4956
|
+
const postgresClient = await loadPostgresDeps2();
|
|
4957
|
+
const sql = postgresClient(options.connectionString);
|
|
4958
|
+
return new PostgresOAuthStateStore(sql);
|
|
4120
4959
|
}
|
|
4121
|
-
function
|
|
4960
|
+
async function createPostgresLinkedIdentityStore(options) {
|
|
4961
|
+
const postgresClient = await loadPostgresDeps2();
|
|
4962
|
+
const sql = postgresClient(options.connectionString);
|
|
4963
|
+
return new PostgresLinkedIdentityStore(sql);
|
|
4964
|
+
}
|
|
4965
|
+
async function createPostgresOAuthStores(options) {
|
|
4966
|
+
const postgresClient = await loadPostgresDeps2();
|
|
4967
|
+
const sql = postgresClient(options.connectionString);
|
|
4122
4968
|
return {
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
clientSecret: config.clientSecret,
|
|
4126
|
-
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
4127
|
-
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
4128
|
-
userInfoUrl: "https://openidconnect.googleapis.com/v1/userinfo",
|
|
4129
|
-
scopes: config.scopes ?? ["openid", "email", "profile"],
|
|
4130
|
-
redirectUri: config.redirectUri
|
|
4969
|
+
stateStore: new PostgresOAuthStateStore(sql),
|
|
4970
|
+
linkedIdentityStore: new PostgresLinkedIdentityStore(sql)
|
|
4131
4971
|
};
|
|
4132
4972
|
}
|
|
4133
|
-
function
|
|
4973
|
+
async function loadPostgresDeps2() {
|
|
4974
|
+
try {
|
|
4975
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
4976
|
+
const postgresMod = await dynamicImport("postgres");
|
|
4977
|
+
return postgresMod.default;
|
|
4978
|
+
} catch {
|
|
4979
|
+
throw new Error(
|
|
4980
|
+
'PostgreSQL OAuth stores require the "postgres" package. Install it in your project dependencies.'
|
|
4981
|
+
);
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4984
|
+
function rowToOAuthState2(row) {
|
|
4134
4985
|
return {
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
redirectUri: config.redirectUri
|
|
4986
|
+
state: row.state,
|
|
4987
|
+
provider: row.provider,
|
|
4988
|
+
redirectUri: row.redirect_uri,
|
|
4989
|
+
createdAt: Number(row.created_at),
|
|
4990
|
+
expiresAt: Number(row.expires_at),
|
|
4991
|
+
metadata: parseMetadata2(row.metadata_json),
|
|
4992
|
+
codeVerifier: row.code_verifier ?? void 0
|
|
4143
4993
|
};
|
|
4144
4994
|
}
|
|
4145
|
-
function
|
|
4146
|
-
|
|
4995
|
+
function parseMetadata2(value) {
|
|
4996
|
+
if (!value) return void 0;
|
|
4997
|
+
const parsed = JSON.parse(value);
|
|
4998
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
4999
|
+
}
|
|
5000
|
+
function rowToLinkedIdentity2(row) {
|
|
4147
5001
|
return {
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
scopes: config.scopes ?? ["openid", "email", "profile", "User.Read"],
|
|
4155
|
-
redirectUri: config.redirectUri
|
|
5002
|
+
id: row.id,
|
|
5003
|
+
userId: row.user_id,
|
|
5004
|
+
provider: row.provider,
|
|
5005
|
+
providerUserId: row.provider_user_id,
|
|
5006
|
+
email: row.email,
|
|
5007
|
+
linkedAt: Number(row.linked_at)
|
|
4156
5008
|
};
|
|
4157
5009
|
}
|
|
4158
|
-
function
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
4163
|
-
binary += String.fromCharCode(bytes[i]);
|
|
4164
|
-
}
|
|
4165
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
5010
|
+
function isUniqueViolation(error) {
|
|
5011
|
+
if (!(error instanceof Error)) return false;
|
|
5012
|
+
const code = error.code;
|
|
5013
|
+
return code === "23505" || error.message.includes("unique constraint") || error.message.includes("duplicate key");
|
|
4166
5014
|
}
|
|
4167
5015
|
|
|
4168
5016
|
// src/session/session.ts
|
|
4169
|
-
import { KoraError as
|
|
4170
|
-
var SessionError = class extends
|
|
5017
|
+
import { KoraError as KoraError11 } from "@korajs/core";
|
|
5018
|
+
var SessionError = class extends KoraError11 {
|
|
4171
5019
|
constructor(message, code, context) {
|
|
4172
5020
|
super(message, code, context);
|
|
4173
5021
|
this.name = "SessionError";
|
|
@@ -4396,8 +5244,8 @@ function generateSessionId() {
|
|
|
4396
5244
|
}
|
|
4397
5245
|
|
|
4398
5246
|
// src/mfa/totp.ts
|
|
4399
|
-
import { KoraError as
|
|
4400
|
-
var TotpError = class extends
|
|
5247
|
+
import { KoraError as KoraError12 } from "@korajs/core";
|
|
5248
|
+
var TotpError = class extends KoraError12 {
|
|
4401
5249
|
constructor(message, code, context) {
|
|
4402
5250
|
super(message, code, context);
|
|
4403
5251
|
this.name = "TotpError";
|
|
@@ -4820,8 +5668,8 @@ function timingSafeEqual(a, b) {
|
|
|
4820
5668
|
}
|
|
4821
5669
|
|
|
4822
5670
|
// src/admin/admin-api.ts
|
|
4823
|
-
import { KoraError as
|
|
4824
|
-
var AdminApiError = class extends
|
|
5671
|
+
import { KoraError as KoraError13 } from "@korajs/core";
|
|
5672
|
+
var AdminApiError = class extends KoraError13 {
|
|
4825
5673
|
constructor(message, code, context) {
|
|
4826
5674
|
super(message, code, context);
|
|
4827
5675
|
this.name = "AdminApiError";
|
|
@@ -4855,7 +5703,7 @@ var AdminApi = class {
|
|
|
4855
5703
|
throw new AdminUserNotFoundError(userId);
|
|
4856
5704
|
}
|
|
4857
5705
|
await this.audit("admin.user_lookup", adminId, userId, "user");
|
|
4858
|
-
return
|
|
5706
|
+
return toAuthUser3(user);
|
|
4859
5707
|
}
|
|
4860
5708
|
/**
|
|
4861
5709
|
* List users with optional filtering and pagination.
|
|
@@ -4873,7 +5721,7 @@ var AdminApi = class {
|
|
|
4873
5721
|
filtered = filtered.filter((u) => u.emailVerified === query.emailVerified);
|
|
4874
5722
|
}
|
|
4875
5723
|
const total = filtered.length;
|
|
4876
|
-
const data = filtered.slice(offset, offset + limit).map(
|
|
5724
|
+
const data = filtered.slice(offset, offset + limit).map(toAuthUser3);
|
|
4877
5725
|
return { data, total, limit, offset };
|
|
4878
5726
|
}
|
|
4879
5727
|
/**
|
|
@@ -4895,7 +5743,7 @@ var AdminApi = class {
|
|
|
4895
5743
|
}
|
|
4896
5744
|
await this.userStore.update(user);
|
|
4897
5745
|
await this.audit("user.update", adminId, userId, "user", { updates });
|
|
4898
|
-
return
|
|
5746
|
+
return toAuthUser3(user);
|
|
4899
5747
|
}
|
|
4900
5748
|
/**
|
|
4901
5749
|
* Delete a user and all associated sessions.
|
|
@@ -4960,7 +5808,7 @@ var AdminApi = class {
|
|
|
4960
5808
|
});
|
|
4961
5809
|
}
|
|
4962
5810
|
};
|
|
4963
|
-
function
|
|
5811
|
+
function toAuthUser3(stored) {
|
|
4964
5812
|
return {
|
|
4965
5813
|
id: stored.id,
|
|
4966
5814
|
email: stored.email,
|
|
@@ -4971,8 +5819,8 @@ function toAuthUser2(stored) {
|
|
|
4971
5819
|
}
|
|
4972
5820
|
|
|
4973
5821
|
// src/admin/audit-log.ts
|
|
4974
|
-
import { KoraError as
|
|
4975
|
-
var AuditLogError = class extends
|
|
5822
|
+
import { KoraError as KoraError14 } from "@korajs/core";
|
|
5823
|
+
var AuditLogError = class extends KoraError14 {
|
|
4976
5824
|
constructor(message, code, context) {
|
|
4977
5825
|
super(message, code, context);
|
|
4978
5826
|
this.name = "AuditLogError";
|
|
@@ -5097,8 +5945,8 @@ function generateAuditId() {
|
|
|
5097
5945
|
}
|
|
5098
5946
|
|
|
5099
5947
|
// src/admin/webhooks.ts
|
|
5100
|
-
import { KoraError as
|
|
5101
|
-
var WebhookError = class extends
|
|
5948
|
+
import { KoraError as KoraError15 } from "@korajs/core";
|
|
5949
|
+
var WebhookError = class extends KoraError15 {
|
|
5102
5950
|
constructor(message, code, context) {
|
|
5103
5951
|
super(message, code, context);
|
|
5104
5952
|
this.name = "WebhookError";
|
|
@@ -5327,6 +6175,7 @@ export {
|
|
|
5327
6175
|
CannotRemoveOwnerError,
|
|
5328
6176
|
CircularInheritanceError,
|
|
5329
6177
|
DuplicateEmailError,
|
|
6178
|
+
DuplicateLinkedIdentityError,
|
|
5330
6179
|
EmailVerificationError,
|
|
5331
6180
|
EmailVerificationManager,
|
|
5332
6181
|
ExternalAuthOperationNotSupportedError,
|
|
@@ -5336,6 +6185,7 @@ export {
|
|
|
5336
6185
|
InMemoryAuditLogStore,
|
|
5337
6186
|
InMemoryChallengeStore,
|
|
5338
6187
|
InMemoryEmailVerificationStore,
|
|
6188
|
+
InMemoryLinkedIdentityStore,
|
|
5339
6189
|
InMemoryOAuthStateStore,
|
|
5340
6190
|
InMemoryOrgStore,
|
|
5341
6191
|
InMemoryPasswordResetStore,
|
|
@@ -5368,6 +6218,8 @@ export {
|
|
|
5368
6218
|
PasskeyVerificationError,
|
|
5369
6219
|
PasswordResetError,
|
|
5370
6220
|
PasswordResetManager,
|
|
6221
|
+
PostgresLinkedIdentityStore,
|
|
6222
|
+
PostgresOAuthStateStore,
|
|
5371
6223
|
PostgresUserStore,
|
|
5372
6224
|
ROLE_HIERARCHY,
|
|
5373
6225
|
RbacEngine,
|
|
@@ -5382,6 +6234,8 @@ export {
|
|
|
5382
6234
|
SessionManager,
|
|
5383
6235
|
SessionMfaRequiredError,
|
|
5384
6236
|
SessionNotFoundError,
|
|
6237
|
+
SqliteLinkedIdentityStore,
|
|
6238
|
+
SqliteOAuthStateStore,
|
|
5385
6239
|
SqliteUserStore,
|
|
5386
6240
|
TokenManager,
|
|
5387
6241
|
TotpAlreadyEnabledError,
|
|
@@ -5400,7 +6254,14 @@ export {
|
|
|
5400
6254
|
base32Encode,
|
|
5401
6255
|
computePublicKeyThumbprint,
|
|
5402
6256
|
createClerkAdapter,
|
|
6257
|
+
createKoraAuthServer,
|
|
6258
|
+
createPostgresLinkedIdentityStore,
|
|
6259
|
+
createPostgresOAuthStateStore,
|
|
6260
|
+
createPostgresOAuthStores,
|
|
5403
6261
|
createPostgresUserStore,
|
|
6262
|
+
createSqliteLinkedIdentityStore,
|
|
6263
|
+
createSqliteOAuthStateStore,
|
|
6264
|
+
createSqliteOAuthStores,
|
|
5404
6265
|
createSqliteUserStore,
|
|
5405
6266
|
createSupabaseAdapter,
|
|
5406
6267
|
decodeJwt,
|