NextAuthJS - Custom user model - next.js

i am using CredentialsProvider to auth users into my app. But in authorize function, even if i give the user variables coming from my API Endpoint: NextAuthJS only catches e-mail variable.
Is there a way to pass all variables inside session?
async authorize(credentials, req){
const res = await fetch('http://localhost:3000/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
headers: {"Content-Type": 'application/json'}
})
const {user} = await res.json()
console.log(user)
if(res.ok && user){
return user
}
return null
}

Try to override the jwt and session callbacks:
providers: [ ... ],
callbacks: {
async jwt({ token, user }) {
if (user) {
return {
...token,
user: user.user,
};
}
return token;
},
async session({ session, token }) {
if (token.user) {
session.user = token.user;
}
return session;
},
},

Related

NextAuth with HttpOnly cookies from external API

Hi looking for a way to implement authentication and authorization with NextAuth and HttpOnly cookies from an external api.
So when you log in on the external api, you get the user info:
{
id: '',
email: '',
role: ''
}
and you also get an Authentication and Refresh token on the Set-Cookie header (with a max-age of 300s and 1w, same expirations are used on the token).
So to my understanding, I have to do the following:
In the authorize fn:
POST login, forward the Set-Cookies using res.setHeader
read out Authentication token and get exp (expiration in seconds)
return the accesTokenExp and the user data
JWT Callback:
on signin (when user is defined), add user to the token object
check if Date.now() is past accessToken expiry, if not return token else
get new accessToken in Set-Cookie and UserModel from api and forward it using res.setHeader
return token with user and new accesTokenExp
import { NextApiRequest, NextApiResponse } from 'next';
import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { RestUserModel } from '../../../types/models';
import logger from '../../../utils/logger';
import { getData, postData } from '../../../utils/request';
import { getCookieValue, getTokenExpiryFromCookies } from '../../../utils/utils';
import jsonwebtoken from "jsonwebtoken"
type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
interface UserToken extends RestUserModel {
accessTokenExpiry: number
}
const nextAuthOptions: NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse): NextAuthOptions => {
return {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email", placeholder: "jhon#doe.com" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
logger.info(`NextAuth authorize for user ${credentials?.email}`);
try {
const response = await postData<RestUserModel>('/auth/signin', {
body: {
email: credentials?.email,
password: credentials?.password,
},
})
logger.info("Authorization was successful!");
const cookies = response.headers['set-cookie'] || [];
console.log(cookies)
logger.info("cookies: " + cookies);
res.setHeader('Set-Cookie', cookies)
const accessToken = getCookieValue(cookies.join(','), 'Authentication') || '';
logger.info('token: ' + accessToken)
logger.info('tokenSecret: ' + process.env.NEXTAUTH_SECRET)
logger.info('cookie decoding: ' + JSON.stringify(jsonwebtoken.decode(accessToken), null, 2));
const accessTokenExpiry = (getTokenExpiryFromCookies(cookies.join(',')) || 0) * 1000;
return {
...response.data,
accessTokenExpiry,
}
} catch (error: any) {
logger.error(`NextAuth authorize error: ${error.message}`);
throw new Error(error)
}
}
})
],
callbacks: {
async signIn({ user }) {
if (!(user.role === "admin" || user.role === "manager")) return false
return true
},
async jwt({ token, user }) {
logger.info(`JWT User ${JSON.stringify(user, null, 2)}`);
logger.info(`JWT Token ${JSON.stringify(token, null, 2)}`);
if (user) {
token.user = user
}
const shouldrefreshtime = Math.round(((token.user as UserToken).accessTokenExpiry - 60 * 1000) - Date.now())
logger.info(`Token accessExpiry: ${(token.user as UserToken).accessTokenExpiry} - Date.now(): ${Date.now()}`)
logger.info(`Token accessExpiry - 60ms * 1000: ${(token.user as UserToken).accessTokenExpiry - 60 * 1000}`)
logger.info(`Token should refresh: ${shouldrefreshtime}`)
if (shouldrefreshtime > 0) return token;
try {
const response = await getData<RestUserModel>('/auth/refresh', {
cookies: 'Refresh=' + req.cookies['Refresh']
})
response.headers['set-cookie'] && res.setHeader('Set-Cookie', response.headers['set-cookie'])
const accessTokenExpiry = getTokenExpiryFromCookies((response.headers['set-cookie'] || []).join(','));
logger.debug(`JWT Refresh Response Cookies: ${JSON.stringify(response.headers['set-cookie'], null, 2)}`)
delete token.error;
return {
...token,
user: {
...response.data,
accessTokenExpiry: (accessTokenExpiry || 0) * 1000
}
}
} catch (error) {
logger.error(`JWT Callback ERROR: ${error}`)
return {
...token,
error: "RefreshAccessTokenError",
}
}
},
async session({ session, user, token }) {
console.log('session', { session, user, token })
return {
...session,
user: {
...session.user,
...token.user as UserToken,
},
}
}
},
session: {
strategy: "jwt"
},
events: {
async signOut() {
res.setHeader("Set-Cookie", [
"Authentication=deleted;Max-Age=0;path=/;",
"Refresh=deleted;Max-Age=0;path=/;"
]);
},
},
secret: process.env.NEXTAUTH_SECRET,
}
}
const Auth = (req: NextApiRequest, res: NextApiResponse) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
export default Auth;
There seem to be an issue with the Refresh cookie being undefined.
But now I wonder if there isn't an easier way. I seem to be doing twice the work. Also is the use of NextAuth a good solution for this external API?

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.

