Replies: 1 comment
-
I've managed to work around this by:
However this is still incredibly fragile as any changes to next-auth core is likely to break it. Any suggestions welcome. This is the functional patch file: diff --git a/node_modules/@auth/core/lib/actions/callback/index.js b/node_modules/@auth/core/lib/actions/callback/index.js
index edcb7f9..52f096a 100644
--- a/node_modules/@auth/core/lib/actions/callback/index.js
+++ b/node_modules/@auth/core/lib/actions/callback/index.js
@@ -23,9 +23,9 @@ export async function callback(request, options, sessionStore, cookies) {
// and see if it contains a valid origin to redirect to. If it does, we
// redirect the user to that origin with the original state.
if (options.isOnRedirectProxy && params?.state) {
- // NOTE: We rely on the state being encrypted using a shared secret
- // between the proxy and the original server.
- const parsedState = await state.decode(params.state, options);
+ // NOTE: State in query is NOT encrypted
+ // but the original server will verify using encrypted cookie.
+ const parsedState = state.decodeRaw(params.state, options);
const shouldRedirect = parsedState?.origin &&
new URL(parsedState.origin).origin !== options.url.origin;
if (shouldRedirect) {
diff --git a/node_modules/@auth/core/lib/actions/callback/oauth/callback.js b/node_modules/@auth/core/lib/actions/callback/oauth/callback.js
index f781597..9ca3ffd 100644
--- a/node_modules/@auth/core/lib/actions/callback/oauth/callback.js
+++ b/node_modules/@auth/core/lib/actions/callback/oauth/callback.js
@@ -42,7 +42,12 @@ export async function handleOAuth(params, cookies, options) {
};
const resCookies = [];
const state = await checks.state.use(cookies, resCookies, options);
- const codeGrantParams = o.validateAuthResponse(as, client, new URLSearchParams(params), provider.checks.includes("state") ? state : o.skipStateCheck);
+ // I don't know why these parameters suddenly appear on a decoded payload, next-auth bug?
+ const { iat, exp, jti,...decodedPayload } = await checks.state.decode(state, options)
+ // re-b64encode decoded state, as the check is comparing the unencrypted state with
+ // what we pulled from the cookie. This makes them the same.
+ const encodedState = Buffer.from(JSON.stringify(decodedPayload)).toString('base64')
+ const codeGrantParams = o.validateAuthResponse(as, client, new URLSearchParams(params), provider.checks.includes("state") ? encodedState : o.skipStateCheck);
/** https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1 */
if (o.isOAuth2Error(codeGrantParams)) {
const cause = { providerId: provider.id, ...codeGrantParams };
@@ -54,6 +59,14 @@ export async function handleOAuth(params, cookies, options) {
if (!options.isOnRedirectProxy && provider.redirectProxyUrl) {
redirect_uri = provider.redirectProxyUrl;
}
+
+ // The redirect URL in the token request MUST be the same as the one in the authorize request
+ // redirect_uri is the stable deployment (i.e main/PRODUCTION_URL) which the request will end up
+ // But is not the one as the user was sent to - which is the COGNITO_REDIRECT_URI
+ if (process.env.NODE_ENV === 'production') {
+ redirect_uri = process.env.COGNITO_REDIRECT_URI
+ }
+
let codeGrantResponse = await o.authorizationCodeGrantRequest(as, client, codeGrantParams, redirect_uri, codeVerifier ?? "auth", // TODO: review fallback code verifier,
{
[o.customFetch]: (...args) => {
diff --git a/node_modules/@auth/core/lib/actions/callback/oauth/checks.js b/node_modules/@auth/core/lib/actions/callback/oauth/checks.js
index 2300fc2..f45869b 100644
--- a/node_modules/@auth/core/lib/actions/callback/oauth/checks.js
+++ b/node_modules/@auth/core/lib/actions/callback/oauth/checks.js
@@ -94,19 +94,20 @@ const encodedStateSalt = "encodedState";
*/
export const state = {
/** Creates a state cookie with an optionally encoded body. */
- async create(options, origin) {
+ async create(options, data) {
const { provider } = options;
if (!provider.checks.includes("state")) {
- if (origin) {
+ if (data) {
throw new InvalidCheck("State data was provided but the provider is not configured to use state");
}
return;
}
// IDEA: Allow the user to pass data to be stored in the state
const payload = {
- origin,
+ ...data,
random: o.generateRandomState(),
};
+ const encodedState = Buffer.from(JSON.stringify(payload)).toString('base64')
const value = await encode({
secret: options.jwt.secret,
token: payload,
@@ -114,7 +115,7 @@ export const state = {
maxAge: STATE_MAX_AGE,
});
const cookie = await sealCookie("state", value, options);
- return { cookie, value };
+ return { cookie, value: encodedState };
},
/**
* Returns state if the provider is configured to use state,
@@ -139,6 +140,19 @@ export const state = {
throw new InvalidCheck("State could not be decoded", { cause: error });
}
},
+ /** Decodes the base64 encoded state. If it could not be decoded, it throws an error. */
+ decodeRaw(state, options) {
+ try {
+ options.logger.debug("DECODE_RAW_STATE", { state });
+ const payload = JSON.parse(Buffer.from(state, 'base64').toString())
+ if (payload)
+ return payload;
+ throw new Error("Invalid raw state");
+ }
+ catch (error) {
+ throw new InvalidCheck("Raw state could not be decoded", { cause: error });
+ }
+ },
};
export const nonce = {
async create(options) {
diff --git a/node_modules/@auth/core/lib/actions/signin/authorization-url.js b/node_modules/@auth/core/lib/actions/signin/authorization-url.js
index a03c6ae..a8b527c 100644
--- a/node_modules/@auth/core/lib/actions/signin/authorization-url.js
+++ b/node_modules/@auth/core/lib/actions/signin/authorization-url.js
@@ -23,12 +23,25 @@ export async function getAuthorizationUrl(query, options) {
}
const authParams = url.searchParams;
let redirect_uri = provider.callbackUrl;
- let data;
+ let data = {};
if (!options.isOnRedirectProxy && provider.redirectProxyUrl) {
redirect_uri = provider.redirectProxyUrl;
data = provider.callbackUrl;
logger.debug("using redirect proxy", { redirect_uri, data });
}
+
+ // Production login requests are double proxied (potentially)
+ // If on prod URL (i.e. req.host === PRODUCTION_URL), then only the COGNITO_REDIRECT_URI occurs
+ // Otherwise, it goes via COGNITO_REDIRECT_URI and then PRODUCTION_URL
+ // This means only 2 URLs are required to be validated against (i.e. in DDB)
+ // prod.example.com -> will be in prod ddb
+ // staging.example.com -> will be in nonprod ddb
+ if (process.env.NODE_ENV === 'production') {
+ data.redirect_uri = redirect_uri
+ // Obviously, this will differ between prod/nonprod
+ redirect_uri = process.env.COGNITO_REDIRECT_URI
+ }
+
const params = Object.assign({
response_type: "code",
// clientId can technically be undefined, should we check this in assert.ts or rely on the Authorization Server to do it?
@@ -39,6 +52,9 @@ export async function getAuthorizationUrl(query, options) {
}, Object.fromEntries(provider.authorization?.url.searchParams ?? []), query);
for (const k in params)
authParams.set(k, params[k]);
+
+ authParams.set('redirect_uri', redirect_uri)
+
const cookies = [];
const state = await checks.state.create(options, data);
if (state) {
diff --git a/node_modules/@auth/core/lib/utils/cookie.js b/node_modules/@auth/core/lib/utils/cookie.js
index cbbc1a2..daf82c2 100644
--- a/node_modules/@auth/core/lib/utils/cookie.js
+++ b/node_modules/@auth/core/lib/utils/cookie.js
@@ -183,12 +183,12 @@ _SessionStore_chunks = new WeakMap(), _SessionStore_option = new WeakMap(), _Ses
cookies.push({ ...cookie, name, value });
__classPrivateFieldGet(this, _SessionStore_chunks, "f")[name] = value;
}
- __classPrivateFieldGet(this, _SessionStore_logger, "f").debug("CHUNKING_SESSION_COOKIE", {
- message: `Session cookie exceeds allowed ${ALLOWED_COOKIE_SIZE} bytes.`,
- emptyCookieSize: ESTIMATED_EMPTY_COOKIE_SIZE,
- valueSize: cookie.value.length,
- chunks: cookies.map((c) => c.value.length + ESTIMATED_EMPTY_COOKIE_SIZE),
- });
+ // __classPrivateFieldGet(this, _SessionStore_logger, "f").debug("CHUNKING_SESSION_COOKIE", {
+ // message: `Session cookie exceeds allowed ${ALLOWED_COOKIE_SIZE} bytes.`,
+ // emptyCookieSize: ESTIMATED_EMPTY_COOKIE_SIZE,
+ // valueSize: cookie.value.length,
+ // chunks: cookies.map((c) => c.value.length + ESTIMATED_EMPTY_COOKIE_SIZE),
+ // });
return cookies;
}, _SessionStore_clean = function _SessionStore_clean() {
const cleanedChunks = {};
|
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Hello,
This is serving as a "please help me" and also self-rubberduck.
We use next-auth v5.0.0-beta.20. I had implemented some very hacky patches to allow for the behaviour we needed which was:
We accomplished this by doing the following:
PRODUCTION_URL
(poor naming i think from me) environment variable. This would feed into nextauth asredirectProxyUrl: process.env.PRODUCTION_URL + '/api/auth'
getAuthorizationUrl
to add to the state parameter the original redirect_uri, and replace the redirect_uri with COGNITO_REDIRECT_URIThis would mean the flow for a prod deployment would be :
And preview deployments
The lambda would do this:
The patch is given here for clarity
Now since updating to beta.22, this PR https://github.com/nextauthjs/next-auth/pull/11517/files was merged.
This has significantly changed how the state parameter is used.
Previously it was just base64 encoded data - and now it is an encrypted JWT. This means the lambda cannot be used, as it cannot decode the key without also having knowledge of every single secret used to encrypt them.
I suppose my questions here are:
Beta Was this translation helpful? Give feedback.
All reactions