How to use next-auth using ldap and prisma - next.js

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

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.

How to pass additional parameters in next-auth social login in NextJs?

I need to pass additional parameters to signIn function using next-auth in a NextJs project.
Here is what I tried.
<button
onClick={() =>
signIn(providers.facebook.id, { userType: "customer" })
}
>
<img src="images/facebook.svg" className="w-5 h-5" />
</button>
[...nextAuth].js code
import NextAuth from "next-auth";
import dbConnect from "../../../lib/dbConnect";
import CredentialsProvider from "next-auth/providers/credentials";
import User from "../../../models/User";
import brcypt from "bcryptjs";
import GoogleProvider from "next-auth/providers/google";
import FacebookProvider from "next-auth/providers/facebook";
import InstagramProvider from "next-auth/providers/instagram";
dbConnect();
export default NextAuth({
session: {
strategy: "jwt",
},
secret: process.env.NEXTAUTH_SECRET,
//The providers are the authentication method
providers: [
CredentialsProvider({
// The name to display on the sign in form (e.g. 'Sign in with...')
name: "Credentials",
// The credentials is used to generate a suitable form on the sign in page.
// You can specify whatever fields you are expecting to be submitted.
// e.g. domain, username, password, 2FA token, etc.
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
try {
const email = credentials.email;
const password = credentials.password;
const user = await User.findOne({ email: email });
if (!user) {
return null;
}
if (user) {
let allow = await signInUser({ password, user });
if (allow == true) {
return user;
} else {
return null;
}
}
} catch (error) {
return null;
}
},
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorizationUrl:
"https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code",
}),
FacebookProvider({
clientId: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
authorization: {
params: {
userType: "customer" || "admin",
},
},
}),
InstagramProvider({
clientId: process.env.INSTAGRAM_CLIENT_ID,
clientSecret: process.env.INSTAGRAM_CLIENT_SECRET,
}),
],
pages: {
signIn: "/login",
},
database: process.env.MONGODB_URI,
callbacks: {
async jwt(token, profile) {
console.log("jwt token>>>>", token);
console.log("jwt profile>>>>", profile);
return token;
},
async session({ session, user, token }) {
if (token) {
const name = token.token.user.name;
const email = token.token.user.email;
const image = token.token.user.image;
const platform = token.token.account.provider;
handleUser(name, email, image, platform);
}
return token.token.token;
},
},
});
const signInUser = async ({ password, user }) => {
let allow = true;
if (!password) {
allow = false;
}
const isMatch = await brcypt.compare(password, user.password);
if (!isMatch) {
allow = false;
}
return allow;
};
async function handleUser(name, email, image, platform) {
console.log("Handle User>>>>>", name);
console.log("Handle email>>>>>", email);
console.log("Handle image>>>>>", image);
console.log("Handle platform>>>>>", platform);
}
Inside the callbacks function I tried logging token & profile. The additional params I passed is not being sent.
What is the right way to achieve this in Next.js?
You can find an article on the GitHub discussion section for next-auth here. The general gist of the answer is that you specify additional custom parameters in the Google Provider parameter (as below). There are other steps, but overall they appear to have solved it. Hope this helps.
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
userType: "user" || "admin", <-- THIS ONE
},
},
}),

NextAuthJS - Custom user model

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;
},
},

Firebase google authentication need to double-click to log in

I try to login into firebase with google authentication (using React hooks), and I need to click on the button twice. I want the user to get into the homepage after the first click. What is wrong here?
googleLogin.js
import firebase from "firebase/app";
import { auth,app } from "../../../../config/firebase";
export const googleLogin = async (e, information, setInformation, signup, login, history) => {
var provider = new firebase.auth.GoogleAuthProvider();
firebase
.auth()
.signInWithPopup(provider)
.then( resp => {
let {user, credential,additionalUserInfo: userInfo} = resp;
let token = credential.accessToken;
if (userInfo.isNewUser) signupWithGoogle(user, credential, userInfo);
})
.then(()=>{
history.push('/')
})
.catch((error) => {
...
});
};
const signupWithGoogle = (user, credential, userInfo)=>{
app.firestore().collection('users').doc(user.uid).set({
firstName: userInfo.profile.given_name,
lastName: userInfo.profile.family_name});
const batch = app.firestore().batch();
const initData = [
{ Applied: { positionIds: [], title: 'Applied' } },
{ Contract: { positionIds: [], title: 'Contract' } },
{ Denied: { positionIds: [], title: 'Denied' } },
{ InProgress: { positionIds: [], title: 'In Progress' } },
{ ReceivedTask: { positionIds: [], title: 'Received Task' } },
];
initData.forEach((doc) => {
const docRef = app
.firestore()
.collection('users')
.doc( user.uid)
.collection('columns')
.doc(Object.keys(doc)[0]);
batch.set(docRef, Object.values(doc)[0]);
});
return batch.commit();
}
I had the same problem to login and the problem was in my HTLM code.
I was using a form.
To resolve it, I used a simpler code in HTLM.

Resources