Skip to content

Commit 72ea54b

Browse files
Fix social auth follow-up issues from PR #81
Agent-Logs-Url: https://github.com/geturbackend/urBackend/sessions/b448bc8a-7b88-4981-bd67-1b8225e6973c Co-authored-by: yash-pouranik <172860064+yash-pouranik@users.noreply.github.com>
1 parent 2b245a6 commit 72ea54b

6 files changed

Lines changed: 73 additions & 18 deletions

File tree

apps/dashboard-api/src/controllers/project.controller.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,12 @@ module.exports.updateProject = async (req, res) => {
10991099
try {
11001100
const { name, siteUrl, resendApiKey, resendFromEmail } = req.body;
11011101
const updateFields = {};
1102-
if (name !== undefined) updateFields.name = name;
1102+
if (name !== undefined) {
1103+
if (typeof name !== "string" || !name.trim()) {
1104+
return res.status(400).json({ error: "name must be a non-empty string." });
1105+
}
1106+
updateFields.name = name.trim();
1107+
}
11031108
if (resendFromEmail !== undefined) {
11041109
if (typeof resendFromEmail !== "string") {
11051110
return res.status(400).json({ error: "resendFromEmail must be a string." });
@@ -1338,11 +1343,19 @@ module.exports.toggleAuth = async (req, res) => {
13381343
const { projectId } = req.params;
13391344
const { enable } = req.body; // true or false
13401345

1341-
// Ensure user owns project
1346+
// Ensure user owns project, and load authProviders secrets so sanitizeAuthProviders
1347+
// can correctly compute hasClientSecret in the response.
13421348
const project = await Project.findOne({
13431349
_id: projectId,
13441350
owner: req.user._id,
1345-
});
1351+
}).select(
1352+
"+authProviders.github.clientSecret.encrypted " +
1353+
"+authProviders.github.clientSecret.iv " +
1354+
"+authProviders.github.clientSecret.tag " +
1355+
"+authProviders.google.clientSecret.encrypted " +
1356+
"+authProviders.google.clientSecret.iv " +
1357+
"+authProviders.google.clientSecret.tag"
1358+
);
13461359
if (!project) return res.status(404).json({ error: "Project not found" });
13471360

13481361
if (enable) {

apps/public-api/src/controllers/userAuth.controller.js

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@ const getSocialRefreshExchangeKey = (rtCode) => `project:social-auth:refresh-exc
5858
*/
5959
const getFrontendCallbackBaseUrl = (project) => {
6060
const configured = String(project?.siteUrl || '').trim();
61-
const base = configured || process.env.FRONTEND_URL || 'http://localhost:5173';
62-
return `${base.replace(/\/$/, '')}/auth/callback`;
61+
const base = configured || process.env.FRONTEND_URL || '';
62+
if (!base) {
63+
console.warn('[social-auth] getFrontendCallbackBaseUrl: siteUrl is not set on the project and FRONTEND_URL env is not configured. Falling back to http://localhost:5173. Set siteUrl in Project Settings or configure FRONTEND_URL.');
64+
}
65+
return `${(base || 'http://localhost:5173').replace(/\/$/, '')}/auth/callback`;
6366
};
6467

6568
/**
@@ -69,6 +72,43 @@ const getFrontendCallbackBaseUrl = (project) => {
6972
*/
7073
const toBase64UrlBuffer = (input) => Buffer.from(input.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(input.length / 4) * 4, '='), 'base64');
7174

75+
/**
76+
* In-memory cache for Google's public JWK keys.
77+
* Keys are refreshed when the cache expires (based on Cache-Control max-age).
78+
*/
79+
const googleJwkCache = { keys: null, expiresAt: 0 };
80+
81+
/**
82+
* Fetches Google's public JWK keys, using an in-memory cache keyed by Cache-Control max-age.
83+
* @returns {Promise<Array>} Array of JWK key objects
84+
*/
85+
const getGooglePublicKeys = async () => {
86+
const now = Date.now();
87+
if (googleJwkCache.keys && now < googleJwkCache.expiresAt) {
88+
return googleJwkCache.keys;
89+
}
90+
91+
const certsResponse = await fetch('https://www.googleapis.com/oauth2/v3/certs');
92+
if (!certsResponse.ok) {
93+
throw new Error('Unable to fetch Google JWK keys');
94+
}
95+
96+
const certsPayload = await certsResponse.json();
97+
const keys = certsPayload.keys || [];
98+
99+
// Parse Cache-Control max-age from response headers to determine TTL.
100+
const cacheControl = (typeof certsResponse.headers?.get === 'function')
101+
? (certsResponse.headers.get('cache-control') || '')
102+
: '';
103+
const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
104+
const ttlMs = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) * 1000 : 3600 * 1000;
105+
106+
googleJwkCache.keys = keys;
107+
googleJwkCache.expiresAt = now + ttlMs;
108+
109+
return keys;
110+
};
111+
72112
/**
73113
* Asserts that a project has auth enabled and a valid users collection schema.
74114
* Throws with an appropriate statusCode on failure.
@@ -315,10 +355,9 @@ const verifyGoogleIdToken = async ({ idToken, clientId }) => {
315355
throw new Error('Unsupported Google id_token signature');
316356
}
317357

318-
const certsResponse = await fetch('https://www.googleapis.com/oauth2/v3/certs');
319-
const certsPayload = await certsResponse.json();
320-
const signingKey = certsPayload.keys?.find((key) => key.kid === header.kid);
321-
if (!certsResponse.ok || !signingKey) {
358+
const certsKeys = await getGooglePublicKeys();
359+
const signingKey = certsKeys.find((key) => key.kid === header.kid);
360+
if (!signingKey) {
322361
throw new Error('Unable to verify Google id_token signing key');
323362
}
324363

apps/public-api/src/middlewares/verifyApiKey.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ const {
88
module.exports = async (req, res, next) => {
99
try {
1010
// x-api-key header is preferred. For browser-navigation endpoints (e.g. social OAuth start),
11+
// a publishable key may be supplied via the `key` query parameter instead.
1112
const headerKey = req.header('x-api-key');
13+
const queryKey = typeof req.query?.key === 'string' ? req.query.key : undefined;
1214

13-
const apiKey = headerKey;
15+
// Only allow publishable keys (pk_live_) via query param; secret keys must use the header.
16+
const apiKey = headerKey || (queryKey?.startsWith('pk_live_') ? queryKey : undefined);
1417
if (!apiKey) {
1518
return res.status(401).json({ error: 'API key not found' });
1619
}

examples/social-demo/COLLECTIONS_GUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ For GitHub social auth in this demo, these extra `users` fields are recommended:
3838
```
3939
Field Name | Type | Required | Unique | Default
4040
------------------|---------|----------|--------|----------
41-
githubId | String | No | No | -
41+
githubId | String | No | Yes | -
4242
authProviders | Array | No | No | []
4343
```
4444

examples/social-demo/client/src/lib/api.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export const authApi = {
129129
return response;
130130
},
131131
getSocialStartUrl: (provider) => (
132-
`${PROXY_URL}/userAuth/social/${encodeURIComponent(provider)}/start`
132+
`${API_BASE_URL}${API_PREFIX}/userAuth/social/${encodeURIComponent(provider)}/start?key=${encodeURIComponent(PUBLIC_KEY)}`
133133
),
134134
exchangeSocialAuth: async ({ token, rtCode }) => {
135135
const response = await axios.post(

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)