timmm
timmm
CCConvex Community
Created by timmm on 3/4/2025 in #support-community
Non OpenID Connect auth
It’s ok the dashboard. It’s the convex actions url. I think I added an env variable for it
22 replies
CCConvex Community
Created by timmm on 3/4/2025 in #support-community
Non OpenID Connect auth
If you come up with a nicer pattern than this lmk 🙏
22 replies
CCConvex Community
Created by timmm on 3/4/2025 in #support-community
Non OpenID Connect auth
cont.
const fetchAccessToken = useCallback(
async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
try {
const privyToken = await getAccessToken();

// If the user is not authenticated, remove the token
// this is to prevent the backend from running with an invalid
// privy token
if (!authenticated) {
Cookies.remove(TOKEN_COOKIE);
return null;
}

// Use .current for ref access
const cachedToken = Cookies.get(TOKEN_COOKIE);

if (
!forceRefreshToken &&
cachedToken &&
privyToken &&
(privyToken === lastPrivyTokenRef.current ||
!lastPrivyTokenRef.current)
) {
return cachedToken;
}

if (!privyToken) {
throw new Error("Failed to get Privy token");
}

lastPrivyTokenRef.current = privyToken;

const newToken = await fetchAndStoreToken(privyToken);
return newToken;
} catch (error) {
console.error("Error in fetchAccessToken:", error);
throw error;
}
},
[authenticated, getAccessToken, fetchAndStoreToken]
);

return useMemo(() => {
return {
isLoading: !ready,
isAuthenticated: authenticated,
fetchAccessToken,
};
}, [ready, authenticated, fetchAccessToken]);
}
const fetchAccessToken = useCallback(
async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
try {
const privyToken = await getAccessToken();

// If the user is not authenticated, remove the token
// this is to prevent the backend from running with an invalid
// privy token
if (!authenticated) {
Cookies.remove(TOKEN_COOKIE);
return null;
}

// Use .current for ref access
const cachedToken = Cookies.get(TOKEN_COOKIE);

if (
!forceRefreshToken &&
cachedToken &&
privyToken &&
(privyToken === lastPrivyTokenRef.current ||
!lastPrivyTokenRef.current)
) {
return cachedToken;
}

if (!privyToken) {
throw new Error("Failed to get Privy token");
}

lastPrivyTokenRef.current = privyToken;

const newToken = await fetchAndStoreToken(privyToken);
return newToken;
} catch (error) {
console.error("Error in fetchAccessToken:", error);
throw error;
}
},
[authenticated, getAccessToken, fetchAndStoreToken]
);

return useMemo(() => {
return {
isLoading: !ready,
isAuthenticated: authenticated,
fetchAccessToken,
};
}, [ready, authenticated, fetchAccessToken]);
}
22 replies
CCConvex Community
Created by timmm on 3/4/2025 in #support-community
Non OpenID Connect auth
then in react
const TOKEN_COOKIE = "my_app_token";

function usePrivyAuthProvider() {
const { authenticated, getAccessToken, ready } = usePrivy();

const { reauthorize } = useOAuthTokens({
onOAuthTokenGrant: async ({ oAuthTokens, user }) => {
const token = await getAccessToken();
if (oAuthTokens.provider === "twitter") {
await fetch(
`${import.meta.env.VITE_CONVEX_HTTP_API_URL}/set-twitter-tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
twitterOAuthToken: oAuthTokens.accessToken,
twitterRefreshToken: oAuthTokens.refreshToken,
}),
}
);
}
},
});

// Convert these to refs since they're just used for internal tracking
const isRefreshingRef = React.useRef(false);
const lastPrivyTokenRef = React.useRef<string | null>(null);

const fetchAndStoreToken = useCallback(async (privyToken: string) => {
try {
const response = await fetch(
`${import.meta.env.VITE_CONVEX_HTTP_API_URL}/token-conversion`,
{
method: "POST",
headers: {
Authorization: `Bearer ${privyToken}`,
},
}
);
const data = (await response.json()) as { token: string };

Cookies.set(TOKEN_COOKIE, data.token, {
expires: new Date(Date.now() + 55 * 60 * 1000),
secure: true,
sameSite: "strict",
});

return data.token;
} catch (error) {
console.error("Error fetching token:", error);
Cookies.remove(TOKEN_COOKIE);
throw error;
}
}, []);
const TOKEN_COOKIE = "my_app_token";

function usePrivyAuthProvider() {
const { authenticated, getAccessToken, ready } = usePrivy();

const { reauthorize } = useOAuthTokens({
onOAuthTokenGrant: async ({ oAuthTokens, user }) => {
const token = await getAccessToken();
if (oAuthTokens.provider === "twitter") {
await fetch(
`${import.meta.env.VITE_CONVEX_HTTP_API_URL}/set-twitter-tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
twitterOAuthToken: oAuthTokens.accessToken,
twitterRefreshToken: oAuthTokens.refreshToken,
}),
}
);
}
},
});