Middleware Firebase authentication clarification

I'm setting up my API routes with express and mongoose. Is this a secure way to do user authentication? Is there any way that the user could somehow inject another Firebase user.uid to get the token of an admin user (I'm using Firebase for auth)?
Backend:
myRoute.route('/sample/:id').delete((req, res, next) => {
var user = req['currentUser'];
UserModel.findById(user.uid, (error, data) => {
if (error) {
return next(error)
} else {
user = data;
if (user.admin) {
SampleModel.findByIdAndRemove(req.params.id, (error, data) => {
if (error) {
return next(error)
} else {
res.status(200).json({
msg: data
})
}
})
} else {
res.status(403).send('You are not authorised!');
}
}
})
})
async function decodeIDToken(req, res, next) {
if (req.headers?.authorization?.startsWith('Bearer ')) {
const idToken = req.headers.authorization.split('Bearer ')[1];
console.log(idToken);
try {
const decodedToken = await admin.auth().verifyIdToken(idToken);
req['currentUser'] = decodedToken;
} catch (err) {
console.log(err);
}
}
next();
}
Frontend:
const user = auth.currentUser;
const token = user && (await user.getIdToken());
axios.delete(`${this.baseApiURL}/sample/${id}`, { headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
}
}).then(() => {
console.log("Done");
})
Is this a secure way to do user authentication?
Yes, just verifying the Firebase ID Token is enough.
Is there any way that the user could somehow inject another Firebase user.uid to get the token of an admin user
Creating a JWT is pretty straightforward but you'll need to know the exact signing key that Firebase uses to sign the token else verifyIdToken() will thrown an error.

How to use next-auth using ldap and prisma

