How to redirect using `getServerSideProps` with props in Next.js? - next.js

After a user signs in, I use router.push() to redirect the user to their profile page. I am using getServerSideProps() for authentication right now. When the redirect happens, the props don't seem to be fetched and I have to refresh the browser myself to call gSSR. Is this behavior normal or is there a way to fix it?
Demonstration - Updated
login.js
import {useRouter} from 'next/router';
export default function Login({user}) {
const router = useRouter();
// invoked on submission
async function submitLoginForm(email, password) {
const user = await signIn(email, password)
const username = await getUsernameFromDB(user);
router.push("/" + username);
}
return ( ... );
}
export async function getServerSideProps({req}) {
const user = await getUserFromCookie(req);
if(user === null) {
return {
props: {}
}
}
return {
redirect: {
destination: `/${user.username}`,
permanent: false
}
}
}
[username].js
export default function Profile({user, isUser}) {
// Use isUser to render different interface.
return ( ... );
}
export async function getServerSideProps({params, req}) {
// The username of the path.
const profileUsername = params.username
// Current user.
const user = await getUserFromCookie(req);
...
if(user !== null) {
return {
props: {
user: user,
isUser: user !== null && profileUsername === user.username
}
}
}
return {
notFound: true
}
}
The cookie is set in the _app.js using the Supabase auth sdk.
function MyApp({Component, pageProps}) {
supabase.auth.onAuthStateChange( ( event, session ) => {
fetch( "/api/auth", {
method: "POST",
headers: new Headers( { "Content-Type": "application/json" } ),
credentials: "same-origin",
body: JSON.stringify( { event, session } ),
} );
} );
return (
<Component {...pageProps} />
);
}

I would recommend that you update your _app.js like that:
import { useEffect } from 'react';
function MyApp({ Component, pageProps }) {
// make sure to run this code only once per application lifetime
useEffect(() => {
// might return an unsubscribe handler
return supabase.auth.onAuthStateChange(( event, session ) => {
fetch( "/api/auth", {
method: "POST",
headers: new Headers( { "Content-Type": "application/json" } ),
credentials: "same-origin",
body: JSON.stringify( { event, session } ),
});
});
}, []);
return <Component {...pageProps} />;
}
Also, please make clear what is happening. E.g. my current expectation:
Not authenticated user opens the "/login" page
He does some login against a backend, that sets a cookie value with user information
Then router.push("/" + username); is called
But the problem now: On page "/foo" he sees now the Not-Found page instead of the user profile
Only after page reload, you see the profile page correctly
If the above is correct, then it is possible the following line is not correctly awaiting the cookie to be persisted before the navigation happens:
const user = await signIn(email, password)
It could be that some internal promise is not correctly chained/awaited.
As an recommendation, I would log to the console the current cookie value before calling the router.push to see if the cookie was already saved.

Related

Is it possible to add more scopes to NextAuth provider during session?

