useSession stays in loading state after sign-in using REST API - next-auth

I am using the Credentials provider and next-auth 4.3.1
I go to page /protected
This page does useSession({ required: true, onUnauthenticated: () => router.push('/login?redirect=/protected') })
I login on the login page I got redirected too with this code:
const { data: { csrfToken } } = await axios.get('/api/auth/csrf');
const res = await axios
.post(
'/api/auth/callback/credentials',
{
json: true,
csrfToken,
redirect: false,
email: form.email,
password: form.password
},
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}
);
router.push(router.query.redirect || '/');
After succesfully logging in, notice it pushes back router.push(router.query.redirect), so it takes me back to /protected
However useSession returns { data: undefined, status: loading } and triggers onUnauthenticated, taking me back to the login page
Now, I don't login again, I just type in URL bar https://localhost:3000/protected it will load the protected page and useSession properly finds the logged-in session.
Is there something I have to do to make useSession see signIn was just called?
Here is my [...nextauth].ts:
const handler = NextAuth({
secret: process.env.NEXTAUTH_SECRET,
session: {
strategy: 'jwt'
},
debug: process.env.NODE_ENV === 'development',
providers: [
CredentialsProvider({
credentials: {
email: { label: 'Email', type: 'text' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials, req) {
////// removed
}
})
],
pages: {
signIn: '/login'
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.user = { id: user.id };
}
return token;
},
async session({ session, token }) {
if (session?.user) {
session.user.id = token.user.id;
}
return session;
}
}
});

Related

unstable_getServerSession to secure apis (nextAuth)

i need to secure the API so that only authorized user can access them. I followed the documentation in this link https://next-auth.js.org/tutorials/securing-pages-and-api-routes#securing-api-routes but apparently I am not retrieving the session.
I am able to console.log the authOptions but if I try to console log the session (and I am logged in), it logs "null"
This is the code
pages/api/profile.js
import prisma from "../../../lib/prisma";
import { unstable_getServerSession } from "next-auth/next";
import { authOptions } from "../auth/[...nextauth]";
export default async function handler(req, res) {
const session = await unstable_getServerSession(req, res, authOptions);
console.log("SESSION", session); // this logs "null"
if (!session) {
return res.status(401).json("Not authorized");
}
try {
const user = await prisma.user.findUnique({
where: { email: session.user.email },
});
return res.status(200).json(user);
} catch (error) {
console.error(error);
return res
.status(503)
.json(
"Our server is not able to process the request at the moment, please try again later!"
);
}
pages/api/auth/[...nextauth].js
import NextAuth from "next-auth";
import CognitoProvider from "next-auth/providers/cognito";
import prisma from "../../../lib/prisma";
export const authOptions = {
providers: [
CognitoProvider({
clientId: process.env.CLIENTID_NEXTAUTH,
issuer: process.env.COGNITO_ISSUER,
clientSecret: process.env.CLIENTSECRET_NEXTAUTH,
}),
],
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60,
updateAge: 24 * 60 * 60,
},
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
}
return token;
},
async session({ session, token }) {
const user = await prisma.user.findUnique({
where: { email: session?.user?.email },
});
if (!user) throw new Error("User not found in the database.");
const mySession = {
...session,
accessToken: token.accessToken,
email: user.email,
};
return mySession;
},
},
};
export default NextAuth(authOptions);
pages/dashboard/index.js
import axios from "axios";
import React, { useState } from "react";
import { getSession, useSession } from "next-auth/react";
const Dashboard = (props) => {
let { data: session, status } = useSession();
if (status === "loading") {
return <p>Loading...</p>;
}
if (status === "unauthenticated") {
window.location.reload();
}
return (
<p>
{props.userInfo.name}
</p>
);
};
export default Dashboard;
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
console.log("SESSION IN INDEX", session); // this logs the session
const userInfo = await axios.get(
`${process.env.BASE_URL}/api/profile?email=${session.email}`
);
return {
props: {
session,
userInfo: userInfo.data ? userInfo.data : null,
},
};
}
so when I login, I can see the SESSION in INDEX but when I hit the api/profile, the session from unstable_getServerSession is null, so I canno see nothing in the dashboard
resolved:
when calling the api you need to pass the headers, for example in the dashboard/index.js
const userInfo = await axios.get(
`${process.env.BASE_URL}/api/profiles/profile?email=${session.email}`,
{
withCredentials: true,
headers: {
Cookie: context.req.headers.cookie,
},
}
);
while in the API endpoint
import { getServerSession, getSession } from "next-auth/next";
import { authOptions } from "../auth/[...nextauth]";
export default async function handler(req, res) {
const session = await getServerSession(req, res, authOptions);
console.log("SESSION", session);
//your code
}