i am using next-auth with ldap to authenticate user name and password. i am able to log/authenticate the user using username and password. but when i can't create user using prisma as await is not allowed inside promise.
this is my [...next-auth].js
`
const ldap = require("ldapjs");
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaClient } from "#prisma/client";
const url = `ldap://${process.env.LDAP_SERVER}`;
const prisma = new PrismaClient();
export default NextAuth({
providers: [
CredentialsProvider({
name: "LDAP",
credentials: {
username: { label: "DN", type: "text", placeholder: "" },
password: { label: "Password", type: "password" },
},
authorize: async (credentials, req) => {
// You might want to pull this call out so we're not making a new LDAP client on every login attemp
const client = ldap.createClient({
url: url,
});
return new Promise((resolve, reject) => {
client.bind(
`${credentials.username}#${process.env.LDAP_DOMAIN}`,
credentials.password,
(error) => {
if (error) {
console.log("Wrong email or password.");
reject("Wrong email or password.");
} else {
console.log("Successfully Logged In");
resolve({
username: credentials.username,
password: credentials.password,
});
}
const filter = `(sAMAccountName=${credentials.username})`;
client.search(
process.env.LDAP_BASE_DN,
{
filter,
scope: "sub",
attributes: [
"mail",
"employeeid",
"title",
"name",
"division",
"department",
"section",
],
},
(err, results) => {
if (err) {
reject(`User ${username} LDAP search error`);
}
const entries = [];
results.on("searchEntry", (entry) => {
entries.push(entry.object);
});
results.on("error", (err) => {
reject("LDAP SEARCH error");
});
results.on("end", (result) => {
if (entries.length == 0) {
reject("Something went wrong. Please try again. (AD)");
}
console.log({ entries });
const searchResult = JSON.stringify(entries[0]);
const adEmployee = JSON.parse(searchResult);
const empId = adEmployee?.employeeID;
const name = adEmployee.name;
console.log(empId);
const newUser= await prisma.user.findUnique({
where:{
oracleId:oracleId
}
})
if(!newUser){
await prisma.user.create({
data:{
oracleId:empId,
fullName:name
}
})
}
});
}
);
}
);
});
},
}),
],
pages: {
signIn: "/auth/sign-in",
},
callbacks: {
jwt: async ({ token, user }) => {
if (user) {
token.username = user.username;
token.password = user.password;
}
return token;
},
session: async ({ session, token }) => {
if (token) {
session.id = token.id;
session.username = token.username;
}
// console.log(token);
return session;
},
},
debug: process.env.NODE_ENV === "development",
secret: process.env.NEXTAUTH_SECRET,
jwt: {
secret: process.env.NEXTAUTH_SECRET,
encryption: true,
},
});
`
await is not allowed inside promise, where should i call prisma.
Thanks
For this u need to use API endpoint (as prisma is used on server side and cannot be used on client side especially when you pass db url from env also not shown on frontend), your create for example /api/register where:
import { PrismaClient } from '#prisma/client';
import dotenv from 'dotenv'
dotenv.config();
const prisma = new PrismaClient();
const Handler = async (
req,
res
) => {
await prisma.$connect()
const users = await prisma.user.findMany()
//check if user u add is already in db
//if not then
try {
savedUser = await prisma.user.create({ data: new_user });
await prisma.$disconnect()
} catch (error: any) {
await prisma.$disconnect()
// show db error
return res.status(501).json({message: error.message})
}
res.status(200).json({ message: 'User added to db ' + savedUser.name });
}
this is just a simple explanation of what you need to do to make it work, you may add some safety:
const { username, password } = req.body
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
if (!username || !password) {
return res.status(400).json({ message: 'Username and password are required' });
}
then u call api endpoint:
const response = await axios.post(
LOGIN_URL,
JSON.stringify({ username, password }),
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true
}
)
where LOGIN_URL could be /api/register

Pass more data to session in Next-auth

We are doing OTP auth in our website. So in order to get authorized, a visitor enter his phone number in input and we send him a OPT number and he again enters the sent opt, then if it matches, we send him his account credendials (token, userID) if exists or we create new and we want to save that credentails in session useing next-auth.
This is where i got so far:
export default NextAuth({
providers: [
CredentialsProvider({
credentials: {
phoneNumber: { label: 'PhoneNumber', type: 'text' },
code: { label: 'Code', type: 'text' },
type: { label: 'Type', type: 'text' },
},
async authorize(credentials, req) {
const user_needs = await requests.auth.signInEnterOtp(
credentials.phoneNumber,
credentials.code,
credentials.type
)
return user_needs.ok ? true : null
},
}),
],
callbacks: {
async session({ session, user, token }) {
return session
},
},
secret: process.env.JWT_SECRET,
})
I need to save the user_needs in session but how can i pass it throuh authorize to session?
I tried returning the user_need in authorize but it was not passed to session callback.
Eventually i figured it out like this:
async authorize(credentials, req) {
const res = fetchUserInfo(credentials.opt)
if(res.ok) return {user: res.data} // res.data contains whatever received from DB call => fetchUserInfo(credentials.opt)
return null
},
callbacks: {
async jwt({ token, user }) {
// the user present here gets the same data as received from DB call made above -> fetchUserInfo(credentials.opt)
return { ...token, ...user }
},
async session({ session, user, token }) {
// user param present in the session(function) does not recive all the data from DB call -> fetchUserInfo(credentials.opt)
return token
},
},
Edit: 2023 Feb 15
I myself understood callback cycle better now:
authorize --> jwt --> session
jwt callback(cb) accepts the user object that authorize cb returns.
By default jwt retuns the token and things return from there is then available in the token object of session cb
Example:
async authorize(credentials, req) {
return { user: { role: 'ADMIN' } }
},
async jwt({ token, user }) {
return { ...token, role: user.role }
},
async session({ session, token }) {
return { ...session, token.role }
}
But when i use Providers, i won't have authorize cb, so to get user's role i need to query db in jwt cb but this callback runs a lot and i don't know what is the better option
A.Anvarbekov, i think it works but maybe in callbacks we should also pass other session properties? something like:
async session({ session, token }) {
return {
...session,
user:{...token.user}
},

Resources