I am currently using NextAuth to signIn in my application, and want to add more scopes into it while the user is already signed in so I can use the Google Fit API.
I've been reading the documentation of NextAuth and doing some research but did not find anything helpful for the current NextAuth v4 in this scope situation.
My current Google configuration:
import NextAuth from 'next-auth';
import GoogleProvider from "next-auth/providers/google"
const GOOGLE_AUTHORIZATION_URL =
'https://accounts.google.com/o/oauth2/v2/auth?' +
new URLSearchParams({
prompt: 'consent',
access_type: 'offline',
response_type: 'code'
})
export default NextAuth({
// Configure one or more authentication providers
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: GOOGLE_AUTHORIZATION_URL,
}),
],
callbacks: {
async jwt({ token, user, account }) {
// Initial sign in
if (account && user) {
return {
accessToken: account.access_token,
accessTokenExpires: Date.now() + account.expires_in * 1000,
refreshToken: account.refresh_token,
user
}
}
// Return previous token if the access token has not expired yet
if (Date.now() < token.accessTokenExpires) {
return token
}
// Access token has expired, try to update it
return refreshAccessToken(token)
},
async session({ session, token }) {
session.user = token.user;
session.accessToken = token.accessToken
session.error = token.error
return session
}
},
jwt: {
secret: process.env.NEXTAUTH_JWT_SECRET,
},
secret: process.env.NEXTAUTH_SECRET,
})
async function refreshAccessToken(token) {
try {
const url =
"https://oauth2.googleapis.com/token?" +
new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
})
const response = await fetch(url, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
})
const refreshedTokens = await response.json()
if (!response.ok) {
throw refreshedTokens
}
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_at * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
}
} catch (error) {
console.log(error)
return {
...token,
error: "RefreshAccessTokenError",
}
}
}
My current code is working just fine, so I just need the scopes to authorize and use the Google Fitness API.
Actually made it work, created a file called add_scopes.js inside pages/api/auth/
export default (req, res) => {
if (req.method === 'POST') {
// construct the authorize URL with additional scopes
const scopes = 'openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/fitness.activity.read https://www.googleapis.com/auth/fitness.location.read'
const redirectUri = process.env.GOOGLE_CALLBACK_URL
const clientId = process.env.GOOGLE_CLIENT_ID
const authorizationUrl = `https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code&scope=${scopes}&redirect_uri=${redirectUri}&client_id=${clientId}`
// send the authorization URL to the client
res.json({ authorizationUrl });
} else {
res.status(405).end(); // Method Not Allowed
}
}
then made a button to call this api route:
import { useCallback } from 'react';
import { Button } from 'react-bootstrap';
const AddScopesButton = ({scopes=scopes}) => {
const isAuthorized = scopes.includes("https://www.googleapis.com/auth/fitness.activity.read") && scopes.includes("https://www.googleapis.com/auth/fitness.location.read")
const handleClick = useCallback(async () => {
try {
const res = await fetch("/api/auth/add_scopes", { method: "POST" });
const json = await res.json()
if (res.ok) {
window.location.href = json.authorizationUrl;
} else {
throw new Error(res.statusText);
}
} catch (error) {
console.error(error);
}
}, []);
return (
<>
{!isAuthorized && (
<Button className='mt-2' onClick={handleClick}>Add Scopes</Button>
)}
{isAuthorized && <span>Authorized</span>}
</>
);
};
export default AddScopesButton;
The only problem is if you signOut and signIn back in you need to get the authorization again, would really like to know if there is a way to save the accessToken/scopes that were authorized.

next-auth.js with next.js middleware redirects to sign-in page after successful sign-in

I use next-auth.js with Google as my login provider and Django as my backend. To protect pages in next.js, I am trying to integrate next-auth.js with next.js middleware. Reference link
The issue I have is when the user is logged out, the middleware correctly routes to the login page. But after successful login, the user is redirected to the login page again. What am I missing?
middleware.js
export { default } from "next-auth/middleware"
export const config = { matcher: ["/jobs/:path*", "/accounts/:path*", "/profile/:path*", "/uploads/:path*"] }
/pages/api/auth/[...nextauth.js]
import axios from "axios";
import NextAuth from "next-auth"
import Google from "next-auth/providers/google";
import { isJwtExpired } from "../../../constants/Utils";
async function refreshAccessToken(token) {
try {
const response = await axios.post(
process.env.NEXT_PUBLIC_BACKEND_BASE + "/api/auth/token/refresh/", {
refresh: token.refreshToken
});
const { access, refresh } = response.data;
return {
...token,
accessToken: access,
refreshToken: refresh,
}
} catch (error) {
console.log(error)
return {
...token,
error: "RefreshTokenError"
}
}
}
export default NextAuth({
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: {
params: {
access_type: "offline",
response_type: "code",
scope:'openid profile email'
}
}
}),
],
callbacks: {
async jwt({ token, user, account}) {
// Initial sign in
if (account && user) {
if (account.provider === "google") {
const { access_token: accessToken } = account;
try {
// make a GET request to the DRF backend
const response = await axios.get(
process.env.NEXT_PUBLIC_BACKEND_BASE + "/api/auth/register-by-token/google-oauth2/",
{
params:
{
access_token: accessToken
}
}
);
const { access, refresh } = response.data;
token = {
...token,
accessToken: access,
refreshToken: refresh,
};
return token
} catch (error) {
console.log(error)
return {
...token,
error: "NewUserTokenError"
}
}
}
return {
...token,
error: "InvalidProviderError"
}
}
if (isJwtExpired(token.accessToken)) {
return refreshAccessToken(token)
} else {
return token
}
},
async session({ session, token }) {
session.accessToken = token.accessToken
session.refreshToken = token.refreshToken
session.error = token.error
return session
}
}
})
Upgrading next-auth.js to 4.7.0 with next.js at 12.2.0 fixed it for me.
I've run into the same problem and I was able to get it working correctly by disabled prefetching on the <Link prefetch={false} href={'/protected-route'}/> component associated with the protected pages in the application. I think that the prefetched version is cached and, upon successful singIn(), the cached version is served.
I hope it helps!
In nextjs 11.1.4 and NextAuth 4.18.8 this problem still persist
i fixed issue like this.
import { withAuth } from "next-auth/middleware"
// i used advanced middleware configuration
export default withAuth(
function middleware(req) {
// some actions here
},
{
callbacks: {
authorized: ({ token }) => {
// verify token and return a boolean
return true
},
},
}
)
export const config = { matcher: ["/jobs/:path*", "/accounts/:path*", "/profile/:path*", "/uploads/:path*"] }

