@robelest/convex-auth 0.0.4-preview.27 → 0.0.4-preview.28
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 +3 -5
- package/dist/bin.js +6488 -1571
- package/dist/browser/index.js +10 -7
- package/dist/browser/locks.js +3 -5
- package/dist/browser/navigation.js +7 -10
- package/dist/browser/runtime.js +35 -33
- package/dist/client/core/types.js +17 -0
- package/dist/client/factors/device.js +26 -19
- package/dist/client/index.js +151 -163
- package/dist/client/runtime/proxy.js +6 -6
- package/dist/client/services/adapters.js +3 -7
- package/dist/client/services/http.js +2 -5
- package/dist/client/services/resolve.js +5 -11
- package/dist/client/services/runtime.js +2 -5
- package/dist/component/_generated/component.d.ts +46 -0
- package/dist/component/index.d.ts +3 -3
- package/dist/component/model.d.ts +25 -25
- package/dist/component/public/identity/sessions.js +38 -1
- package/dist/component/public/identity/tokens.js +81 -3
- package/dist/component/public/identity/verifiers.js +9 -3
- package/dist/component/public.js +3 -3
- package/dist/component/schema.d.ts +320 -320
- package/dist/core/index.d.ts +380 -0
- package/dist/core/index.js +83 -0
- package/dist/otel.d.ts +13 -17
- package/dist/otel.js +39 -49
- package/dist/providers/email.d.ts +2 -2
- package/dist/providers/password.js +8 -16
- package/dist/providers/phone.js +2 -9
- package/dist/server/auth-context.d.ts +204 -0
- package/dist/server/auth-context.js +76 -0
- package/dist/server/auth.d.ts +25 -187
- package/dist/server/auth.js +5 -96
- package/dist/server/componentContext.d.ts +12 -0
- package/dist/server/componentContext.js +1 -0
- package/dist/server/config.js +1 -12
- package/dist/server/constants.js +6 -0
- package/dist/server/contract.d.ts +1 -1
- package/dist/server/core.js +5 -14
- package/dist/server/crypto.js +26 -18
- package/dist/server/db.js +6 -1
- package/dist/server/device.js +88 -78
- package/dist/server/http.d.ts +4 -3
- package/dist/server/http.js +74 -86
- package/dist/server/index.d.ts +2 -1
- package/dist/server/limits.js +22 -15
- package/dist/server/mounts.d.ts +103 -103
- package/dist/server/mutations/account.js +6 -4
- package/dist/server/mutations/invalidate.js +3 -6
- package/dist/server/mutations/oauth.js +86 -88
- package/dist/server/mutations/refresh.js +45 -87
- package/dist/server/mutations/register.js +19 -19
- package/dist/server/mutations/retrieve.js +17 -15
- package/dist/server/mutations/signature.js +9 -13
- package/dist/server/mutations/signin.js +7 -3
- package/dist/server/mutations/signout.js +10 -15
- package/dist/server/mutations/store.js +22 -12
- package/dist/server/mutations/verifier.js +11 -6
- package/dist/server/mutations/verify.js +55 -46
- package/dist/server/oauth/runtime.js +27 -25
- package/dist/server/passkey.js +299 -250
- package/dist/server/prefetch.js +283 -281
- package/dist/server/refresh.js +7 -60
- package/dist/server/runtime.d.ts +82 -206
- package/dist/server/runtime.js +63 -56
- package/dist/server/services/config.js +5 -3
- package/dist/server/services/logger.js +2 -4
- package/dist/server/services/providers.js +2 -4
- package/dist/server/services/refresh.js +2 -4
- package/dist/server/services/resolve.js +15 -14
- package/dist/server/services/signin.js +2 -4
- package/dist/server/sessions.js +32 -33
- package/dist/server/signin.js +177 -142
- package/dist/server/sso/domain.d.ts +20 -68
- package/dist/server/sso/domain.js +444 -413
- package/dist/server/sso/http.js +53 -59
- package/dist/server/sso/oidc.js +94 -80
- package/dist/server/tokens.js +13 -3
- package/dist/server/totp.js +153 -116
- package/dist/server/types.d.ts +2 -2
- package/dist/server/users.js +18 -23
- package/dist/server/utils/cache.js +51 -0
- package/dist/server/utils/dispatch.js +36 -0
- package/dist/server/utils/retry.js +24 -0
- package/dist/server/utils/span.js +32 -0
- package/dist/shared/errors.js +9 -3
- package/dist/shared/log.js +20 -22
- package/package.json +41 -33
|
@@ -4,7 +4,6 @@ import { authDb } from "../db.js";
|
|
|
4
4
|
import { log, maybeRedact } from "../log.js";
|
|
5
5
|
import { AUTH_STORE_REF } from "./store/refs.js";
|
|
6
6
|
import { ConvexError, v } from "convex/values";
|
|
7
|
-
import { Effect, Match } from "effect";
|
|
8
7
|
|
|
9
8
|
//#region src/server/mutations/account.ts
|
|
10
9
|
const modifyAccountArgs = v.object({
|
|
@@ -14,7 +13,7 @@ const modifyAccountArgs = v.object({
|
|
|
14
13
|
secret: v.string()
|
|
15
14
|
})
|
|
16
15
|
});
|
|
17
|
-
function modifyAccountImpl(ctx, args, getProviderOrThrow, config) {
|
|
16
|
+
async function modifyAccountImpl(ctx, args, getProviderOrThrow, config) {
|
|
18
17
|
const { provider, account } = args;
|
|
19
18
|
const db = authDb(ctx, config);
|
|
20
19
|
log(LOG_LEVELS.DEBUG, "modifyAccountImpl args:", {
|
|
@@ -24,10 +23,13 @@ function modifyAccountImpl(ctx, args, getProviderOrThrow, config) {
|
|
|
24
23
|
secret: maybeRedact(account.secret ?? "")
|
|
25
24
|
}
|
|
26
25
|
});
|
|
27
|
-
|
|
26
|
+
const existingAccount = await db.accounts.get(provider, account.id);
|
|
27
|
+
if (existingAccount === null) throw new ConvexError({
|
|
28
28
|
code: "ACCOUNT_NOT_FOUND",
|
|
29
29
|
message: `Cannot modify account with ID ${account.id} because it does not exist`
|
|
30
|
-
})
|
|
30
|
+
});
|
|
31
|
+
const hashedSecret = await hash(getProviderOrThrow(provider), account.secret);
|
|
32
|
+
await db.accounts.patch(existingAccount._id, { secret: hashedSecret });
|
|
31
33
|
}
|
|
32
34
|
const callModifyAccount = async (ctx, args) => {
|
|
33
35
|
return ctx.runMutation(AUTH_STORE_REF, { args: {
|
|
@@ -4,7 +4,6 @@ import { log } from "../log.js";
|
|
|
4
4
|
import { AUTH_STORE_REF } from "./store/refs.js";
|
|
5
5
|
import { deleteSession } from "../sessions.js";
|
|
6
6
|
import { v } from "convex/values";
|
|
7
|
-
import { Effect } from "effect";
|
|
8
7
|
|
|
9
8
|
//#region src/server/mutations/invalidate.ts
|
|
10
9
|
const invalidateSessionsArgs = v.object({
|
|
@@ -17,15 +16,13 @@ const callInvalidateSessions = async (ctx, args) => {
|
|
|
17
16
|
...args
|
|
18
17
|
} });
|
|
19
18
|
};
|
|
20
|
-
function invalidateSessionsImpl(ctx, args, config) {
|
|
19
|
+
async function invalidateSessionsImpl(ctx, args, config) {
|
|
21
20
|
log(LOG_LEVELS.DEBUG, "invalidateSessionsImpl args:", args);
|
|
22
21
|
const { userId, except } = args;
|
|
23
22
|
const exceptSet = new Set(except ?? []);
|
|
24
23
|
const typedUserId = userId;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
yield* Effect.forEach(sessions, (session) => exceptSet.has(session._id) ? Effect.void : Effect.promise(() => deleteSession(ctx, session, config)), { discard: true });
|
|
28
|
-
});
|
|
24
|
+
const sessions = await authDb(ctx, config).sessions.listByUser(typedUserId);
|
|
25
|
+
await Promise.all(sessions.map((session) => exceptSet.has(session._id) ? Promise.resolve() : deleteSession(ctx, session, config)));
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
//#endregion
|
|
@@ -9,7 +9,6 @@ import { GROUP_OIDC_PROVIDER_PREFIX, GROUP_SAML_PROVIDER_PREFIX, isGroupProvider
|
|
|
9
9
|
import { createSyntheticOAuthMaterializedConfig } from "../sso/oidc.js";
|
|
10
10
|
import { normalizeGroupConnectionPolicy, resolveProvisionedRoleIds } from "../sso/policy.js";
|
|
11
11
|
import { ConvexError, v } from "convex/values";
|
|
12
|
-
import { Effect } from "effect";
|
|
13
12
|
|
|
14
13
|
//#region src/server/mutations/oauth.ts
|
|
15
14
|
const OAUTH_SIGN_IN_EXPIRATION_MS = 1e3 * 60 * 2;
|
|
@@ -44,106 +43,105 @@ function normalizeAccountExtend(provider, providerAccountId, accountExtend) {
|
|
|
44
43
|
}
|
|
45
44
|
};
|
|
46
45
|
}
|
|
47
|
-
function userOAuthImpl(ctx, args, getProviderOrThrow, config) {
|
|
46
|
+
async function userOAuthImpl(ctx, args, getProviderOrThrow, config) {
|
|
48
47
|
log("DEBUG", "userOAuthImpl args:", args);
|
|
49
48
|
const { profile, provider, providerAccountId, signature, accountExtend } = args;
|
|
50
49
|
const typedProfile = profile;
|
|
51
50
|
const db = authDb(ctx, config);
|
|
52
51
|
const connectionId = provider.startsWith(GROUP_OIDC_PROVIDER_PREFIX) ? provider.slice(GROUP_OIDC_PROVIDER_PREFIX.length) : provider.startsWith(GROUP_SAML_PROVIDER_PREFIX) ? provider.slice(GROUP_SAML_PROVIDER_PREFIX.length) : null;
|
|
53
52
|
const connectionProtocol = provider.startsWith(GROUP_OIDC_PROVIDER_PREFIX) ? "oidc" : provider.startsWith(GROUP_SAML_PROVIDER_PREFIX) ? "saml" : null;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
message: "Invalid OAuth state. Please try signing in again."
|
|
69
|
-
})
|
|
70
|
-
}).pipe(Effect.flatMap((verifier$1) => verifier$1 === null ? Effect.fail(new ConvexError({
|
|
53
|
+
const existingAccount = await db.accounts.get(provider, providerAccountId);
|
|
54
|
+
const connection = connectionId !== null ? await getGroupConnection(ctx, config.component.public, connectionId) : null;
|
|
55
|
+
const group = connection !== null ? await getGroup(ctx, config.component.public, connection.groupId) : null;
|
|
56
|
+
const connectionPolicy = connection ? normalizeGroupConnectionPolicy(group?.policy) : null;
|
|
57
|
+
const existingScimIdentity = connectionId !== null && existingAccount === null && connectionPolicy?.provisioning.scimReuse.user === "externalId" ? await ctx.runQuery(config.component.public.groupConnectionScimIdentityGet, {
|
|
58
|
+
connectionId,
|
|
59
|
+
resourceType: "user",
|
|
60
|
+
externalId: providerAccountId
|
|
61
|
+
}) : null;
|
|
62
|
+
let verifier;
|
|
63
|
+
try {
|
|
64
|
+
verifier = await db.verifiers.getBySignature(signature);
|
|
65
|
+
} catch {
|
|
66
|
+
throw new ConvexError({
|
|
71
67
|
code: "OAUTH_INVALID_STATE",
|
|
72
68
|
message: "Invalid OAuth state. Please try signing in again."
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (verifier === null) throw new ConvexError({
|
|
72
|
+
code: "OAUTH_INVALID_STATE",
|
|
73
|
+
message: "Invalid OAuth state. Please try signing in again."
|
|
74
|
+
});
|
|
75
|
+
const profileResolved = (config.sso?.hooks?.profileResolved ? await config.sso.hooks.profileResolved({
|
|
76
|
+
protocol: connectionProtocol ?? "oidc",
|
|
77
|
+
connectionId: connectionId ?? void 0,
|
|
78
|
+
profile: typedProfile
|
|
79
|
+
}) : void 0) ?? typedProfile;
|
|
80
|
+
const profileForProvisioning = (config.sso?.hooks?.beforeProvision ? await config.sso.hooks.beforeProvision({
|
|
81
|
+
protocol: connectionProtocol ?? "oidc",
|
|
82
|
+
connectionId: connectionId ?? void 0,
|
|
83
|
+
profile: profileResolved
|
|
84
|
+
}) : void 0) ?? profileResolved;
|
|
85
|
+
const { accountId } = await upsertUserAndAccount(ctx, verifier.sessionId ?? null, existingAccount !== null ? { existingAccount } : { providerAccountId }, {
|
|
86
|
+
type: "oauth",
|
|
87
|
+
provider: isGroupProviderId(provider) ? createSyntheticOAuthMaterializedConfig(provider, { accountLinking: connectionProtocol === "oidc" ? connectionPolicy?.identity.accountLinking.oidc : connectionProtocol === "saml" ? connectionPolicy?.identity.accountLinking.saml : void 0 }) : getProviderOrThrow(provider),
|
|
88
|
+
profile: profileForProvisioning,
|
|
89
|
+
accountExtend: normalizeAccountExtend(provider, providerAccountId, accountExtend)
|
|
90
|
+
}, config, connectionPolicy?.provisioning.user ? {
|
|
91
|
+
existingUserId: existingScimIdentity?.userId,
|
|
92
|
+
provisioningUser: connectionPolicy.provisioning.user,
|
|
93
|
+
source: "login"
|
|
94
|
+
} : existingScimIdentity?.userId ? { existingUserId: existingScimIdentity.userId } : void 0);
|
|
95
|
+
if (connectionId !== null && connectionPolicy?.provisioning.jit.mode === "createUserAndMembership") {
|
|
96
|
+
const userId = (await db.accounts.getById(accountId))?.userId;
|
|
97
|
+
if (userId) {
|
|
98
|
+
const groupId = connection?.groupId;
|
|
99
|
+
if (groupId) {
|
|
100
|
+
const provisionedRoleIds = resolveProvisionedRoleIds({
|
|
101
|
+
policy: connectionPolicy,
|
|
102
|
+
groups: Array.isArray(typedProfile.groups) ? typedProfile.groups : void 0,
|
|
103
|
+
roles: Array.isArray(typedProfile.roles) ? typedProfile.roles : void 0
|
|
104
|
+
});
|
|
105
|
+
const existingMembership = await ctx.runQuery(config.component.public.memberGetByGroupAndUser, {
|
|
106
|
+
userId,
|
|
107
|
+
groupId
|
|
108
|
+
});
|
|
109
|
+
if (existingMembership === null) await ctx.runMutation(config.component.public.memberAdd, {
|
|
110
|
+
groupId,
|
|
111
|
+
userId,
|
|
112
|
+
roleIds: provisionedRoleIds,
|
|
113
|
+
status: "active"
|
|
114
|
+
});
|
|
115
|
+
else if (provisionedRoleIds.length > 0) await ctx.runMutation(config.component.public.memberUpdate, {
|
|
116
|
+
memberId: existingMembership._id,
|
|
117
|
+
data: { roleIds: provisionedRoleIds }
|
|
118
|
+
});
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
121
|
+
}
|
|
122
|
+
if (connectionId !== null) {
|
|
123
|
+
const userId = (await db.accounts.getById(accountId))?.userId;
|
|
124
|
+
if (userId) {
|
|
125
|
+
if (config.sso?.hooks?.afterProvision) await config.sso.hooks.afterProvision({
|
|
126
|
+
protocol: connectionProtocol ?? "oidc",
|
|
127
|
+
connectionId,
|
|
128
|
+
profile: profileForProvisioning,
|
|
129
|
+
userId
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
return code;
|
|
132
|
+
}
|
|
133
|
+
const code = generateRandomString(8, "0123456789");
|
|
134
|
+
await db.verifiers.delete(verifier._id);
|
|
135
|
+
const existingVerificationCode = await db.verificationCodes.getByAccountId(accountId);
|
|
136
|
+
if (existingVerificationCode !== null) await db.verificationCodes.delete(existingVerificationCode._id);
|
|
137
|
+
await db.verificationCodes.create({
|
|
138
|
+
code: await sha256(code),
|
|
139
|
+
accountId,
|
|
140
|
+
provider,
|
|
141
|
+
expirationTime: Date.now() + OAUTH_SIGN_IN_EXPIRATION_MS,
|
|
142
|
+
verifier: verifier._id
|
|
146
143
|
});
|
|
144
|
+
return code;
|
|
147
145
|
}
|
|
148
146
|
const callUserOAuth = async (ctx, args) => {
|
|
149
147
|
return ctx.runMutation(AUTH_STORE_REF, { args: {
|
|
@@ -1,104 +1,62 @@
|
|
|
1
1
|
import { authDb } from "../db.js";
|
|
2
2
|
import { log, maybeRedact } from "../log.js";
|
|
3
3
|
import { AUTH_STORE_REF } from "./store/refs.js";
|
|
4
|
-
import { REFRESH_TOKEN_REUSE_WINDOW_MS,
|
|
4
|
+
import { REFRESH_TOKEN_REUSE_WINDOW_MS, parseRefreshToken, refreshTokenExpirationTime } from "../refresh.js";
|
|
5
5
|
import { generateTokensForSession } from "../sessions.js";
|
|
6
|
+
import { withSpan } from "../utils/span.js";
|
|
6
7
|
import { v } from "convex/values";
|
|
7
|
-
import { Data, Effect, Match } from "effect";
|
|
8
8
|
|
|
9
9
|
//#region src/server/mutations/refresh.ts
|
|
10
|
-
const asSessionId = (id) => id;
|
|
11
|
-
const asRefreshTokenId = (id) => id;
|
|
12
10
|
const refreshSessionArgs = v.object({ refreshToken: v.string() });
|
|
13
|
-
|
|
14
|
-
const softTry = (try_, reason) => Effect.tryPromise({
|
|
15
|
-
try: try_,
|
|
16
|
-
catch: () => new RefreshFailure({ reason })
|
|
17
|
-
});
|
|
18
|
-
const softTryEffect = (effect, reason) => effect.pipe(Effect.mapError(() => new RefreshFailure({ reason })));
|
|
19
|
-
const softCleanup = (effect) => effect.pipe(Effect.catchTag("RefreshFailure", (failure) => Effect.sync(() => {
|
|
20
|
-
log("DEBUG", failure.reason);
|
|
21
|
-
})), Effect.asVoid);
|
|
22
|
-
function refreshSessionImpl(ctx, args, _getProviderOrThrow, config) {
|
|
11
|
+
async function refreshSessionImpl(ctx, args, config) {
|
|
23
12
|
const db = authDb(ctx, config);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
const { session, refreshTokenDoc } = validationResult;
|
|
40
|
-
const sessionId = session._id;
|
|
41
|
-
const userId = session.userId;
|
|
42
|
-
const tokenFirstUsed = refreshTokenDoc.firstUsedTime;
|
|
43
|
-
const tokenDispatch = tokenFirstUsed === void 0 ? { tag: "firstUse" } : {
|
|
44
|
-
tag: "reuse",
|
|
45
|
-
tokenFirstUsed
|
|
46
|
-
};
|
|
47
|
-
return yield* Match.value(tokenDispatch).pipe(Match.when({ tag: "firstUse" }, () => softTryEffect(Effect.gen(function* () {
|
|
48
|
-
yield* Effect.promise(() => db.refreshTokens.patch(asRefreshTokenId(refreshTokenId), { firstUsedTime: Date.now() }));
|
|
49
|
-
const result = yield* Effect.promise(() => generateTokensForSession(ctx, config, {
|
|
50
|
-
userId,
|
|
51
|
-
sessionId,
|
|
52
|
-
issuedRefreshTokenId: null,
|
|
53
|
-
parentRefreshTokenId: asRefreshTokenId(refreshTokenId)
|
|
54
|
-
}));
|
|
55
|
-
const { refreshTokenId: newRefreshTokenId } = yield* parseRefreshToken(result.refreshToken).pipe(Effect.mapError((error) => new RefreshFailure({ reason: error.data.message })));
|
|
56
|
-
yield* Effect.sync(() => {
|
|
57
|
-
log("DEBUG", `Exchanged ${maybeRedact(refreshTokenDoc._id)} (first use) for new refresh token ${maybeRedact(newRefreshTokenId)}`);
|
|
58
|
-
});
|
|
59
|
-
return result;
|
|
60
|
-
}), "Failed during first-use token exchange")), Match.when({ tag: "reuse" }, ({ tokenFirstUsed: tokenFirstUsed$1 }) => softTry(() => authDb(ctx, config).refreshTokens.getActive(asSessionId(tokenSessionId)), "Failed to load active refresh token").pipe(Effect.flatMap((activeRefreshToken) => {
|
|
61
|
-
log("DEBUG", `Active refresh token: ${maybeRedact(activeRefreshToken?._id ?? "(none)")}, parent ${maybeRedact(activeRefreshToken?.parentRefreshTokenId ?? "(none)")}`);
|
|
62
|
-
const reuseDispatch = activeRefreshToken !== null && activeRefreshToken.parentRefreshTokenId === refreshTokenId ? {
|
|
63
|
-
tag: "parentOfActive",
|
|
64
|
-
activeRefreshToken
|
|
65
|
-
} : tokenFirstUsed$1 + REFRESH_TOKEN_REUSE_WINDOW_MS > Date.now() ? { tag: "withinReuseWindow" } : { tag: "outsideReuseWindow" };
|
|
66
|
-
return Match.value(reuseDispatch).pipe(Match.when({ tag: "parentOfActive" }, ({ activeRefreshToken: activeRefreshToken$1 }) => softTry(() => generateTokensForSession(ctx, config, {
|
|
67
|
-
userId,
|
|
68
|
-
sessionId,
|
|
69
|
-
issuedRefreshTokenId: activeRefreshToken$1._id,
|
|
70
|
-
parentRefreshTokenId: asRefreshTokenId(refreshTokenId)
|
|
71
|
-
}), "Failed to generate tokens for parent reuse").pipe(Effect.tap(() => Effect.sync(() => {
|
|
72
|
-
log("DEBUG", `Token ${maybeRedact(refreshTokenDoc._id)} is parent of active refresh token ${maybeRedact(activeRefreshToken$1._id)}, so returning that token`);
|
|
73
|
-
})))), Match.when({ tag: "withinReuseWindow" }, () => softTryEffect(Effect.gen(function* () {
|
|
74
|
-
const result = yield* Effect.promise(() => generateTokensForSession(ctx, config, {
|
|
75
|
-
userId,
|
|
13
|
+
return withSpan("convex-auth.refresh.session", { hasRefreshToken: true }, async () => {
|
|
14
|
+
try {
|
|
15
|
+
let refreshTokenId;
|
|
16
|
+
let sessionId;
|
|
17
|
+
try {
|
|
18
|
+
({refreshTokenId, sessionId} = parseRefreshToken(args.refreshToken));
|
|
19
|
+
} catch {
|
|
20
|
+
throw new RefreshFailure("Failed to parse refresh token");
|
|
21
|
+
}
|
|
22
|
+
log("DEBUG", `refreshSessionImpl args: Token ID: ${maybeRedact(refreshTokenId)} Session ID: ${maybeRedact(sessionId)}`);
|
|
23
|
+
let exchanged;
|
|
24
|
+
try {
|
|
25
|
+
exchanged = await db.refreshTokens.exchange({
|
|
26
|
+
refreshTokenId,
|
|
76
27
|
sessionId,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const { refreshTokenId: newRefreshTokenId } = yield* parseRefreshToken(result.refreshToken).pipe(Effect.mapError((error) => new RefreshFailure({ reason: error.data.message })));
|
|
81
|
-
yield* Effect.sync(() => {
|
|
82
|
-
log("DEBUG", `Exchanged ${maybeRedact(refreshTokenDoc._id)} (reuse) for new refresh token ${maybeRedact(newRefreshTokenId)}`);
|
|
83
|
-
});
|
|
84
|
-
return result;
|
|
85
|
-
}), "Failed to generate tokens for reuse window")), Match.when({ tag: "outsideReuseWindow" }, () => softTryEffect(Effect.gen(function* () {
|
|
86
|
-
yield* Effect.sync(() => {
|
|
87
|
-
log("ERROR", "Refresh token used outside of reuse window");
|
|
88
|
-
log("DEBUG", `Token ${maybeRedact(refreshTokenDoc._id)} being used outside of reuse window, so invalidating all refresh tokens in subtree`);
|
|
28
|
+
now: Date.now(),
|
|
29
|
+
refreshTokenExpirationTime: refreshTokenExpirationTime(config),
|
|
30
|
+
reuseWindowMs: REFRESH_TOKEN_REUSE_WINDOW_MS
|
|
89
31
|
});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
32
|
+
} catch {
|
|
33
|
+
throw new RefreshFailure("Failed to exchange refresh token");
|
|
34
|
+
}
|
|
35
|
+
if (exchanged === null) return null;
|
|
36
|
+
try {
|
|
37
|
+
return await generateTokensForSession(config, {
|
|
38
|
+
userId: exchanged.userId,
|
|
39
|
+
sessionId: exchanged.sessionId,
|
|
40
|
+
refreshTokenId: exchanged.refreshTokenId
|
|
93
41
|
});
|
|
42
|
+
} catch {
|
|
43
|
+
throw new RefreshFailure("Failed to generate refresh-session tokens");
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
if (e instanceof RefreshFailure) {
|
|
47
|
+
log("DEBUG", e.reason);
|
|
94
48
|
return null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return null;
|
|
100
|
-
})));
|
|
49
|
+
}
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
101
53
|
}
|
|
54
|
+
var RefreshFailure = class extends Error {
|
|
55
|
+
constructor(reason) {
|
|
56
|
+
super(reason);
|
|
57
|
+
this.reason = reason;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
102
60
|
const callRefreshSession = async (ctx, args) => {
|
|
103
61
|
return ctx.runMutation(AUTH_STORE_REF, { args: {
|
|
104
62
|
type: "refreshSession",
|
|
@@ -7,7 +7,6 @@ import { getAuthSessionId } from "../sessions.js";
|
|
|
7
7
|
import { upsertUserAndAccount } from "../users.js";
|
|
8
8
|
import { payloadRecordValidator } from "../payloads.js";
|
|
9
9
|
import { ConvexError, v } from "convex/values";
|
|
10
|
-
import { Effect, Match } from "effect";
|
|
11
10
|
|
|
12
11
|
//#region src/server/mutations/register.ts
|
|
13
12
|
const createAccountFromCredentialsArgs = v.object({
|
|
@@ -20,7 +19,7 @@ const createAccountFromCredentialsArgs = v.object({
|
|
|
20
19
|
shouldLinkViaEmail: v.optional(v.boolean()),
|
|
21
20
|
shouldLinkViaPhone: v.optional(v.boolean())
|
|
22
21
|
});
|
|
23
|
-
function createAccountFromCredentialsImpl(ctx, args, getProviderOrThrow, config) {
|
|
22
|
+
async function createAccountFromCredentialsImpl(ctx, args, getProviderOrThrow, config) {
|
|
24
23
|
log(LOG_LEVELS.DEBUG, "createAccountFromCredentialsImpl args:", {
|
|
25
24
|
provider: args.provider,
|
|
26
25
|
account: {
|
|
@@ -32,10 +31,11 @@ function createAccountFromCredentialsImpl(ctx, args, getProviderOrThrow, config)
|
|
|
32
31
|
const db = authDb(ctx, config);
|
|
33
32
|
const provider = getProviderOrThrow(providerId);
|
|
34
33
|
const typedProfile = profile;
|
|
35
|
-
|
|
34
|
+
const existingAccount = await db.accounts.get(provider.id, account.id);
|
|
35
|
+
if (existingAccount === null) {
|
|
36
36
|
const accountSecret = account.secret;
|
|
37
|
-
const secret = accountSecret === void 0 ? void 0 :
|
|
38
|
-
const { userId, accountId } =
|
|
37
|
+
const secret = accountSecret === void 0 ? void 0 : await hash(provider, accountSecret);
|
|
38
|
+
const { userId, accountId } = await upsertUserAndAccount(ctx, await getAuthSessionId(ctx), {
|
|
39
39
|
providerAccountId: account.id,
|
|
40
40
|
secret
|
|
41
41
|
}, {
|
|
@@ -44,38 +44,38 @@ function createAccountFromCredentialsImpl(ctx, args, getProviderOrThrow, config)
|
|
|
44
44
|
profile: typedProfile,
|
|
45
45
|
shouldLinkViaEmail,
|
|
46
46
|
shouldLinkViaPhone
|
|
47
|
-
}, config)
|
|
48
|
-
const [createdAccount, createdUser] =
|
|
49
|
-
if (createdAccount === null)
|
|
47
|
+
}, config);
|
|
48
|
+
const [createdAccount, createdUser] = await Promise.all([db.accounts.getById(accountId), db.users.getById(userId)]);
|
|
49
|
+
if (createdAccount === null) throw new ConvexError({
|
|
50
50
|
code: "ACCOUNT_NOT_FOUND",
|
|
51
51
|
message: "Created account was not found."
|
|
52
|
-
})
|
|
53
|
-
if (createdUser === null)
|
|
52
|
+
});
|
|
53
|
+
if (createdUser === null) throw new ConvexError({
|
|
54
54
|
code: "USER_UPDATE_FAILED",
|
|
55
55
|
message: "Created user was not found."
|
|
56
|
-
})
|
|
56
|
+
});
|
|
57
57
|
return {
|
|
58
58
|
account: createdAccount,
|
|
59
59
|
user: createdUser
|
|
60
60
|
};
|
|
61
|
-
}
|
|
61
|
+
} else {
|
|
62
62
|
if (account.secret !== void 0) {
|
|
63
63
|
const accountSecret = account.secret;
|
|
64
|
-
if (!
|
|
64
|
+
if (!await verify(provider, accountSecret, existingAccount.secret ?? "")) throw new ConvexError({
|
|
65
65
|
code: "INVALID_CREDENTIALS",
|
|
66
66
|
message: "Invalid credentials."
|
|
67
|
-
})
|
|
67
|
+
});
|
|
68
68
|
}
|
|
69
|
-
const user =
|
|
70
|
-
if (user === null)
|
|
69
|
+
const user = await db.users.getById(existingAccount.userId);
|
|
70
|
+
if (user === null) throw new ConvexError({
|
|
71
71
|
code: "ACCOUNT_NOT_FOUND",
|
|
72
72
|
message: `Linked user for account ${account.id} was not found.`
|
|
73
|
-
})
|
|
73
|
+
});
|
|
74
74
|
return {
|
|
75
|
-
account: existingAccount
|
|
75
|
+
account: existingAccount,
|
|
76
76
|
user
|
|
77
77
|
};
|
|
78
|
-
}
|
|
78
|
+
}
|
|
79
79
|
}
|
|
80
80
|
const callCreateAccountFromCredentials = async (ctx, args) => {
|
|
81
81
|
return ctx.runMutation(AUTH_STORE_REF, { args: {
|
|
@@ -5,7 +5,6 @@ import { log, maybeRedact } from "../log.js";
|
|
|
5
5
|
import { AUTH_STORE_REF } from "./store/refs.js";
|
|
6
6
|
import { isSignInRateLimited, recordFailedSignIn, resetSignInRateLimit } from "../limits.js";
|
|
7
7
|
import { v } from "convex/values";
|
|
8
|
-
import { Effect, Match } from "effect";
|
|
9
8
|
|
|
10
9
|
//#region src/server/mutations/retrieve.ts
|
|
11
10
|
const retrieveAccountWithCredentialsArgs = v.object({
|
|
@@ -15,7 +14,7 @@ const retrieveAccountWithCredentialsArgs = v.object({
|
|
|
15
14
|
secret: v.optional(v.string())
|
|
16
15
|
})
|
|
17
16
|
});
|
|
18
|
-
function retrieveAccountWithCredentialsImpl(ctx, args, getProviderOrThrow, config) {
|
|
17
|
+
async function retrieveAccountWithCredentialsImpl(ctx, args, getProviderOrThrow, config) {
|
|
19
18
|
const { provider: providerId, account } = args;
|
|
20
19
|
const db = authDb(ctx, config);
|
|
21
20
|
log(LOG_LEVELS.DEBUG, "retrieveAccountWithCredentialsImpl args:", {
|
|
@@ -25,27 +24,30 @@ function retrieveAccountWithCredentialsImpl(ctx, args, getProviderOrThrow, confi
|
|
|
25
24
|
secret: maybeRedact(account.secret ?? "")
|
|
26
25
|
}
|
|
27
26
|
});
|
|
28
|
-
|
|
29
|
-
const existingAccount =
|
|
27
|
+
try {
|
|
28
|
+
const existingAccount = await db.accounts.get(providerId, account.id);
|
|
30
29
|
if (existingAccount === null) return "InvalidAccountId";
|
|
31
30
|
if (account.secret !== void 0) {
|
|
32
31
|
const accountSecret = account.secret;
|
|
33
|
-
if (
|
|
34
|
-
if (!
|
|
35
|
-
|
|
32
|
+
if (await isSignInRateLimited(ctx, existingAccount._id, config)) return "TooManyFailedAttempts";
|
|
33
|
+
if (!await verify(getProviderOrThrow(providerId), accountSecret, existingAccount.secret ?? "")) {
|
|
34
|
+
await recordFailedSignIn(ctx, existingAccount._id, config);
|
|
36
35
|
return "InvalidSecret";
|
|
37
36
|
}
|
|
38
|
-
|
|
37
|
+
await resetSignInRateLimit(ctx, existingAccount._id, config);
|
|
39
38
|
}
|
|
40
|
-
const user =
|
|
41
|
-
|
|
39
|
+
const user = await db.users.getById(existingAccount.userId);
|
|
40
|
+
if (user === null) {
|
|
42
41
|
log(LOG_LEVELS.ERROR, `Account ${existingAccount._id} is linked to missing user ${existingAccount.userId}`);
|
|
43
|
-
return
|
|
44
|
-
}
|
|
42
|
+
return "InvalidAccountId";
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
45
|
account: existingAccount,
|
|
46
|
-
user
|
|
47
|
-
}
|
|
48
|
-
}
|
|
46
|
+
user
|
|
47
|
+
};
|
|
48
|
+
} catch {
|
|
49
|
+
return "InvalidAccountId";
|
|
50
|
+
}
|
|
49
51
|
}
|
|
50
52
|
const callRetrieveAccountWithCredentials = async (ctx, args) => {
|
|
51
53
|
return ctx.runMutation(AUTH_STORE_REF, { args: {
|
|
@@ -1,31 +1,27 @@
|
|
|
1
1
|
import { authDb } from "../db.js";
|
|
2
2
|
import { AUTH_STORE_REF } from "./store/refs.js";
|
|
3
3
|
import { ConvexError, v } from "convex/values";
|
|
4
|
-
import { Effect, Option, pipe } from "effect";
|
|
5
4
|
|
|
6
5
|
//#region src/server/mutations/signature.ts
|
|
7
6
|
const verifierSignatureArgs = v.object({
|
|
8
7
|
verifier: v.string(),
|
|
9
8
|
signature: v.string()
|
|
10
9
|
});
|
|
11
|
-
function verifierSignatureImpl(ctx, args, config) {
|
|
10
|
+
async function verifierSignatureImpl(ctx, args, config) {
|
|
12
11
|
const { verifier, signature } = args;
|
|
13
12
|
const db = authDb(ctx, config);
|
|
14
13
|
const invalidVerifierError = new ConvexError({
|
|
15
14
|
code: "INVALID_VERIFIER",
|
|
16
15
|
message: "Invalid or expired verifier."
|
|
17
16
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}));
|
|
27
|
-
yield* Effect.promise(() => db.verifiers.patch(existingVerifier._id, { signature }));
|
|
28
|
-
});
|
|
17
|
+
let verifierDoc;
|
|
18
|
+
try {
|
|
19
|
+
verifierDoc = await db.verifiers.getById(verifier);
|
|
20
|
+
} catch {
|
|
21
|
+
throw invalidVerifierError;
|
|
22
|
+
}
|
|
23
|
+
if (verifierDoc == null) throw invalidVerifierError;
|
|
24
|
+
await db.verifiers.patch(verifierDoc._id, { signature });
|
|
29
25
|
}
|
|
30
26
|
const callVerifierSignature = async (ctx, args) => {
|
|
31
27
|
return ctx.runMutation(AUTH_STORE_REF, { args: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LOG_LEVELS } from "../../shared/log.js";
|
|
2
2
|
import { log } from "../log.js";
|
|
3
3
|
import { AUTH_STORE_REF } from "./store/refs.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getAuthSessionId, issueSession } from "../sessions.js";
|
|
5
5
|
import { v } from "convex/values";
|
|
6
6
|
|
|
7
7
|
//#region src/server/mutations/signin.ts
|
|
@@ -13,8 +13,12 @@ const signInArgs = v.object({
|
|
|
13
13
|
async function signInImpl(ctx, args, config) {
|
|
14
14
|
log(LOG_LEVELS.DEBUG, "signInImpl args:", args);
|
|
15
15
|
const { userId, sessionId: existingSessionId, generateTokens } = args;
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
return await issueSession(ctx, config, {
|
|
17
|
+
userId,
|
|
18
|
+
existingSessionId,
|
|
19
|
+
replaceSessionId: existingSessionId === void 0 ? await getAuthSessionId(ctx) ?? void 0 : void 0,
|
|
20
|
+
generateTokens
|
|
21
|
+
});
|
|
18
22
|
}
|
|
19
23
|
const callSignIn = async (ctx, args) => {
|
|
20
24
|
return ctx.runMutation(AUTH_STORE_REF, { args: {
|