// Convert these to refs since they're just used for internal tracking
const isRefreshingRef = React.useRef(false);
const lastPrivyTokenRef = React.useRef<string | null>(null);

const fetchAndStoreToken = useCallback(async (privyToken: string) => {
try {
const response = await fetch(
`${import.meta.env.VITE_CONVEX_HTTP_API_URL}/token-conversion`,
{
method: "POST",
headers: {
Authorization: `Bearer ${privyToken}`,
},
}
);
const data = (await response.json()) as { token: string };

Cookies.set(TOKEN_COOKIE, data.token, {
expires: new Date(Date.now() + 55 * 60 * 1000),
secure: true,
sameSite: "strict",
});

return data.token;
} catch (error) {
console.error("Error fetching token:", error);
Cookies.remove(TOKEN_COOKIE);
throw error;
}
}, []);
22 replies
CCConvex Community
Created by timmm on 3/4/2025 in #support-community
Non OpenID Connect auth
auth.node.ts
export const signToken = internalAction({
args: {
payload: v.any(),
},
returns: v.string(),
handler: async (ctx, args) => {
// Convert JWK to private key
const jwk = JSON.parse(process.env.CONVEX_PRIVATE_JWK!);
const privateKey = createPrivateKey({ key: jwk, format: "jwk" });

// Create the JWT header and payload
const header = {
alg: "RS256",
typ: "JWT",
kid: "key-1",
};
const now = Math.floor(Date.now() / 1000);
const finalPayload = {
...args.payload,
iat: now,
exp: now + 3600, // 1 hour
};

// Create JWT parts
const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
"base64url"
);
const encodedPayload = Buffer.from(JSON.stringify(finalPayload)).toString(
"base64url"
);
const signingInput = `${encodedHeader}.${encodedPayload}`;

// Sign the token
const signer = createSign("RSA-SHA256");
signer.update(signingInput);
const signature = signer.sign(privateKey, "base64url");

// Combine all parts
return `${signingInput}.${signature}`;
},
});

export const getJwks = internalAction({
args: {},
returns: v.object({
keys: v.array(v.any()),
}),
handler: async (ctx) => {
const publicJwk = JSON.parse(process.env.CONVEX_PUBLIC_JWK!);
return { keys: [publicJwk] };
},
});
export const signToken = internalAction({
args: {
payload: v.any(),
},
returns: v.string(),
handler: async (ctx, args) => {
// Convert JWK to private key
const jwk = JSON.parse(process.env.CONVEX_PRIVATE_JWK!);
const privateKey = createPrivateKey({ key: jwk, format: "jwk" });

// Create the JWT header and payload
const header = {
alg: "RS256",
typ: "JWT",
kid: "key-1",
};
const now = Math.floor(Date.now() / 1000);
const finalPayload = {
...args.payload,
iat: now,
exp: now + 3600, // 1 hour
};

// Create JWT parts
const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
"base64url"
);
const encodedPayload = Buffer.from(JSON.stringify(finalPayload)).toString(
"base64url"
);
const signingInput = `${encodedHeader}.${encodedPayload}`;

// Sign the token
const signer = createSign("RSA-SHA256");
signer.update(signingInput);
const signature = signer.sign(privateKey, "base64url");

// Combine all parts
return `${signingInput}.${signature}`;
},
});

