How to redirect to starting point after authorizing with auth0 in a Nextjs application using #auth0/nextjs-auth0 - next.js

I'm currently using auth0 to authenticate users in a Next.js application.
I'm using the #auth0/nextjs-auth0 SDK and following along with the documentation.
However, I'm having trouble figuring out how to redirect users dynamically after login based on the page they accessed the login form from.
In the app I’m currently trying to build, users can log in from “/” which is the home page, and from the navbar element in “/browse”. However, after logging in, it always redirects back to “/”, while I would like to redirect users to “/browse” or "/browse/[id] if that is where they began the login process from.
I’ve tried using https://community.auth0.com/t/redirecting-to-another-page-other-than-using-nextjs-auth0/66920 as a guide but this method only allows me to redirect to a pre-defined route. I would like to know how I could make the redirect URL dynamic.
Thanks in advance!
Edit: I’ve managed to find a solution for now by digging in to the req object and setting the returnTo value to “referer”.
import { handleAuth, handleLogin } from '#auth0/nextjs-auth0';
const getLoginState = (req, loginOptions) => {
return {
returnTo: req.headers.referer
};
};
export default handleAuth({
async login(req, res) {
try {
await handleLogin(req, res, { getLoginState });
} catch (err) {
res.status(err.status ?? 500).end(err.message)
}
}
});
I’m not seeing any obvious problems so far but I’m not entirely sure if this method has any drawbacks, so I would appreciate any feedback.

How about this?
Step 1: Initialize Auth0 SDK
https://auth0.github.io/nextjs-auth0/modules/instance.html#initauth0
# /lib/auth0,js
import { initAuth0 } from "#auth0/nextjs-auth0";
export default initAuth0({
secret: process.env.SESSION_COOKIE_SECRET,
issuerBaseURL: process.env.NEXT_PUBLIC_AUTH0_DOMAIN,
baseURL: process.env.NEXT_PUBLIC_BASE_URL,
clientID: process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
routes: {
callback:
process.env.NEXT_PUBLIC_REDIRECT_URI ||
"http://localhost:3000/api/auth/callback",
postLogoutRedirect:
process.env.NEXT_PUBLIC_POST_LOGOUT_REDIRECT_URI ||
"http://localhost:3000",
},
authorizationParams: {
response_type: "code",
scope: process.env.NEXT_PUBLIC_AUTH0_SCOPE,
},
session: {
absoluteDuration: process.env.SESSION_COOKIE_LIFETIME,
},
});
Step 2: Configure Login
https://auth0.github.io/nextjs-auth0/modules/handlers_login.html#handlelogin
https://auth0.github.io/nextjs-auth0/interfaces/handlers_login.loginoptions.html#returnto
# /pages/api/auth/login.js
import auth0 from "../../../lib/auth0";
export default async function login(req, res) {
let options = {
returnTo: 'http://localhost:3000/dashboard'
}
try {
await auth0.handleLogin(req, res, options);
} catch (error) {
console.error(error);
res.status(error.status || 500).end(error.message);
}
}
Now you will land on the dashboard page after successfully authenticating.
Step 3: Helpful Sanity Check
create /pages/api/auth/callback.js with the following content
import auth0 from "../../../lib/auth0";
const afterCallback = (req, res, session, state) => {
// console.log(session)
console.log(state)
return session
};
export default async function callback(req, res) {
try {
console.log(auth0)
await auth0.handleCallback(req, res, { afterCallback });
} catch (error) {
console.error(error);
res.status(error.status || 500).end(error.message);
}
}
Try logging in and look for the state in the console,
{ returnTo: 'http://localhost:3000/dashboard' }
Cheers!

Related

Unable to catch the 'auth' event in Hub.listen while calling Auth.federatedSignIn