Get supabase `user` server side in next.js

I am attempting to get the current logged in supabase user while server side.
I have attempted to use const user = supabase.auth.user(); but I always get a null response.
I have also attempted const user = supabase.auth.getUserByCookie(req) but it also returns null. I think because I am not sending a cookie to the api when calling it from the hook.
I have tried passing the user.id from the hook to the api but the api is not receiving the parameters.
I also attempted this approach but the token is never fetched. It seems to not exist in req.cookies.
let supabase = createClient(supabaseUrl, supabaseKey);
let token = req.cookies['sb:token'];
if (!token) {
return
}
let authRequestResult = await fetch(`${supabaseUrl}/auth/v1/user`, {
headers: {
'Authorization': `Bearer ${token}`,
'APIKey': supabaseKey
}
});
`
Does anyone know how to get the current logged in user in server side code?
If you need to get the user in server-side, you need to set the Auth Cookie in the server using the given Next.js API.
// pages/api/auth.js
import { supabase } from "../path/to/supabaseClient/definition";
export default function handler(req, res) {
if (req.method === "POST") {
supabase.auth.api.setAuthCookie(req, res);
} else {
res.setHeader("Allow", ["POST"]);
res.status(405).json({
message: `Method ${req.method} not allowed`,
});
}
}
This endpoint needs to be called every time the state of the user is changed, i.e. the events SIGNED_IN and SIGNED_OUT
You can set up a useEffect in _app.js or probably in a User Context file.
// _app.js
import "../styles/globals.css";
import { supabase } from '../path/to/supabaseClient/def'
function MyApp({ Component, pageProps }) {
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
handleAuthChange(event, session)
if (event === 'SIGNED_IN') {
// TODO: Actions to Perform on Sign In
}
if (event === 'SIGNED_OUT') {
// TODO: Actions to Perform on Logout
}
})
checkUser()
return () => {
authListener.unsubscribe()
}
}, [])
return <Component {...pageProps} />;
}
async function handleAuthChange(event, session) {
await fetch('/api/auth', {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'same-origin',
body: JSON.stringify({ event, session }),
})
}
export default MyApp;
You can now handle this user with a state and pass it to the app or whichever way you'd like to.
You can get the user in the server-side in any Next.js Page
// pages/user_route.js
import { supabase } from '../path/to/supabaseClient/def'
export default function UserPage ({ user }) {
return (
<h1>Email: {user.email}</h1>
)
}
export async function getServerSideProps({ req }) {
const { user } = await supabase.auth.api.getUserByCookie(req)
if (!user) {
return { props: {}, redirect: { destination: '/sign-in' } }
}
return { props: { user } }
}
Here's a YouTube Tutorial from Nader Dabit - https://www.youtube.com/watch?v=oXWImFqsQF4
And his GitHub Repository - https://github.com/dabit3/supabase-nextjs-auth
supabase have a library of helpers for managing auth for both client- and server-side auth and fetching in a couple of frameworks including Next.js: https://github.com/supabase/auth-helpers and appears to be the recommended solution for similar problems based on this thread: https://github.com/supabase/supabase/issues/3783
This is how I'm using it in an API handler, but provided you have access to req, you can access the user object this way:
import { supabaseServerClient } from '#supabase/auth-helpers-nextjs';
const { user } = await supabaseServerClient({ req, res }).auth.api.getUser(req.cookies["sb-access-token"]);
Note that you will need to use the helper library supabaseClient and supabaseServerClient on the client and server side respectively for this to work as intended.
I was following a tutorial today and was having a similar issue and the below is how i managed to fix it.
I've got this package installed github.com/jshttp/cookie which is why i'm calling cookie.parse.
Supabase Instance:
`//../../../utils/supabase`
import { createClient } from "#supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_KEY
);
In my case this was my API page:
import { supabase } from "../../../utils/supabase";
import cookie from "cookie";
import initStripe from "stripe";
const handler = async (req, res) => {
const { user } = await supabase.auth.api.getUserByCookie(req);
if (!user) {
return res.status(401).send("Unathorized");
}
const token = cookie.parse(req.headers.cookie)["sb-access-token"];
supabase.auth.session = () => ({
access_token: token,
});`
const {
data: { stripe_customer },
} = await supabase
.from("profile")
.select("stripe_customer")
.eq("id", user.id)
.single();
For anyone who tries to figure out how to get the user server side with the new #supabase/auth-helpers-nextjs, Michele gave the answer.
Just a note: If you're trying to get the user on nextJs's Middleware, instead of:
... req.cookies["sb-access-token"]
You have to use: req.cookies.get('sb-access-token')
For example:
import { supabaseServerClient } from '#supabase/auth-helpers-nextjs';
const { user } = await supabaseServerClient({ req, res }).auth.api.getUser(req.cookies.get('sb-access-token'))
UPDATE: 2023. Available now on Supabase Docs here
import { createServerSupabaseClient } from '#supabase/auth-helpers-nextjs'
export default function Profile({ user }) {
return <div>Hello {user.name}</div>
}
export const getServerSideProps = async (ctx) => {
// Create authenticated Supabase Client
const supabase = createServerSupabaseClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
return {
props: {
initialSession: session,
user: session.user,
},
}
}

How can I built a HOC for guarding routes if I am using httpOnly cookie?

I am storing my token in a httpOnly cookie, but when I want to built a HOC for guarding the routes there is no way to access the cookie directly inside a component, I have to do it inside the server side,
I tried to do something like this but it doesn't work:
import Cookie from "cookies";
const withAuth = (Page) => {
Page.getServerSideProps = async ({ req, res }) => {
const cookie = new Cookie(req, res);
const token = cookie.get("token");
if (!token)
return {
redirect: {
permanent: false,
destination: "/login",
},
};
return {
props: {
token,
},
};
};
return Page;
};
export default withAuth;
The getServerSideProps function only works in pages, not components.
The following snippet should help you create a HOC for authentication. This example uses the concepts of closures. I'll call this one withAdministrator.jsx.
// withAdministrator.jsx
export default (GetServerSidePropsFunction) => async (ctx) => {
// 1. Check if there is a token.
const token = ctx.req.cookies?.jwt || null;
// 2. Perform an authorized HTTP GET request to the private API to get user data.
// In here, assume that 'getAuth' is a function to perform authorized GET request using the token value in the 'Authorization' header.
const { data } = await getAuth(`${process.env.PRIVATE_API_URL}/api/v1/users/user`, token);
// 3. If there is no user, or the user is not an admin, then redirect to unauthorized.
if (!data || data.role !== 'admin') {
return {
redirect: {
destination: '/unauthorized',
permanent: false,
},
};
}
// 4. Return via closure: 'GetServerSidePropsFunction'.
return await GetServerSidePropsFunction(ctx);
};
You'll call it like this. Let's say you want to access the /admin route.
export const getServerSideProps = withAdministrator(() => {
return {
props: {},
};
});
const Admin = () => {
return (
<YourComponent />
);
};
You can do anything you want inside the returned function. For example, you might want to fetch data after authenticating the user.
Further reading: Data fetching in Next.js.

Nuxt middleware: How to access vuex store?

I am trying to block user on client-side from editing another user's profile. My URL structure is like so:
/users/edit/XpuBjKFoLSRHJAloNg38Amqn2jQ2
Thus, if user tries to acccess path of another user (ie, http://localhost:3000/users/edit/blahdasd) I need to redirect him to homepage.
I tried to set up an anonymous middle ware like so on my page:
export default {
middleware({ store, params, redirect }) {
if (store.state.user.currentUser.uid !== params.uid) {
return redirect('/')
}
},
But, I get page error of:
Cannot read property 'uid' of null
So, how do I correctly access the store here? I have no problem accessing uid from computed property on same page:
user() {
return this.$store.state.user.currentUser
},
Update (more information):
Here is my edit user profile page:
export default {
middleware({ store, params, redirect }) {
if (store.state.user.currentUser.uid !== params.uid) {
// return redirect('/')
console.log(store.state.user.currentUser.uid)
console.log(params.uid)
}
},
computed: {
user() {
return this.$store.state.user.currentUser
},
And here is my store/user.js file:
export const state = () => ({
currentUser: null,
})
export const mutations = {
SET_AUTH_USER(state, payload) {
state.currentUser = payload
}
}
export const actions = {
async onAuthStateChangedAction({ commit, dispatch }, { authUser }) {
console.log('auth state changed....')
try {
if (authUser && authUser.emailVerified) {
const {
uid,
email,
emailVerified,
displayName = '',
photoURL,
metadata,
providerData,
providerId,
tenantId
} = authUser
commit('SET_AUTH_USER', {
uid,
email,
emailVerified,
displayName,
photoURL,
metadata,
providerData,
providerId,
tenantId
})
console.log('fetching profile...')
await dispatch('getUserProfile', authUser)
} else {
console.log('User logged out or not verified')
return null
}
} catch (error) {
console.error('Error with Auth State observer: ', error)
}
},

Resources