NextAuth + TwitterProvider + FirestoreAdapter => Missing Screen Name

I'm using next-auth along with next-auth/firebase-adapter packages to log to my web app via Twitter. The flow is working nicely, but I can not obtain the user's screenname (aka handle).
Before using next-auth I've been using Firebase auth(). In their package, the user's screen name has always been available.
I've been playing around with NextAuthOptions but to no avail.
Does anyone know how to make add the user's screenname to the session?
export const authOptions: NextAuthOptions = {
session: {
strategy: 'jwt',
},
adapter: FirestoreAdapter(firebaseConfig),
pages: {
signIn: '/sign-in',
},
callbacks: {
session: ({ session, token }) => {
return {
...session,
user: {
...session.user,
id: token.sub!,
},
}
},
},
events: {
createUser: async ({ user }) => {
const ref = admin
.firestore()
.collection(Constants.UsersTable)
.doc(user.id)
await ref.set(
{
screenName: <MISSING>,
daily_target: 0,
onboarded: false,
},
{ merge: true }
)
},
},
providers: [
TwitterProvider({
clientId: process.env.TWITTER_AUTH_CLIENTID,
clientSecret: process.env.TWITTER_AUTH_CLIENTSECRET,
version: '2.0',
}),
],
}
Instead of obtaining the screenname I decided to obtain the providerAccountToken and add it to my JWT token with a next-auth callback.
This is even better as I use the unique user id instead of a public handle (which the user might change in the future).
callbacks: {
jwt: ({ token, account = {} }) => {
// Persist the OAuth access_token to the token right after signin
if (account.providerAccountId) {
token.providerAccountId = account.providerAccountId
}
return token
},
},

NextAuth - AccessToken not refreshed with MongoDB and Coinbase