I am using SolidJS and building a SPA (no server rendering). For authentication, I use the #aws-amplify/core and #aws-amplify/auth packages. At the application root I call the Hub.listen function:
Hub.listen('auth', ({ payload }) => console.log(payload));
In the SignUp component I call Auth.federatedSignIn:
const SignUp = () => {
return (
<button onClick={() => {
Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Google });
}}>
Sign up
</button>
);
}
I have configured the Amplify as such:
Amplify.configure({
Auth: {
region: import.meta.env.VITE_AWS_REGION,
userPoolId: import.meta.env.VITE_AWS_POOL_ID,
userPoolWebClientId: import.meta.env.VITE_AWS_POOL_CLIENT_ID,
oauth: {
domain: import.meta.env.VITE_AUTH_URL,
responseType: 'code',
redirectSignIn: location.origin + '/account/external',
redirectSignOut: location.origin + '/my',
},
},
});
When I click on the button I am redirected to the import.meta.env.VITE_AUTH_URL (simply outside of my app), choose an account, and then return back to the /account/external page. At that time I expect a consoled payload object in Web tools, but there is nothing. I get it when I call Auth.signOut(), so I assume that I configured Amplify correctly and Hub is subscribed to the auth channel.
My thoughts were that Hub cannot catch any events because after returning the application basically renders again in a new context and Hub simply isn't able to catch anything (events aren't sent from AWS?). I tried to declare the urlOpener function under the oauth property in the config and Google's sign page opened in a new tab, but even then I couldn't get any events in the preserved old page (from which I called Auth.federatedSignIn).
Questions:
How should I organize the code to get the signIn and signUp events?
Can I pass some data into the Auth.federatedSignIn to get it back in the Hub.listen, so I will be able to join the CognitoUser with the data that existed at the time of starting Sign in/Sign up (I want to add a new login type to existed user)?
Here is an example regarding the first question. Just check that your listener is set before you call the Auth.federatedSignIn() method.
export default class SignInService {
constructor(private landingFacade: LandingFacade) {
this.setupAuthListeners(); // Should be called at the top level.
}
private setupAuthListeners() {
Hub.listen('auth', ({ payload: { event, data } }) => {
switch (event) {
case 'signIn':
this.landingFacade.signInSuccess();
break;
case 'signIn_failure':
console.log('Sign in failure', data);
break;
case 'configured':
console.log('the Auth module is configured', data);
}
});
}
public async signIn(): Promise<void> {
await Auth.federatedSignIn();
}
}
For the second one: I'll use a local state and set/query the object you need.

NextJS Actioncable Proxy

So I'm trying to do two things at the same time and it's not going too well.
I have a NextJS app and a Rails API server this app connects to. For authentication I'm using a JWT token stored in an http-only encrypted cookie that the Rails API sets and the front end should not be touching. Naturally that creates a necessity for the frontend to send all the api requests though the NextJs server which proxies them to the real API.
To do that I have set up a next-http-proxy-middleware in my /pages/api/[...path] in the following way:
export const config = { api: { bodyParser: false, externalResolver: true } }
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
httpProxyMiddleware(req, res, {
target: process.env.BACKEND_URL,
pathRewrite: [{ patternStr: "^/?api", replaceStr: "" }],
})
}
Which works great and life would be just great, but turns out I need to do the same thing with ActionCable subscriptions. Not to worry, found some handy tutorials, packed #rails/actioncable into my package list and off we go.
import {useCurrentUser} from "../../../data";
import {useEffect, useState} from "react";
const UserSocket = () => {
const { user } = useCurrentUser()
const [roomSocket, setRoomSocket] = useState<any>(null)
const loadConsumer = async () => {
// #ts-ignore
const { createConsumer } = await import("#rails/actioncable")
const newCable = createConsumer('/api/wsp')
console.log('Cable loaded')
setRoomSocket(newCable.subscriptions.create({
channel: 'RoomsChannel'
},{
connected: () => { console.log('Room Connected') },
received: (data: any) => { console.log(data) },
}))
return newCable
}
useEffect(() => {
if (typeof window !== 'undefined' && user?.id) {
console.log('Cable loading')
loadConsumer().then(() => {
console.log('Cable connected')
})
}
return () => { roomSocket?.disconnect() }
}, [typeof window, user?.id])
return <></>
}
export default UserSocket
Now when I go to load the page with that component, I get the log output all the way to Cable connected however I don't see the Room Connected part.
I tried looking at the requests made and for some reason I see 2 requests made to wsp. First is directed at the Rails backend (which means the proxy worked) but it lacks the Cookie headers and thus gets disconnected like this:
{
"type": "disconnect",
"reason": "unauthorized",
"reconnect": false
}
The second request is just shown as ws://localhost:5000/api/wsp (which is my NextJS dev server) with provisional headers and it just hangs up in pending. So neither actually connect properly to the websocket. But if I just replace the /api/wsp parameter with the actual hardcoded API address (ws://localhost:3000/wsp) it all works at once (that however would not work in production since those will be different domains).
Can anyone help me here? I might be missing something dead obvious but can't figure it out.

NextJS 12.2 middleware upgrade, return 401 basic auth