export const getJwks = internalAction({
args: {},
returns: v.object({
keys: v.array(v.any()),
}),
handler: async (ctx) => {
const publicJwk = JSON.parse(process.env.CONVEX_PUBLIC_JWK!);
return { keys: [publicJwk] };
},
});
22 replies
CCConvex Community
Created by timmm on 3/4/2025 in #support-community
Non OpenID Connect auth
cont.
// Use internal action directly instead of HTTP request
const appToken = await ctx.runAction(internal.authNode.signToken, {
payload: {
iss: CONVEX_HTTP_API_URL,
sub: verifiedClaims.userId,
aud: PRIVY_APP_ID,
exp: adjustedExpiry,
},
});

await ctx.runMutation(internal.auth.storeSession, {
userId,
privyTokenIdentifier: verifiedClaims.userId,
privyToken,
appToken,
expiresAt: Date.now() + 60 * 60 * 1000,
});

return new Response(JSON.stringify({ token: cashYapsToken }), {
status: 200,
headers: addCorsHeaders({ "Content-Type": "application/json" }),
});
} catch (error) {
console.error("Token conversion error:", error);
return new Response(JSON.stringify({ error: "Failed to convert token" }), {
status: 401,
headers: addCorsHeaders({ "Content-Type": "application/json" }),
});
}
});
// Use internal action directly instead of HTTP request
const appToken = await ctx.runAction(internal.authNode.signToken, {
payload: {
iss: CONVEX_HTTP_API_URL,
sub: verifiedClaims.userId,
aud: PRIVY_APP_ID,
exp: adjustedExpiry,
},
});

await ctx.runMutation(internal.auth.storeSession, {
userId,
privyTokenIdentifier: verifiedClaims.userId,
privyToken,
appToken,
expiresAt: Date.now() + 60 * 60 * 1000,
});

