@robelest/convex-auth 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/bin.cjs +27733 -0
- package/dist/client/index.d.ts +49 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +283 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +36 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +295 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +4 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/index.d.ts +15 -0
- package/dist/component/index.d.ts.map +1 -0
- package/dist/component/index.js +13 -0
- package/dist/component/index.js.map +1 -0
- package/dist/component/public.d.ts +450 -0
- package/dist/component/public.d.ts.map +1 -0
- package/dist/component/public.js +528 -0
- package/dist/component/public.js.map +1 -0
- package/dist/component/schema.d.ts +107 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +26 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/providers/Anonymous.d.ts +50 -0
- package/dist/providers/Anonymous.d.ts.map +1 -0
- package/dist/providers/Anonymous.js +39 -0
- package/dist/providers/Anonymous.js.map +1 -0
- package/dist/providers/ConvexCredentials.d.ts +88 -0
- package/dist/providers/ConvexCredentials.d.ts.map +1 -0
- package/dist/providers/ConvexCredentials.js +37 -0
- package/dist/providers/ConvexCredentials.js.map +1 -0
- package/dist/providers/Email.d.ts +33 -0
- package/dist/providers/Email.d.ts.map +1 -0
- package/dist/providers/Email.js +50 -0
- package/dist/providers/Email.js.map +1 -0
- package/dist/providers/Password.d.ts +95 -0
- package/dist/providers/Password.d.ts.map +1 -0
- package/dist/providers/Password.js +174 -0
- package/dist/providers/Password.js.map +1 -0
- package/dist/providers/Phone.d.ts +22 -0
- package/dist/providers/Phone.d.ts.map +1 -0
- package/dist/providers/Phone.js +37 -0
- package/dist/providers/Phone.js.map +1 -0
- package/dist/server/convex_types.d.ts +17 -0
- package/dist/server/convex_types.d.ts.map +1 -0
- package/dist/server/convex_types.js +2 -0
- package/dist/server/convex_types.js.map +1 -0
- package/dist/server/cookies.d.ts +35 -0
- package/dist/server/cookies.d.ts.map +1 -0
- package/dist/server/cookies.js +34 -0
- package/dist/server/cookies.js.map +1 -0
- package/dist/server/implementation/db.d.ts +80 -0
- package/dist/server/implementation/db.d.ts.map +1 -0
- package/dist/server/implementation/db.js +59 -0
- package/dist/server/implementation/db.js.map +1 -0
- package/dist/server/implementation/index.d.ts +370 -0
- package/dist/server/implementation/index.d.ts.map +1 -0
- package/dist/server/implementation/index.js +521 -0
- package/dist/server/implementation/index.js.map +1 -0
- package/dist/server/implementation/mutations/createAccountFromCredentials.d.ts +33 -0
- package/dist/server/implementation/mutations/createAccountFromCredentials.d.ts.map +1 -0
- package/dist/server/implementation/mutations/createAccountFromCredentials.js +71 -0
- package/dist/server/implementation/mutations/createAccountFromCredentials.js.map +1 -0
- package/dist/server/implementation/mutations/createVerificationCode.d.ts +25 -0
- package/dist/server/implementation/mutations/createVerificationCode.d.ts.map +1 -0
- package/dist/server/implementation/mutations/createVerificationCode.js +84 -0
- package/dist/server/implementation/mutations/createVerificationCode.js.map +1 -0
- package/dist/server/implementation/mutations/index.d.ts +304 -0
- package/dist/server/implementation/mutations/index.d.ts.map +1 -0
- package/dist/server/implementation/mutations/index.js +108 -0
- package/dist/server/implementation/mutations/index.js.map +1 -0
- package/dist/server/implementation/mutations/invalidateSessions.d.ts +13 -0
- package/dist/server/implementation/mutations/invalidateSessions.d.ts.map +1 -0
- package/dist/server/implementation/mutations/invalidateSessions.js +35 -0
- package/dist/server/implementation/mutations/invalidateSessions.js.map +1 -0
- package/dist/server/implementation/mutations/modifyAccount.d.ts +23 -0
- package/dist/server/implementation/mutations/modifyAccount.d.ts.map +1 -0
- package/dist/server/implementation/mutations/modifyAccount.js +48 -0
- package/dist/server/implementation/mutations/modifyAccount.js.map +1 -0
- package/dist/server/implementation/mutations/refreshSession.d.ts +16 -0
- package/dist/server/implementation/mutations/refreshSession.d.ts.map +1 -0
- package/dist/server/implementation/mutations/refreshSession.js +116 -0
- package/dist/server/implementation/mutations/refreshSession.js.map +1 -0
- package/dist/server/implementation/mutations/retrieveAccountWithCredentials.d.ts +27 -0
- package/dist/server/implementation/mutations/retrieveAccountWithCredentials.d.ts.map +1 -0
- package/dist/server/implementation/mutations/retrieveAccountWithCredentials.js +55 -0
- package/dist/server/implementation/mutations/retrieveAccountWithCredentials.js.map +1 -0
- package/dist/server/implementation/mutations/signIn.d.ts +17 -0
- package/dist/server/implementation/mutations/signIn.d.ts.map +1 -0
- package/dist/server/implementation/mutations/signIn.js +26 -0
- package/dist/server/implementation/mutations/signIn.js.map +1 -0
- package/dist/server/implementation/mutations/signOut.d.ts +11 -0
- package/dist/server/implementation/mutations/signOut.d.ts.map +1 -0
- package/dist/server/implementation/mutations/signOut.js +24 -0
- package/dist/server/implementation/mutations/signOut.js.map +1 -0
- package/dist/server/implementation/mutations/userOAuth.d.ts +19 -0
- package/dist/server/implementation/mutations/userOAuth.d.ts.map +1 -0
- package/dist/server/implementation/mutations/userOAuth.js +84 -0
- package/dist/server/implementation/mutations/userOAuth.js.map +1 -0
- package/dist/server/implementation/mutations/verifier.d.ts +8 -0
- package/dist/server/implementation/mutations/verifier.d.ts.map +1 -0
- package/dist/server/implementation/mutations/verifier.js +19 -0
- package/dist/server/implementation/mutations/verifier.js.map +1 -0
- package/dist/server/implementation/mutations/verifierSignature.d.ts +15 -0
- package/dist/server/implementation/mutations/verifierSignature.d.ts.map +1 -0
- package/dist/server/implementation/mutations/verifierSignature.js +29 -0
- package/dist/server/implementation/mutations/verifierSignature.js.map +1 -0
- package/dist/server/implementation/mutations/verifyCodeAndSignIn.d.ts +21 -0
- package/dist/server/implementation/mutations/verifyCodeAndSignIn.d.ts.map +1 -0
- package/dist/server/implementation/mutations/verifyCodeAndSignIn.js +127 -0
- package/dist/server/implementation/mutations/verifyCodeAndSignIn.js.map +1 -0
- package/dist/server/implementation/provider.d.ts +6 -0
- package/dist/server/implementation/provider.d.ts.map +1 -0
- package/dist/server/implementation/provider.js +21 -0
- package/dist/server/implementation/provider.js.map +1 -0
- package/dist/server/implementation/rateLimit.d.ts +6 -0
- package/dist/server/implementation/rateLimit.d.ts.map +1 -0
- package/dist/server/implementation/rateLimit.js +76 -0
- package/dist/server/implementation/rateLimit.js.map +1 -0
- package/dist/server/implementation/redirects.d.ts +6 -0
- package/dist/server/implementation/redirects.d.ts.map +1 -0
- package/dist/server/implementation/redirects.js +40 -0
- package/dist/server/implementation/redirects.js.map +1 -0
- package/dist/server/implementation/refreshTokens.d.ts +40 -0
- package/dist/server/implementation/refreshTokens.d.ts.map +1 -0
- package/dist/server/implementation/refreshTokens.js +160 -0
- package/dist/server/implementation/refreshTokens.js.map +1 -0
- package/dist/server/implementation/sessions.d.ts +43 -0
- package/dist/server/implementation/sessions.d.ts.map +1 -0
- package/dist/server/implementation/sessions.js +94 -0
- package/dist/server/implementation/sessions.js.map +1 -0
- package/dist/server/implementation/signIn.d.ts +31 -0
- package/dist/server/implementation/signIn.d.ts.map +1 -0
- package/dist/server/implementation/signIn.js +148 -0
- package/dist/server/implementation/signIn.js.map +1 -0
- package/dist/server/implementation/tokens.d.ts +7 -0
- package/dist/server/implementation/tokens.d.ts.map +1 -0
- package/dist/server/implementation/tokens.js +18 -0
- package/dist/server/implementation/tokens.js.map +1 -0
- package/dist/server/implementation/types.d.ts +288 -0
- package/dist/server/implementation/types.d.ts.map +1 -0
- package/dist/server/implementation/types.js +182 -0
- package/dist/server/implementation/types.js.map +1 -0
- package/dist/server/implementation/users.d.ts +27 -0
- package/dist/server/implementation/users.d.ts.map +1 -0
- package/dist/server/implementation/users.js +181 -0
- package/dist/server/implementation/users.js.map +1 -0
- package/dist/server/implementation/utils.d.ts +17 -0
- package/dist/server/implementation/utils.d.ts.map +1 -0
- package/dist/server/implementation/utils.js +72 -0
- package/dist/server/implementation/utils.js.map +1 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +54 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/oauth/authorizationUrl.d.ts +13 -0
- package/dist/server/oauth/authorizationUrl.d.ts.map +1 -0
- package/dist/server/oauth/authorizationUrl.js +91 -0
- package/dist/server/oauth/authorizationUrl.js.map +1 -0
- package/dist/server/oauth/callback.d.ts +19 -0
- package/dist/server/oauth/callback.d.ts.map +1 -0
- package/dist/server/oauth/callback.js +173 -0
- package/dist/server/oauth/callback.js.map +1 -0
- package/dist/server/oauth/checks.d.ts +52 -0
- package/dist/server/oauth/checks.d.ts.map +1 -0
- package/dist/server/oauth/checks.js +106 -0
- package/dist/server/oauth/checks.js.map +1 -0
- package/dist/server/oauth/convexAuth.d.ts +12 -0
- package/dist/server/oauth/convexAuth.d.ts.map +1 -0
- package/dist/server/oauth/convexAuth.js +137 -0
- package/dist/server/oauth/convexAuth.js.map +1 -0
- package/dist/server/oauth/lib/utils/customFetch.d.ts +9 -0
- package/dist/server/oauth/lib/utils/customFetch.d.ts.map +1 -0
- package/dist/server/oauth/lib/utils/customFetch.js +11 -0
- package/dist/server/oauth/lib/utils/customFetch.js.map +1 -0
- package/dist/server/oauth/lib/utils/providers.d.ts +3 -0
- package/dist/server/oauth/lib/utils/providers.d.ts.map +1 -0
- package/dist/server/oauth/lib/utils/providers.js +7 -0
- package/dist/server/oauth/lib/utils/providers.js.map +1 -0
- package/dist/server/oauth/providers/oauth.d.ts +43 -0
- package/dist/server/oauth/providers/oauth.d.ts.map +1 -0
- package/dist/server/oauth/providers/oauth.js +3 -0
- package/dist/server/oauth/providers/oauth.js.map +1 -0
- package/dist/server/oauth/types.d.ts +24 -0
- package/dist/server/oauth/types.d.ts.map +1 -0
- package/dist/server/oauth/types.js +5 -0
- package/dist/server/oauth/types.js.map +1 -0
- package/dist/server/provider_utils.d.ts +76 -0
- package/dist/server/provider_utils.d.ts.map +1 -0
- package/dist/server/provider_utils.js +177 -0
- package/dist/server/provider_utils.js.map +1 -0
- package/dist/server/types.d.ts +412 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server/utils.d.ts +3 -0
- package/dist/server/utils.d.ts.map +1 -0
- package/dist/server/utils.js +11 -0
- package/dist/server/utils.js.map +1 -0
- package/package.json +126 -0
- package/providers/Anonymous/package.json +6 -0
- package/providers/ConvexCredentials/package.json +6 -0
- package/providers/Email/package.json +6 -0
- package/providers/Password/package.json +6 -0
- package/providers/Phone/package.json +6 -0
- package/server/package.json +6 -0
- package/src/cli/command.ts +69 -0
- package/src/cli/generateKeys.ts +20 -0
- package/src/cli/index.ts +840 -0
- package/src/client/index.ts +415 -0
- package/src/component/_generated/api.ts +52 -0
- package/src/component/_generated/component.ts +586 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/convex.config.ts +5 -0
- package/src/component/index.ts +40 -0
- package/src/component/public.ts +607 -0
- package/src/component/schema.ts +35 -0
- package/src/providers/Anonymous.ts +79 -0
- package/src/providers/ConvexCredentials.ts +108 -0
- package/src/providers/Email.ts +60 -0
- package/src/providers/Password.ts +253 -0
- package/src/providers/Phone.ts +46 -0
- package/src/server/convex_types.ts +55 -0
- package/src/server/cookies.ts +42 -0
- package/src/server/implementation/db.ts +125 -0
- package/src/server/implementation/index.ts +815 -0
- package/src/server/implementation/mutations/createAccountFromCredentials.ts +113 -0
- package/src/server/implementation/mutations/createVerificationCode.ts +139 -0
- package/src/server/implementation/mutations/index.ts +157 -0
- package/src/server/implementation/mutations/invalidateSessions.ts +47 -0
- package/src/server/implementation/mutations/modifyAccount.ts +65 -0
- package/src/server/implementation/mutations/refreshSession.ts +188 -0
- package/src/server/implementation/mutations/retrieveAccountWithCredentials.ts +87 -0
- package/src/server/implementation/mutations/signIn.ts +51 -0
- package/src/server/implementation/mutations/signOut.ts +38 -0
- package/src/server/implementation/mutations/userOAuth.ts +112 -0
- package/src/server/implementation/mutations/verifier.ts +29 -0
- package/src/server/implementation/mutations/verifierSignature.ts +44 -0
- package/src/server/implementation/mutations/verifyCodeAndSignIn.ts +205 -0
- package/src/server/implementation/provider.ts +38 -0
- package/src/server/implementation/rateLimit.ts +105 -0
- package/src/server/implementation/redirects.ts +58 -0
- package/src/server/implementation/refreshTokens.ts +221 -0
- package/src/server/implementation/sessions.ts +155 -0
- package/src/server/implementation/signIn.ts +253 -0
- package/src/server/implementation/tokens.ts +29 -0
- package/src/server/implementation/types.ts +220 -0
- package/src/server/implementation/users.ts +286 -0
- package/src/server/implementation/utils.ts +91 -0
- package/src/server/index.ts +74 -0
- package/src/server/oauth/NOTICE.txt +21 -0
- package/src/server/oauth/README.md +7 -0
- package/src/server/oauth/authorizationUrl.ts +113 -0
- package/src/server/oauth/callback.ts +243 -0
- package/src/server/oauth/checks.ts +136 -0
- package/src/server/oauth/convexAuth.ts +168 -0
- package/src/server/oauth/lib/utils/customFetch.ts +18 -0
- package/src/server/oauth/lib/utils/providers.ts +12 -0
- package/src/server/oauth/providers/oauth.ts +56 -0
- package/src/server/oauth/types.ts +60 -0
- package/src/server/provider_utils.ts +222 -0
- package/src/server/types.ts +470 -0
- package/src/server/utils.ts +12 -0
- package/src/test.ts +24 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ConvexAuthConfig } from "../types.js";
|
|
2
|
+
import { Doc, MutationCtx } from "./types.js";
|
|
3
|
+
import { createAuthDb } from "./db.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_SIGN_IN_ATTEMPTS_PER_HOUR = 10;
|
|
6
|
+
|
|
7
|
+
export async function isSignInRateLimited(
|
|
8
|
+
ctx: MutationCtx,
|
|
9
|
+
identifier: string,
|
|
10
|
+
config: ConvexAuthConfig,
|
|
11
|
+
) {
|
|
12
|
+
const state = await getRateLimitState(ctx, identifier, config);
|
|
13
|
+
if (state === null) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return state.attempsLeft < 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function recordFailedSignIn(
|
|
20
|
+
ctx: MutationCtx,
|
|
21
|
+
identifier: string,
|
|
22
|
+
config: ConvexAuthConfig,
|
|
23
|
+
) {
|
|
24
|
+
const state = await getRateLimitState(ctx, identifier, config);
|
|
25
|
+
if (state !== null) {
|
|
26
|
+
if (config.component !== undefined) {
|
|
27
|
+
await createAuthDb(ctx, config.component).rateLimits.patch(state.limit._id, {
|
|
28
|
+
attemptsLeft: state.attempsLeft - 1,
|
|
29
|
+
lastAttemptTime: Date.now(),
|
|
30
|
+
});
|
|
31
|
+
} else {
|
|
32
|
+
await ctx.db.patch(state.limit._id, {
|
|
33
|
+
attemptsLeft: state.attempsLeft - 1,
|
|
34
|
+
lastAttemptTime: Date.now(),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
const maxAttempsPerHour = configuredMaxAttempsPerHour(config);
|
|
39
|
+
if (config.component !== undefined) {
|
|
40
|
+
await createAuthDb(ctx, config.component).rateLimits.create({
|
|
41
|
+
identifier,
|
|
42
|
+
attemptsLeft: maxAttempsPerHour - 1,
|
|
43
|
+
lastAttemptTime: Date.now(),
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
await ctx.db.insert("limit", {
|
|
47
|
+
identifier,
|
|
48
|
+
attemptsLeft: maxAttempsPerHour - 1,
|
|
49
|
+
lastAttemptTime: Date.now(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function resetSignInRateLimit(
|
|
56
|
+
ctx: MutationCtx,
|
|
57
|
+
identifier: string,
|
|
58
|
+
config: ConvexAuthConfig,
|
|
59
|
+
) {
|
|
60
|
+
const existingState = await getRateLimitState(ctx, identifier, config);
|
|
61
|
+
if (existingState !== null) {
|
|
62
|
+
if (config.component !== undefined) {
|
|
63
|
+
await createAuthDb(ctx, config.component).rateLimits.delete(
|
|
64
|
+
existingState.limit._id,
|
|
65
|
+
);
|
|
66
|
+
} else {
|
|
67
|
+
await ctx.db.delete(existingState.limit._id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function getRateLimitState(
|
|
73
|
+
ctx: MutationCtx,
|
|
74
|
+
identifier: string,
|
|
75
|
+
config: ConvexAuthConfig,
|
|
76
|
+
) {
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
const maxAttempsPerHour = configuredMaxAttempsPerHour(config);
|
|
79
|
+
const limit =
|
|
80
|
+
config.component !== undefined
|
|
81
|
+
? ((await createAuthDb(ctx, config.component).rateLimits.get(identifier)) as
|
|
82
|
+
| Doc<"limit">
|
|
83
|
+
| null)
|
|
84
|
+
: await ctx.db
|
|
85
|
+
.query("limit")
|
|
86
|
+
.withIndex("identifier", (q) => q.eq("identifier", identifier))
|
|
87
|
+
.unique();
|
|
88
|
+
if (limit === null) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const elapsed = now - limit.lastAttemptTime;
|
|
92
|
+
const maxAttempsPerMs = maxAttempsPerHour / (60 * 60 * 1000);
|
|
93
|
+
const attempsLeft = Math.min(
|
|
94
|
+
maxAttempsPerHour,
|
|
95
|
+
limit.attemptsLeft + elapsed * maxAttempsPerMs,
|
|
96
|
+
);
|
|
97
|
+
return { limit, attempsLeft };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function configuredMaxAttempsPerHour(config: ConvexAuthConfig) {
|
|
101
|
+
return (
|
|
102
|
+
config.signIn?.maxFailedAttempsPerHour ??
|
|
103
|
+
DEFAULT_MAX_SIGN_IN_ATTEMPTS_PER_HOUR
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ConvexAuthMaterializedConfig } from "../types.js";
|
|
2
|
+
import { requireEnv } from "../utils.js";
|
|
3
|
+
|
|
4
|
+
export async function redirectAbsoluteUrl(
|
|
5
|
+
config: ConvexAuthMaterializedConfig,
|
|
6
|
+
params: { redirectTo: unknown },
|
|
7
|
+
) {
|
|
8
|
+
if (params.redirectTo !== undefined) {
|
|
9
|
+
if (typeof params.redirectTo !== "string") {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`Expected \`redirectTo\` to be a string, got ${params.redirectTo as any}`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
const redirectCallback =
|
|
15
|
+
config.callbacks?.redirect ?? defaultRedirectCallback;
|
|
16
|
+
return await redirectCallback(params as { redirectTo: string });
|
|
17
|
+
}
|
|
18
|
+
return siteUrl();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function defaultRedirectCallback({ redirectTo }: { redirectTo: string }) {
|
|
22
|
+
const baseUrl = siteUrl();
|
|
23
|
+
if (redirectTo.startsWith("?") || redirectTo.startsWith("/")) {
|
|
24
|
+
return `${baseUrl}${redirectTo}`;
|
|
25
|
+
}
|
|
26
|
+
if (redirectTo.startsWith(baseUrl)) {
|
|
27
|
+
const after = redirectTo[baseUrl.length];
|
|
28
|
+
if (after === undefined || after === "?" || after === "/") {
|
|
29
|
+
return redirectTo;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Invalid \`redirectTo\` ${redirectTo} for configured SITE_URL: ${baseUrl.toString()}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Temporary work-around because Convex doesn't support
|
|
38
|
+
// schemes other than http and https.
|
|
39
|
+
export function setURLSearchParam(
|
|
40
|
+
absoluteUrl: string,
|
|
41
|
+
param: string,
|
|
42
|
+
value: string,
|
|
43
|
+
) {
|
|
44
|
+
const pattern = /([^:]+):(.*)/;
|
|
45
|
+
const [, scheme, rest] = absoluteUrl.match(pattern)!;
|
|
46
|
+
const hasNoDomain = /^\/\/(?:\/|$|\?)/.test(rest);
|
|
47
|
+
const startsWithPath = hasNoDomain && rest.startsWith("///");
|
|
48
|
+
const url = new URL(
|
|
49
|
+
`http:${hasNoDomain ? "//googblibok" + rest.slice(2) : rest}`,
|
|
50
|
+
);
|
|
51
|
+
url.searchParams.set(param, value);
|
|
52
|
+
const [, , withParam] = url.toString().match(pattern)!;
|
|
53
|
+
return `${scheme}:${hasNoDomain ? (startsWithPath ? "/" : "") + "//" + withParam.slice(13) : withParam}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function siteUrl() {
|
|
57
|
+
return requireEnv("SITE_URL").replace(/\/$/, "");
|
|
58
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { GenericId } from "convex/values";
|
|
2
|
+
import { ConvexAuthConfig } from "../types.js";
|
|
3
|
+
import { Doc, MutationCtx } from "./types.js";
|
|
4
|
+
import {
|
|
5
|
+
LOG_LEVELS,
|
|
6
|
+
REFRESH_TOKEN_DIVIDER,
|
|
7
|
+
logWithLevel,
|
|
8
|
+
maybeRedact,
|
|
9
|
+
stringToNumber,
|
|
10
|
+
} from "./utils.js";
|
|
11
|
+
import { createAuthDb } from "./db.js";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_SESSION_INACTIVE_DURATION_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
14
|
+
export const REFRESH_TOKEN_REUSE_WINDOW_MS = 10 * 1000; // 10 seconds
|
|
15
|
+
export async function createRefreshToken(
|
|
16
|
+
ctx: MutationCtx,
|
|
17
|
+
config: ConvexAuthConfig,
|
|
18
|
+
sessionId: GenericId<"session">,
|
|
19
|
+
parentRefreshTokenId: GenericId<"token"> | null,
|
|
20
|
+
) {
|
|
21
|
+
const expirationTime =
|
|
22
|
+
Date.now() +
|
|
23
|
+
(config.session?.inactiveDurationMs ??
|
|
24
|
+
stringToNumber(process.env.AUTH_SESSION_INACTIVE_DURATION_MS) ??
|
|
25
|
+
DEFAULT_SESSION_INACTIVE_DURATION_MS);
|
|
26
|
+
if (config.component !== undefined) {
|
|
27
|
+
return (await createAuthDb(ctx, config.component).refreshTokens.create({
|
|
28
|
+
sessionId,
|
|
29
|
+
expirationTime,
|
|
30
|
+
parentRefreshTokenId: parentRefreshTokenId ?? undefined,
|
|
31
|
+
})) as GenericId<"token">;
|
|
32
|
+
}
|
|
33
|
+
const newRefreshTokenId = await ctx.db.insert("token", {
|
|
34
|
+
sessionId,
|
|
35
|
+
expirationTime,
|
|
36
|
+
parentRefreshTokenId: parentRefreshTokenId ?? undefined,
|
|
37
|
+
});
|
|
38
|
+
return newRefreshTokenId;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const formatRefreshToken = (
|
|
42
|
+
refreshTokenId: GenericId<"token">,
|
|
43
|
+
sessionId: GenericId<"session">,
|
|
44
|
+
) => {
|
|
45
|
+
return `${refreshTokenId}${REFRESH_TOKEN_DIVIDER}${sessionId}`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const parseRefreshToken = (
|
|
49
|
+
refreshToken: string,
|
|
50
|
+
): {
|
|
51
|
+
refreshTokenId: GenericId<"token">;
|
|
52
|
+
sessionId: GenericId<"session">;
|
|
53
|
+
} => {
|
|
54
|
+
const [refreshTokenId, sessionId] = refreshToken.split(REFRESH_TOKEN_DIVIDER);
|
|
55
|
+
if (!refreshTokenId || !sessionId) {
|
|
56
|
+
throw new Error(`Can't parse refresh token: ${maybeRedact(refreshToken)}`);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
refreshTokenId: refreshTokenId as GenericId<"token">,
|
|
60
|
+
sessionId: sessionId as GenericId<"session">,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Mark all refresh tokens descending from the given refresh token as invalid immediately.
|
|
66
|
+
* This is used when we detect an invalid use of a refresh token, and want to revoke
|
|
67
|
+
* the entire tree.
|
|
68
|
+
*
|
|
69
|
+
* @param ctx
|
|
70
|
+
* @param refreshToken
|
|
71
|
+
*/
|
|
72
|
+
export async function invalidateRefreshTokensInSubtree(
|
|
73
|
+
ctx: MutationCtx,
|
|
74
|
+
refreshToken: Doc<"token">,
|
|
75
|
+
config: ConvexAuthConfig,
|
|
76
|
+
) {
|
|
77
|
+
const authDb =
|
|
78
|
+
config.component !== undefined ? createAuthDb(ctx, config.component) : null;
|
|
79
|
+
const tokensToInvalidate = [refreshToken];
|
|
80
|
+
let frontier = [refreshToken._id];
|
|
81
|
+
while (frontier.length > 0) {
|
|
82
|
+
const nextFrontier = [];
|
|
83
|
+
for (const currentTokenId of frontier) {
|
|
84
|
+
const children =
|
|
85
|
+
authDb !== null
|
|
86
|
+
? ((await authDb.refreshTokens.getChildren(
|
|
87
|
+
refreshToken.sessionId,
|
|
88
|
+
currentTokenId,
|
|
89
|
+
)) as Doc<"token">[])
|
|
90
|
+
: await ctx.db
|
|
91
|
+
.query("token")
|
|
92
|
+
.withIndex("sessionIdAndParentRefreshTokenId", (q) =>
|
|
93
|
+
q
|
|
94
|
+
.eq("sessionId", refreshToken.sessionId)
|
|
95
|
+
.eq("parentRefreshTokenId", currentTokenId),
|
|
96
|
+
)
|
|
97
|
+
.collect();
|
|
98
|
+
tokensToInvalidate.push(...children);
|
|
99
|
+
nextFrontier.push(...children.map((child) => child._id));
|
|
100
|
+
}
|
|
101
|
+
frontier = nextFrontier;
|
|
102
|
+
}
|
|
103
|
+
for (const token of tokensToInvalidate) {
|
|
104
|
+
// Mark these as used so they can't be used again (even within the reuse window)
|
|
105
|
+
if (
|
|
106
|
+
token.firstUsedTime === undefined ||
|
|
107
|
+
token.firstUsedTime > Date.now() - REFRESH_TOKEN_REUSE_WINDOW_MS
|
|
108
|
+
) {
|
|
109
|
+
if (authDb !== null) {
|
|
110
|
+
await authDb.refreshTokens.patch(token._id, {
|
|
111
|
+
firstUsedTime: Date.now() - REFRESH_TOKEN_REUSE_WINDOW_MS,
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
await ctx.db.patch(token._id, {
|
|
115
|
+
firstUsedTime: Date.now() - REFRESH_TOKEN_REUSE_WINDOW_MS,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return tokensToInvalidate;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function deleteAllRefreshTokens(
|
|
124
|
+
ctx: MutationCtx,
|
|
125
|
+
sessionId: GenericId<"session">,
|
|
126
|
+
config: ConvexAuthConfig,
|
|
127
|
+
) {
|
|
128
|
+
if (config.component !== undefined) {
|
|
129
|
+
await createAuthDb(ctx, config.component).refreshTokens.deleteAll(sessionId);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const existingRefreshTokens = await ctx.db
|
|
133
|
+
.query("token")
|
|
134
|
+
.withIndex("sessionIdAndParentRefreshTokenId", (q) =>
|
|
135
|
+
q.eq("sessionId", sessionId),
|
|
136
|
+
)
|
|
137
|
+
.collect();
|
|
138
|
+
for (const refreshTokenDoc of existingRefreshTokens) {
|
|
139
|
+
await ctx.db.delete(refreshTokenDoc._id);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function refreshTokenIfValid(
|
|
144
|
+
ctx: MutationCtx,
|
|
145
|
+
refreshTokenId: string,
|
|
146
|
+
tokenSessionId: string,
|
|
147
|
+
config: ConvexAuthConfig,
|
|
148
|
+
) {
|
|
149
|
+
const authDb =
|
|
150
|
+
config.component !== undefined ? createAuthDb(ctx, config.component) : null;
|
|
151
|
+
let refreshTokenDoc: Doc<"token"> | null;
|
|
152
|
+
try {
|
|
153
|
+
refreshTokenDoc =
|
|
154
|
+
authDb !== null
|
|
155
|
+
? ((await authDb.refreshTokens.getById(
|
|
156
|
+
refreshTokenId as GenericId<"token">,
|
|
157
|
+
)) as Doc<"token"> | null)
|
|
158
|
+
: await ctx.db.get(refreshTokenId as GenericId<"token">);
|
|
159
|
+
} catch {
|
|
160
|
+
logWithLevel(LOG_LEVELS.ERROR, "Invalid refresh token format");
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (refreshTokenDoc === null) {
|
|
165
|
+
logWithLevel(LOG_LEVELS.ERROR, "Invalid refresh token");
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
if (refreshTokenDoc.expirationTime < Date.now()) {
|
|
169
|
+
logWithLevel(LOG_LEVELS.ERROR, "Expired refresh token");
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (refreshTokenDoc.sessionId !== tokenSessionId) {
|
|
173
|
+
logWithLevel(LOG_LEVELS.ERROR, "Invalid refresh token session ID");
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
let session: Doc<"session"> | null;
|
|
177
|
+
try {
|
|
178
|
+
session =
|
|
179
|
+
authDb !== null
|
|
180
|
+
? ((await authDb.sessions.getById(refreshTokenDoc.sessionId)) as
|
|
181
|
+
| Doc<"session">
|
|
182
|
+
| null)
|
|
183
|
+
: await ctx.db.get(refreshTokenDoc.sessionId);
|
|
184
|
+
} catch {
|
|
185
|
+
logWithLevel(LOG_LEVELS.ERROR, "Invalid refresh token session format");
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (session === null) {
|
|
189
|
+
logWithLevel(LOG_LEVELS.ERROR, "Invalid refresh token session");
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (session.expirationTime < Date.now()) {
|
|
193
|
+
logWithLevel(LOG_LEVELS.ERROR, "Expired refresh token session");
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return { session, refreshTokenDoc };
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The active refresh token is the most recently created refresh token that has
|
|
200
|
+
* never been used.
|
|
201
|
+
*
|
|
202
|
+
* @param ctx
|
|
203
|
+
* @param sessionId
|
|
204
|
+
*/
|
|
205
|
+
export async function loadActiveRefreshToken(
|
|
206
|
+
ctx: MutationCtx,
|
|
207
|
+
sessionId: GenericId<"session">,
|
|
208
|
+
config: ConvexAuthConfig,
|
|
209
|
+
) {
|
|
210
|
+
if (config.component !== undefined) {
|
|
211
|
+
return (await createAuthDb(ctx, config.component).refreshTokens.getActive(
|
|
212
|
+
sessionId,
|
|
213
|
+
)) as Doc<"token"> | null;
|
|
214
|
+
}
|
|
215
|
+
return ctx.db
|
|
216
|
+
.query("token")
|
|
217
|
+
.withIndex("sessionId", (q) => q.eq("sessionId", sessionId))
|
|
218
|
+
.filter((q) => q.eq(q.field("firstUsedTime"), undefined))
|
|
219
|
+
.order("desc")
|
|
220
|
+
.first();
|
|
221
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { GenericId } from "convex/values";
|
|
2
|
+
import { ConvexAuthConfig } from "../types.js";
|
|
3
|
+
import { Doc, MutationCtx, SessionInfo } from "./types.js";
|
|
4
|
+
import { Auth } from "convex/server";
|
|
5
|
+
import {
|
|
6
|
+
LOG_LEVELS,
|
|
7
|
+
TOKEN_SUB_CLAIM_DIVIDER,
|
|
8
|
+
logWithLevel,
|
|
9
|
+
maybeRedact,
|
|
10
|
+
stringToNumber,
|
|
11
|
+
} from "./utils.js";
|
|
12
|
+
import { generateToken } from "./tokens.js";
|
|
13
|
+
import {
|
|
14
|
+
createRefreshToken,
|
|
15
|
+
formatRefreshToken,
|
|
16
|
+
deleteAllRefreshTokens,
|
|
17
|
+
} from "./refreshTokens.js";
|
|
18
|
+
import { createAuthDb } from "./db.js";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SESSION_TOTAL_DURATION_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
21
|
+
|
|
22
|
+
export async function maybeGenerateTokensForSession(
|
|
23
|
+
ctx: MutationCtx,
|
|
24
|
+
config: ConvexAuthConfig,
|
|
25
|
+
userId: GenericId<"user">,
|
|
26
|
+
sessionId: GenericId<"session">,
|
|
27
|
+
generateTokens: boolean,
|
|
28
|
+
): Promise<SessionInfo> {
|
|
29
|
+
return {
|
|
30
|
+
userId,
|
|
31
|
+
sessionId,
|
|
32
|
+
tokens: generateTokens
|
|
33
|
+
? await generateTokensForSession(ctx, config, {
|
|
34
|
+
userId,
|
|
35
|
+
sessionId,
|
|
36
|
+
issuedRefreshTokenId: null,
|
|
37
|
+
parentRefreshTokenId: null,
|
|
38
|
+
})
|
|
39
|
+
: null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function createNewAndDeleteExistingSession(
|
|
44
|
+
ctx: MutationCtx,
|
|
45
|
+
config: ConvexAuthConfig,
|
|
46
|
+
userId: GenericId<"user">,
|
|
47
|
+
) {
|
|
48
|
+
const authDb =
|
|
49
|
+
config.component !== undefined ? createAuthDb(ctx, config.component) : null;
|
|
50
|
+
const existingSessionId = await getAuthSessionId(ctx);
|
|
51
|
+
if (existingSessionId !== null) {
|
|
52
|
+
const existingSession =
|
|
53
|
+
authDb !== null
|
|
54
|
+
? await authDb.sessions.getById(existingSessionId)
|
|
55
|
+
: await ctx.db.get(existingSessionId);
|
|
56
|
+
if (existingSession !== null) {
|
|
57
|
+
await deleteSession(ctx, existingSession, config);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return await createSession(ctx, userId, config);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function generateTokensForSession(
|
|
64
|
+
ctx: MutationCtx,
|
|
65
|
+
config: ConvexAuthConfig,
|
|
66
|
+
args: {
|
|
67
|
+
userId: GenericId<"user">;
|
|
68
|
+
sessionId: GenericId<"session">;
|
|
69
|
+
issuedRefreshTokenId: GenericId<"token"> | null;
|
|
70
|
+
parentRefreshTokenId: GenericId<"token"> | null;
|
|
71
|
+
},
|
|
72
|
+
) {
|
|
73
|
+
const ids = { userId: args.userId, sessionId: args.sessionId };
|
|
74
|
+
const refreshTokenId =
|
|
75
|
+
args.issuedRefreshTokenId ??
|
|
76
|
+
(await createRefreshToken(
|
|
77
|
+
ctx,
|
|
78
|
+
config,
|
|
79
|
+
args.sessionId,
|
|
80
|
+
args.parentRefreshTokenId,
|
|
81
|
+
));
|
|
82
|
+
const result = {
|
|
83
|
+
token: await generateToken(ids, config),
|
|
84
|
+
refreshToken: formatRefreshToken(refreshTokenId, args.sessionId),
|
|
85
|
+
};
|
|
86
|
+
logWithLevel(
|
|
87
|
+
LOG_LEVELS.DEBUG,
|
|
88
|
+
`Generated token ${maybeRedact(result.token)} and refresh token ${maybeRedact(refreshTokenId)} for session ${maybeRedact(args.sessionId)}`,
|
|
89
|
+
);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function createSession(
|
|
94
|
+
ctx: MutationCtx,
|
|
95
|
+
userId: GenericId<"user">,
|
|
96
|
+
config: ConvexAuthConfig,
|
|
97
|
+
) {
|
|
98
|
+
const expirationTime =
|
|
99
|
+
Date.now() +
|
|
100
|
+
(config.session?.totalDurationMs ??
|
|
101
|
+
stringToNumber(process.env.AUTH_SESSION_TOTAL_DURATION_MS) ??
|
|
102
|
+
DEFAULT_SESSION_TOTAL_DURATION_MS);
|
|
103
|
+
if (config.component !== undefined) {
|
|
104
|
+
return (await createAuthDb(ctx, config.component).sessions.create(
|
|
105
|
+
userId,
|
|
106
|
+
expirationTime,
|
|
107
|
+
)) as GenericId<"session">;
|
|
108
|
+
}
|
|
109
|
+
return await ctx.db.insert("session", { expirationTime, userId });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function deleteSession(
|
|
113
|
+
ctx: MutationCtx,
|
|
114
|
+
session: Doc<"session">,
|
|
115
|
+
config: ConvexAuthConfig,
|
|
116
|
+
) {
|
|
117
|
+
if (config.component !== undefined) {
|
|
118
|
+
await createAuthDb(ctx, config.component).sessions.delete(session._id);
|
|
119
|
+
} else {
|
|
120
|
+
await ctx.db.delete(session._id);
|
|
121
|
+
}
|
|
122
|
+
await deleteAllRefreshTokens(ctx, session._id, config);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Return the current session ID.
|
|
127
|
+
*
|
|
128
|
+
* ```ts filename="convex/myFunctions.tsx"
|
|
129
|
+
* import { mutation } from "./_generated/server";
|
|
130
|
+
* import { getAuthSessionId } from "@robelest/convex-auth/component";
|
|
131
|
+
*
|
|
132
|
+
* export const doSomething = mutation({
|
|
133
|
+
* args: {/* ... *\/},
|
|
134
|
+
* handler: async (ctx, args) => {
|
|
135
|
+
* const sessionId = await getAuthSessionId(ctx);
|
|
136
|
+
* if (sessionId === null) {
|
|
137
|
+
* throw new Error("Client is not authenticated!")
|
|
138
|
+
* }
|
|
139
|
+
* const session = await ctx.db.get(sessionId);
|
|
140
|
+
* // ...
|
|
141
|
+
* },
|
|
142
|
+
* });
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @param ctx query, mutation or action `ctx`
|
|
146
|
+
* @returns the session ID or `null` if the client isn't authenticated
|
|
147
|
+
*/
|
|
148
|
+
export async function getAuthSessionId(ctx: { auth: Auth }) {
|
|
149
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
150
|
+
if (identity === null) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const [, sessionId] = identity.subject.split(TOKEN_SUB_CLAIM_DIVIDER);
|
|
154
|
+
return sessionId as GenericId<"session">;
|
|
155
|
+
}
|