I'm trying to upgrade nextjs to v12.2+, which includes the change from using _middleware files on the page level to a global middleware file (https://nextjs.org/docs/messages/middleware-upgrade-guide). I've also read this guide that says I can no longer return a body in my middleware: https://nextjs.org/docs/messages/returning-response-body-in-middleware.
The problem I have now is that I want to show a Basic Auth prompt on specific pages, which was working in the old setup, but gives me the error "Middleware is returning a response body" in the new setup. I've tried to rewrite the code to use NextResponse.rewrite but that does not give me the basic auth prompt on the user's current page.
How would I rewrite this (old setup in /admin/_middleware.js):
import { NextResponse } from "next/server";
import checkBasicAuth from "#utils/middleware/checkBasicAuth";
export function middleware(req) {
if (
checkBasicAuth(req.headers.get("authorization"), {
username: process.env.AUTH_USERNAME,
password: process.env.AUTH_PASSWORD,
})
) {
return NextResponse.next();
}
return new Response("Authentication required", {
status: 401,
headers: {
"WWW-Authenticate": 'Basic realm="Secure Area"',
},
});
}
to the new middleware setup (/src/middleware.js) so that the user does not get redirected, and gets the basic auth prompt when not logged in?
Found the answer myself, so for anyone stuck on the same problem, this is how I rewrote my old middleware:
import { NextResponse } from "next/server";
import checkBasicAuth from "#utils/middleware/checkBasicAuth";
export function middleware(request) {
if (
!checkBasicAuth(request.headers.get("authorization"), {
username: process.env.AUTH_USERNAME,
password: process.env.AUTH_PASSWORD,
})
) {
return NextResponse.rewrite(
`${request.nextUrl.protocol}//${request.nextUrl.host}/401`,
{
status: 401,
headers: {
"WWW-Authenticate": 'Basic realm="Secure Area"',
},
}
);
}
return NextResponse.next();
}
This will render but not redirect to the /401 error page, the custom header will make sure the basic auth dialog is shown to the user.

Cannot set headers after they are sent to the client with res.writeHead()

I am trying to redirect a user to the login if he isn't authenticated. I hardcoded the jwt for now. This works, but I only get an error saying Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client.
Since the function works I don't know what is wrong and couldn't really find an answer to it either. This is my code for reference:
function redirectUser(ctx, location) {
if (ctx.req) {
ctx.res.writeHead(302, { Location: location });
ctx.res.statusCode = 302;
ctx.res.setHeader(302, location);
ctx.res.end();
return { props: {} };
} else {
Router.push(location);
}
}
// getInitialProps disables automatic static optimization for pages that don't
// have getStaticProps. So article, category and home pages still get SSG.
// Hopefully we can replace this with getStaticProps once this issue is fixed:
// https://github.com/vercel/next.js/discussions/10949
MyApp.getInitialProps = async (ctx) => {
const jwt = false;
// Calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(ctx);
// Fetch global site settings from Strapi
const global = await fetchAPI("/global");
if (!jwt) {
if (ctx?.ctx.pathname === "/account") {
redirectUser(ctx.ctx, "/login");
}
}
// Pass the data to our page via props
return { ...appProps, pageProps: { global } };
};
Any help would be much appreciated.
The error "Error: Can't set headers after they are sent." means that you're already in the body, but some other function tried to set a header or statusCode. In your case it is the function ctx.res.setHeader(302, location); that's causing the issue.
After writeHead, the headers are baked in and you can only call res.write(body), and finally res.end(body).
You do not need to use setHeader when you are already using the writehead method.
Read here more about the writehead
So your redirectUser could be like :
function redirectUser(ctx, location) {
if (ctx.req) {
ctx.res.writeHead(302, { Location: location });
ctx.res.end();
return { props: {} };
} else {
Router.push(location);
}
}

Next.js API / API resolved without sending a response for /api/employees, this may result in stalled requests

I looked over the previous postings but they did not seem to match my issue. I am only using the next.js package and the integrated api pages. Mongoose is what I am using to make my schemas. I keep getting the above error only for my post calls. Any ideas what is going wrong here?
import dbConnect from "../../../utils/dbConnect";
import Employee from "../../../models/Employee";
dbConnect();
export default async (req, res) => {
const { method } = req;
switch (method) {
case "POST":
await Employee.create(req.body, function (err, data) {
if (err) {
res.status(500).json({
success: false,
data: err,
});
} else {
res.status(201).json({
success: true,
data: data,
});
}
});
break;
default:
res.status(405).json({
success: false,
});
}
};
This is a false warning because in the provided code you always return a response. It's just Next.js doesn't know it.
If you are sure that you return a response in every single case, you can disable warnings for unresolved requests.
/pages/api/your_endpoint.js
export const config = {
api: {
externalResolver: true,
},
}
Custom Config For API Routes

Resources