return new Response(JSON.stringify({ token: cashYapsToken }), {
status: 200,
headers: addCorsHeaders({ "Content-Type": "application/json" }),
});
} catch (error) {
console.error("Token conversion error:", error);
return new Response(JSON.stringify({ error: "Failed to convert token" }), {
status: 401,
headers: addCorsHeaders({ "Content-Type": "application/json" }),
});
}
});
22 replies
CCConvex Community
Created by timmm on 3/4/2025 in #support-community
Non OpenID Connect auth
cont.
export const tokenConversionHttp = httpAction(async (ctx, request) => {
const ipAddress = getClientIp(request);
await rateLimiter.limit(ctx, "tokenConversion", { key: ipAddress });

const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
throw new ConvexError("Missing or invalid Authorization header");
}
const privyToken = authHeader.slice(7);

try {
const verifiedClaims = await privy.verifyAuthToken(privyToken);
const privyTokenExpiry = verifiedClaims.expiration;
const adjustedExpiry = privyTokenExpiry - EXPIRY_BUFFER_SECONDS;

let userId =
(await ctx.runQuery(internal.auth.getUser, {
privyUserId: verifiedClaims.userId,
})) ??
(await ctx.runMutation(internal.auth.createUser, {
privyUserId: verifiedClaims.userId,
}));

const existingSession = await ctx.runQuery(
internal.auth.getSessionByPrivyToken,
{ privyToken }
);

if (existingSession && existingSession.expiresAt > Date.now()) {
return new Response(
JSON.stringify({ token: existingSession.cashYapsToken }),
{
status: 200,
headers: addCorsHeaders({ "Content-Type": "application/json" }),
}
);
} else if (existingSession) {
await ctx.runMutation(internal.auth.deleteSession, {
sessionId: existingSession._id,
});
}
export const tokenConversionHttp = httpAction(async (ctx, request) => {
const ipAddress = getClientIp(request);
await rateLimiter.limit(ctx, "tokenConversion", { key: ipAddress });

const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
throw new ConvexError("Missing or invalid Authorization header");
}
const privyToken = authHeader.slice(7);

try {
const verifiedClaims = await privy.verifyAuthToken(privyToken);
const privyTokenExpiry = verifiedClaims.expiration;
const adjustedExpiry = privyTokenExpiry - EXPIRY_BUFFER_SECONDS;

let userId =
(await ctx.runQuery(internal.auth.getUser, {
privyUserId: verifiedClaims.userId,
})) ??
(await ctx.runMutation(internal.auth.createUser, {
privyUserId: verifiedClaims.userId,
}));

const existingSession = await ctx.runQuery(
internal.auth.getSessionByPrivyToken,
{ privyToken }
);

if (existingSession && existingSession.expiresAt > Date.now()) {
return new Response(
JSON.stringify({ token: existingSession.cashYapsToken }),
{
status: 200,
headers: addCorsHeaders({ "Content-Type": "application/json" }),
}
);
} else if (existingSession) {
await ctx.runMutation(internal.auth.deleteSession, {
sessionId: existingSession._id,
});
}
22 replies
CCConvex Community
Created by timmm on 3/4/2025 in #support-community
Non OpenID Connect auth
auth.ts
const addCorsHeaders = (headers: Record<string, string> = {}) => ({
...headers,
"Access-Control-Allow-Origin":
process.env.SITE_URL ?? "http://localhost:3000",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
Vary: "Origin",
});

export const tokenConversionPreflight = httpAction(async (ctx, request) => {
return new Response(null, {
status: 204,
headers: {
...addCorsHeaders(),
"Access-Control-Max-Age": "86400", // 24 hours
},
});
});

export const jwks = httpAction(async (ctx, request) => {
const jwks = await ctx.runAction(internal.authNode.getJwks);
return new Response(JSON.stringify(jwks), {
status: 200,
headers: addCorsHeaders({
"Content-Type": "application/jwk-set+json",
"Cache-Control": "no-store",
Pragma: "no-cache",
}),
});
});

export const wellKnown = httpAction(async (ctx, request) => {
return new Response(
JSON.stringify({
issuer: CONVEX_HTTP_API_URL,
jwks_uri: `${CONVEX_HTTP_API_URL}/jwks`,
id_token_signing_alg_values_supported: ["RS256"],
subject_types_supported: ["public"],
}),
{
status: 200,
headers: addCorsHeaders({ "Content-Type": "application/json" }),
}
);
});
const addCorsHeaders = (headers: Record<string, string> = {}) => ({
...headers,
"Access-Control-Allow-Origin":
process.env.SITE_URL ?? "http://localhost:3000",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
Vary: "Origin",
});

export const tokenConversionPreflight = httpAction(async (ctx, request) => {
return new Response(null, {
status: 204,
headers: {
...addCorsHeaders(),
"Access-Control-Max-Age": "86400", // 24 hours
},
});
});

export const jwks = httpAction(async (ctx, request) => {
const jwks = await ctx.runAction(internal.authNode.getJwks);
return new Response(JSON.stringify(jwks), {
status: 200,
headers: addCorsHeaders({
"Content-Type": "application/jwk-set+json",
"Cache-Control": "no-store",
Pragma: "no-cache",
}),
});
});

export const wellKnown = httpAction(async (ctx, request) => {
return new Response(
JSON.stringify({
issuer: CONVEX_HTTP_API_URL,
jwks_uri: `${CONVEX_HTTP_API_URL}/jwks`,
id_token_signing_alg_values_supported: ["RS256"],
subject_types_supported: ["public"],
}),
{
status: 200,
headers: addCorsHeaders({ "Content-Type": "application/json" }),
}
);
});
22 replies
CCConvex Community
Created by timmm on 3/4/2025 in #support-community
Non OpenID Connect auth
@albert chen here's how I built it auth.config.ts
export default {
providers: [
{
domain: process.env.CONVEX_HTTP_API_URL,
applicationID: process.env.PRIVY_APP_ID!,
},
],
};
export default {
providers: [
{
domain: process.env.CONVEX_HTTP_API_URL,
applicationID: process.env.PRIVY_APP_ID!,
},
],
};
22 replies
CCConvex Community
Created by timmm on 3/4/2025 in #support-community
Non OpenID Connect auth
Give me an hour and I’ll find the code and share it here
22 replies
CCConvex Community
Created by timmm on 3/4/2025 in #support-community
Non OpenID Connect auth
http actions
22 replies
CCConvex Community
Created by MegaHydronics on 12/17/2024 in #support-community
Accessing client IP address in a query/mutation
@ballingt I would definitely use this too. To rate limit on IP address I have to use a http endpoint. I think for me I'd probably want the raw details from the http request (presumably) that happens when initating the web socket
9 replies
CCConvex Community
Created by timmm on 3/4/2025 in #support-community
Non OpenID Connect auth
I ended up getting this working by creating my own openid connect compaitible api using convex http (well known, jwks endpoint etc) and then using a token conversion endpoint to create a session from the privy token and return a token for my application to use for ongoing auth
22 replies
CCConvex Community
Created by timmm on 2/21/2025 in #general
Using Twitter with Convex Auth
Oh I will change to this. Didn't realise you could do that
78 replies
CCConvex Community
Created by timmm on 2/21/2025 in #general
Using Twitter with Convex Auth
Thanks again Tom 🙂 For those coming back here later the full solution was: 1. setup convex auth as per instructions https://labs.convex.dev/auth/setup/manual 2. add the twitter provider to convex/auth.ts
import { convexAuth } from "@convex-dev/auth/server";
import Twitter from "@auth/core/providers/twitter";
import invariant from "tiny-invariant";

const xClientId = process.env.X_CLIENT_ID;
const xClientSecret = process.env.X_CLIENT_SECRET;

invariant(xClientId, "X_CLIENT_ID is not set");
invariant(xClientSecret, "X_CLIENT_SECRET is not set");

export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [
Twitter({
clientId: xClientId,
clientSecret: xClientSecret,
}),
],
});
import { convexAuth } from "@convex-dev/auth/server";
import Twitter from "@auth/core/providers/twitter";
import invariant from "tiny-invariant";

const xClientId = process.env.X_CLIENT_ID;
const xClientSecret = process.env.X_CLIENT_SECRET;

invariant(xClientId, "X_CLIENT_ID is not set");
invariant(xClientSecret, "X_CLIENT_SECRET is not set");

export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [
Twitter({
clientId: xClientId,
clientSecret: xClientSecret,
}),
],
});
3. add a custom schema for users - note that the email needs to be optionally null email: v.optional(v.union(v.string(), v.null()))
users: defineTable({
name: v.optional(v.string()),
image: v.optional(v.string()),
email: v.optional(v.union(v.string(), v.null())),
emailVerificationTime: v.optional(v.number()),
phone: v.optional(v.string()),
phoneVerificationTime: v.optional(v.number()),
isAnonymous: v.optional(v.boolean()),
}).index("email", ["email"]),
users: defineTable({
name: v.optional(v.string()),
image: v.optional(v.string()),
email: v.optional(v.union(v.string(), v.null())),
emailVerificationTime: v.optional(v.number()),
phone: v.optional(v.string()),
phoneVerificationTime: v.optional(v.number()),
isAnonymous: v.optional(v.boolean()),
}).index("email", ["email"]),
4. add the callback URL on twitter to your convex HTTP endpoint. I'm using local dev atm http://127.0.0.1:3211/api/auth/callback/twitter
78 replies
CCConvex Community
Created by timmm on 2/21/2025 in #general
Using Twitter with Convex Auth
This is great. Very much appreciated!
78 replies
CCConvex Community
Created by timmm on 2/21/2025 in #general
Using Twitter with Convex Auth
You move so quick - thanks heaps Tom
78 replies
CCConvex Community
Created by timmm on 2/21/2025 in #general
Using Twitter with Convex Auth
Hey it's working!
78 replies