I have a problem with my authentication via Coinbase (using Nextauth) on NextJS app.
I made this code below, and it saves the profile well in my Mongodb database. But when I re-login, accesstoken and refreshtoken are not changed...
So I can’t use the APIs afterwards.
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
async function refreshAccessToken(token) {
try {
const url =
"https://api.coinbase.com/oauth/token?" +
new URLSearchParams({
client_id: process.env.COINBASE_CLIENT_ID,
client_secret: process.env.COINBASE_SECRET_ID,
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_in * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
}
} catch (error) {
console.log(error)
return {
...token,
error: "RefreshAccessTokenError",
}
}
}
export default NextAuth({
// Configure one or more authentication providers
providers: [
Providers.Coinbase({
clientId: process.env.COINBASE_CLIENT_ID,
clientSecret: process.env.COINBASE_SECRET_ID,
callbackUrl: process.env.COINBASE_CALLBACKURL,
scope: "wallet:accounts:read",
}),
],
callbacks: {
async jwt({ token, user, account, profile, isNewUser }) {
// Initial sign in
if (account && user) {
return {
accessToken: user.data.access_token,
accessTokenExpires: Date.now() + user.data.expires_in * 1000,
refreshToken: user.data.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.accessToken = token.accessToken
return session
}
},
events: {
async signIn(message) { console.log('success signin') },
async signOut(message) { console.log('success signout') },
async createUser(message) { console.log('success user create') },
async updateUser(message) { console.log('success update user') },
async session(message) { console.log('success session') },
async error(message) { console.log('error') }
},
// A database is optional, but required to persist accounts in a database
database: `mongodb+srv://${process.env.NOSQL_USER}:${process.env.NOSQL_PWD}#${process.env.NOSQL_HOST}/${process.env.NOSQL_TABLE}`,
});
I’m still a beginner on NextJS and React in particular:) Thanks for your help

How to get signed in users data?

I have a MERN mobile app thats using passportjs to authenticate and login users (with mongodb database and axios), however, when i eventually get to the the screen to enter in data (a "log"), i cant associate that data/log with the signed in user. How can i grab the user id several screens later after they have already signed in to associate it with the entry? My mongodb database has a number of users, so i only want a specific user's data (eg calories), ie the one that is currently logged in:
// Mongoose schemas
// log.model.js
const Schema = mongoose.Schema;
const logSchema = new Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
calories: {
type: Number,
required: true,
},
},
{
timestamps: true,
}
);
const Log = mongoose.model("Log", logSchema);
// user.model.js
const userSchema = new Schema(
{
_id: Schema.Types.ObjectId, // user id
email: {
type: String,
required: true,
unique: true,
trim: true,
},
password: {
type: String,
required: true,
trim: true,
minlength: 6,
},
},
{
timestamps: true,
}
);
const User = mongoose.model("User", userSchema);
They are first prompted to signin in the app, where they will then navigate to Home. Not all features are added in yet, just in development stage now:
// ./frontend/screens/signin.js
function onLoginPress() {
axios({
method: "POST",
data: {
email: email,
password: password,
},
withCredentials: true,
url: 'http:localhost:5000/users/signin',
})
.then((res) => console.log(res.data))
.catch((error) =>
console.log("ERROR: Promise rejected (sign in): " + error)
);
navigation.navigate("Home");
}
// ./backend/routes/users.js
router.route("/signin").post((req, res, next) => {
passport.authenticate("local", (error, user, info) => {
if (error) {
res.json({
status: "FAILED",
message: error,
});
}
if (!user) {
res.json({
status: "FAILED",
message: "No user exists",
});
} else {
req.logIn(user, (error) => {
if (error) console.log("ERROR: " + error);
res.json({
status: "SUCCESS",
message: "Successfully authenticated",
});
console.log(req.user);
});
}
})(req, res, next);
});
After they sign in, and they wish to enter in calories, i attempt to associate that log (and any future logs they might add) with the signed in user when they hit a button:
// ./frontend/screens/log.js
const [calories, setCalories] = React.useState("");
function onSaveLog() {
axios({
method: "post",
url: "http://localhost:5000/log/add",
data: {
calories: calories,
// CANT GET USER ID HERE?
},
})
.then((res) => {
console.log(res.data);
})
.catch(function () {
console.log("LOG ERROR: promise rejected");
});
}
// ./backend/routes/log.js
router.route("/add").post((req, res) => {
const calories = Number(req.body.calories);
// const user = req.body.user; // CANT GET THE USER ID HERE
const newLog = new Log({
calories,
// user,
});
// saves Log data to mongodb
newLog
.save()
.then(() => res.json("Log added"))
.catch((err) => res.status(400).json("Error: " + err));
});
so, what you doubt is, correct me if I'm wrong is that you want an ID that can be accessed somewhere later in the app to retrieve the users' data.
There are many ways to achieve that,
after you get the id, you can pass it as Navparams. check this for more info RN- params
Next you can store the id in async storage and retrieve it anywhere, I would suggest this cause is the easiest rn--async storage
import AsyncStorage from '#react-native-async-storage/async-storage';
const storeData = async (value) => {
try {
await AsyncStorage.setItem('#storage_Key', value)
} catch (e) {
// saving error
}
}
// read
const getData = async () => {
try {
const value = await AsyncStorage.getItem('#storage_Key')
if(value !== null) {
// value previously stored
}
} catch(e) {
// error reading value
}
}
you can do it this way, do tell me if you're stuck

Cannot get a response from PostgreSQL server

I freely admit that I am completely new to next-auth and the documentation for Credentials is understandably very light. I have got the email link process to work perfectly and will be moving users across top this.
Unfortunately, I have a lot of user data that will require credentials to login and, after spending a few days getting nowhere, I just want to get some idea of what I am doing wrong! This is my [...nextauth].js file:
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
import axios from 'axios'
const options = {
providers: [
Providers.Email({
server: {
host: process.env.EMAIL_SERVER_HOST,
port: process.env.EMAIL_SERVER_PORT,
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD
}
},
from: process.env.EMAIL_FROM
}),
Providers.Credentials({
credentials: {
mem_num: { label: "Membership Number", type: "text", placeholder: "12345" },
password: { label: "Password", type: "text" }
},
authorize: async (credentials) => {
console.log("credentials: ", credentials)
try {
const data = {
mem_num: credentials.mem_num,
password: credentials.password
}
const user = await login(data)
if (user) {
console.log('user:', user)
return user
}
} catch (error) {
if (error.response) {
console.log(error.response)
Promise.reject(new Error('Invalid Number and Password combination'))
}
}
}
})
],
site: process.env.NEXTAUTH_URL || "http://localhost:3000",
database: process.env.DATABASE_URL,
session: {
// Use JSON Web Tokens for session instead of database sessions.
// This option can be used with or without a database for users/accounts.
// Note: `jwt` is automatically set to `true` if no database is specified.
jwt: true,
},
}
const login = async data => {
var config = {
headers: {
'Content-Type': "application/json; charset=utf-8",
'corsOrigin': '*',
"Access-Control-Allow-Origin": "*"
}
};
const url = process.env.DATABASE_URL;
const result = await axios.post(url, data, config);
console.log('result', result);
return result;
};
export default (req, res) => NextAuth(req, res, options);