I am currently using the credentials provider to verify a user on an LDAP. I am also using Hasura as a backend which requires certain claims in the jwt. So a custom encode and decode function is required. This is where i run into my issues. If i used the default encoding for Next-Auth everything works and I am able to log into my app. When i create my own encode and decode function i cannot get past the login screen. I am also using the NextJs middleware to ensure a valid session is present.
Here is my[...nextauth].ts file
export default NextAuth({
providers: [
CredentialsProvider({
id: 'credentials',
name: 'credentials',
credentials: {
username: { label: 'Username', type: 'text', placeholder: '' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials, req) {
console.log(
`Authorize Function called with creds - ${JSON.stringify(
credentials
)}`
)
//Developement Authentication
if (process.env.RUNNING_ENV == 'DEVELOPMENT')
return {
id: process.env.DEV_USER as string,
group: process.env.DEV_GROUP as string,
}
// const { username, password } = credentials
const username = credentials?.username
const password = credentials?.password
if (!username || !password) {
throw new Error('Enter Username and Password')
}
try {
const response = await authenticate(username, password)
//console.log(`Response: ${JSON.stringify(response)}`)
return { username, group: 'cyberlab' }
} catch (err) {
//console.log(`Error authenticating: ${err}`)
}
return null
},
}),
],
secret: process.env.NEXTAUTH_SECRET,
pages: {
signIn: '/login',
},
//Specifies JSON web tokens will be used.
jwt: {
secret: process.env.JWT_SECRET,
async encode({ token, secret, maxAge }) {
console.log(`Before Encode function token: ${JSON.stringify(token)}`)
const jwtClaims = {
sub: token.username,
name: token.username,
iat: Date.now() / 1000,
exp: Math.floor(Date.now() / 1000) + 3600,
hasura: {
'x-hasura-allowed-roles': [
'CRU',
'Forensic',
'EvSpecialist',
'Preview',
'dany',
],
'x-hasura-default-role': 'dany',
'x-hasura-user-id': token.username,
},
}
const encodedToken = jwt.sign(jwtClaims, secret, {
algorithm: 'HS512',
})
console.log(`Encoded Token: ${JSON.stringify(encodedToken)}`)
return encodedToken
},
async decode({ token, secret }) {
const decodedToken = jwt.verify(token, secret, {
algorithms: ['HS512'],
})
//console.log(`Decoded Token: ${JSON.stringify(decodedToken)}`)
return decodedToken
},
},
callbacks: {
async jwt({ token, user }) {
console.log(`Callback token: ${JSON.stringify(token)}`)
console.log(`Callback user: ${JSON.stringify(user)}`)
// * Hasura required claims will be added here
// TODO check to see if hasura claims exist, if not assign them else just pass the token.
if (user) {
console.log(`Adding User in callback`)
token.username = user.id
token.group = user.group
// token.hasura = {
// 'x-hasura-allowed-roles': [
// 'cru',
// 'forensic',
// 'evSpecialist',
// 'preview',
// 'dany',
// ],
// 'x-hasura-default-role': 'dany',
// 'x-hasura-user-id': token.username,
// }
}
return token
},
async session({ session, token, user }) {
// session.type = token.type
session.name = token.username
session.user = user
session.group = token.group
//console.log(`Session in Callback: ${JSON.stringify(session)}`)
//console.log(`Session type in callback: ${session.type}`)
//console.log(`Session User in callback: ${session.name}`)
return session
},
},
session: {
//Sets the session to use JSON Web Token
strategy: 'jwt',
//Sets the max idle time before token expires in seconds - Currently 1hr
maxAge: 3600,
},
})
Here is my terminal output
Callback token: {"sub":"JDoe"}
Callback user: {"id":"JDoe","group":"DEVELOPMENT"}
Adding User in callback
Before Encode function token: {"sub":"JDoe","username":"JDoe","group":"DEVELOPMENT"}
Encoded Token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJKRG9lIiwibmFtZSI6IkpEb2UiLCJpYXQiOjE2NDYzMTg2NTMuODA4LCJleHAiOjE2NDYzMjIyNTMsImhhc3VyYSI6eyJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbIkNSVSIsIkZvcmVuc2ljIiwiRXZTcGVjaWFsaXN0IiwiUHJldmlldyIsImRhbnkiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoiZGFueSIsIngtaGFzdXJhLXVzZXItaWQiOiJKRG9lIn19.lMkiBj6eIKT0CH-6sullN3qO9pDZimKLNfsUSR6G8WUrdtK_DD1kmtmu_nmwpE-RWSkEwSQC-u3g-ocRtrSinQ"
Callback token: {"sub":"JDoe","name":"JDoe","iat":1646318653.808,"exp":1646322253,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany","x-hasura-user-id":"JDoe"}}
Callback user: undefined
Before Encode function token: {"sub":"JDoe","name":"JDoe","iat":1646318653.808,"exp":1646322253,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany","x-hasura-user-id":"JDoe"}}
Encoded Token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NDYzMTg2NTMuODcyLCJleHAiOjE2NDYzMjIyNTMsImhhc3VyYSI6eyJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbIkNSVSIsIkZvcmVuc2ljIiwiRXZTcGVjaWFsaXN0IiwiUHJldmlldyIsImRhbnkiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoiZGFueSJ9fQ.lPao8tCFq7z0Pb8tIO7sm0L91fkwajA-Uuu_OgG6rIgo4sC3z6Zd07q1XaKNQ0P3-xt2c1bF0up6tVab3djG-g"
Callback token: {"sub":"JDoe","name":"JDoe","iat":1646318653.808,"exp":1646322253,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany","x-hasura-user-id":"JDoe"}}
Callback user: undefined
Before Encode function token: {"sub":"JDoe","name":"JDoe","iat":1646318653.808,"exp":1646322253,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany","x-hasura-user-id":"JDoe"}}
Encoded Token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NDYzMTg2NTUuMjk3LCJleHAiOjE2NDYzMjIyNTUsImhhc3VyYSI6eyJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbIkNSVSIsIkZvcmVuc2ljIiwiRXZTcGVjaWFsaXN0IiwiUHJldmlldyIsImRhbnkiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoiZGFueSJ9fQ.wnaes7CejSGCjHTzwNKTsNV3pfhy4iYqyRmfaNhpELQvJDciYMfTyFc2O4byq8cLAP2brUpfDwyQZIFZNMAGPA"
Callback token: {"iat":1646318655.297,"exp":1646322255,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany"}}
Callback user: undefined
Before Encode function token: {"iat":1646318655.297,"exp":1646322255,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany"}}
Encoded Token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NDYzMTg2NTUuNzQ3LCJleHAiOjE2NDYzMjIyNTUsImhhc3VyYSI6eyJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbIkNSVSIsIkZvcmVuc2ljIiwiRXZTcGVjaWFsaXN0IiwiUHJldmlldyIsImRhbnkiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoiZGFueSJ9fQ.6saQlySc7rgHikj85iejpz6nRm9fEtEdupz_j1hcwZ0TcZiQlLvY1-M-9xkju2F-0MlWmQwIj-bfEU7BmuEc5w"
Callback token: {"iat":1646318655.297,"exp":1646322255,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany"}}
Callback user: undefined
Before Encode function token: {"iat":1646318655.297,"exp":1646322255,"hasura":{"x-hasura-allowed-roles":["CRU","Forensic","EvSpecialist","Preview","dany"],"x-hasura-default-role":"dany"}}
Encoded Token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NDYzMTg2NTUuOTgsImV4cCI6MTY0NjMyMjI1NSwiaGFzdXJhIjp7IngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsiQ1JVIiwiRm9yZW5zaWMiLCJFdlNwZWNpYWxpc3QiLCJQcmV2aWV3IiwiZGFueSJdLCJ4LWhhc3VyYS1kZWZhdWx0LXJvbGUiOiJkYW55In19.0tqw2fg_GjQGY55sHCKxscipHaGj9IHrdgNKBETsOzMPn5Rai3CFx1TMJP9ZOcyAzbSBmrXN31uWKCrda37X_g"
Here is what my middleware currently looks like.
import { NextApiRequest } from 'next'
import { getToken } from 'next-auth/jwt'
import { NextResponse, NextRequest } from 'next/server'
//Groups and Pages Associated with groups
// const userGroups = [
// 'CRU',
// 'Forensic',
// 'Evidence Specialist',
// 'Preview',
// 'Dany',
// 'DEVELOPMENT',
// 'TEST',
// ]
//Group access pages.
const CRU_PAGES: string[] = ['']
const FORENSIC_PAGES: string[] = ['']
const EVSPEVIALIST_PAGES: string[] = ['']
const PREVIEW_PAGES: string[] = ['']
const DANY_PAGES: string[] = ['']
const DEV_PAGES: string[] = ['/testpages', '/testpage', '/cases']
const TEST_PAGES: string[] = ['/testpage']
//Default Access Routes
const DEFAULT_PAGES: string[] = [
'/',
'/unauthorized',
'/api',
'/favicon.ico',
'/login',
]
//The following function will protect the routes or redirect to unauthorized if not allowed to access a page.
function checkAuthPath(pathname: string, group: string, req: NextApiRequest) {
var authorizedPages: string[] = ['']
//Based on group assign authorized pages to the authorizedPages array.
switch (group) {
case 'CRU':
authorizedPages = CRU_PAGES
break
case 'Forensic':
authorizedPages = FORENSIC_PAGES
break
case 'Evidence Specialist':
authorizedPages = EVSPEVIALIST_PAGES
break
case 'Preview':
authorizedPages = PREVIEW_PAGES
break
case 'Dany':
authorizedPages = DANY_PAGES
break
case 'DEVELOPMENT':
authorizedPages = DEV_PAGES
break
default:
authorizedPages = TEST_PAGES
}
authorizedPages = authorizedPages.concat(DEFAULT_PAGES)
//Determine if request path is in the authorized paths Array. If not redirect to unauthorized.
if (authorizedPages.includes('/' + pathname.split('/')[3])) {
//console.log(`pathname in checkauth function: ${pathname}`)
//console.log('Authorized')
return NextResponse.next()
} else {
//console.log('Unauthorized')
return NextResponse.redirect(new URL('/unauthorized', req.url))
}
}
export async function middleware(req: NextApiRequest) {
const token = await getToken({ req, secret: process.env.JWT_SECRET })
// console.log(`Middleware Token: ${JSON.stringify(token)}`)
const pathname = req.url
if (typeof pathname === 'string') {
if (pathname.includes('/api/auth') || token) {
if (token) {
//console.log('Token Found')
//console.log(`pathname: ${pathname}`)
const group = token.group as string
return checkAuthPath(pathname, group, req)
} else return NextResponse.next()
}
if (
(!token && pathname !== 'http://localhost:3000/login') ||
pathname.includes('/api/auth')
) {
//&& !pathname.includes('/api/auth/signin'
//console.log('Token Not Found')
return NextResponse.redirect(new URL('/login', req.url))
}
} else {
throw new Error('Pathname Undefined')
}
}
As you can see by the last output i am missing then user and group from the JWT. Also why does it loop so many times?
Solved by including the custom encoding and decoding in the middleware too.
Related
i am using using mongobd adapter for nextauth, i want to persist session in the db if the user chose to login with credentials the adapter create the session in the db but the entire session async function block seems to be skipped there by not returning session to the client dont know ho to wrap around it.
"https://branche.online/next-auth-credentials-provider-with-the-database-session-strategy/" i got the inspiration from here.
import NextAuth from "next-auth"
import { MongoDBAdapter } from "#next-auth/mongodb-adapter"
import clientPromise from "../auth/lib/mongodb"
import { MongoClient } from 'mongodb';
import bcrypt from 'bcrypt'
import GoogleProvider from "next-auth/providers/google";
import FacebookProvider from "next-auth/providers/facebook";
import CredentialsProvider from "next-auth/providers/credentials";
// Modules needed to support key generation, token encryption, and HTTP cookie manipulation
import { randomUUID } from 'crypto'
// import Cookies from 'cookies'
import { setCookie, getCookie } from 'cookies-next';
import { encode, decode } from 'next-auth/jwt'
const MONGODB_URI = process.env.MONGODB_URI
// next auth starts here
export default async function handler(req, res) {
const adapter = MongoDBAdapter(clientPromise)
let userAccount = null
// Do whatever you want here, before the request is passed down to `NextAuth`
const generate = {}
generate.uuid = function () {
return uuidv4()
}
generate.uuidv4 = function () {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
)
}
// Helper functions to generate unique keys and calculate the expiry dates for session cookies
const generateSessionToken = () => {
// Use `randomUUID` if available. (Node 15.6++)
return randomUUID?.() ?? generate.uuid()
}
const fromDate = (time, date = Date.now()) => {
return new Date(date + time * 1000)
}
// callbacks for the sessions
const callbacks = {
async redirect({ url, baseUrl }) {
// Allows relative callback URLs
if (url.startsWith("/")) return `${baseUrl}${url}`
// Allows callback URLs on the same origin
else if (new URL(url).origin === baseUrl) return url
return baseUrl
},
async signIn({ user, account, profile, email, credentials }) {
console.log("User Signin Start: ", user)
// Check if this sign in callback is being called in the credentials authentication flow. If so, use the next-auth adapter to create a session entry in the database (SignIn is called after authorize so we can safely assume the user is valid and already authenticated).
if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
if (user) {
const sessionToken = generateSessionToken()
const sessionMaxAge = 60 * 60 * 24 * 7; //7Days
const sessionExpiry = fromDate(sessionMaxAge)
console.log("Session token is: ", sessionToken)
console.log("sessionExpiry: ", sessionExpiry);
await adapter.createSession({
sessionToken: sessionToken,
userId: user.username,
expires: sessionExpiry
})
setCookie("next-auth.session-token", sessionToken, {
expires: sessionExpiry,
req: req,
res: res,
})
console.log("user Session: ", user)
}
}
return true
},
async jwt({ token, user, account, profile, isNewUser }) {
console.log("JWT callback. Got User: ", user)
if (typeof user !== typeof undefined) {
token.user = user;
}
return token
},
async session({ session, token}) {
console.log("Session. Got User: ", session, token)
if (userAccount !== null) {
console.log("UserAccount Session Generation: ", user)
session.user = {
name: userAccount.name,
email: userAccount.email,
};
console.log("Session.user: ", session.user)
// return session
}
if (
token && typeof token.user !== typeof undefined && (typeof session.user === typeof undefined ||
(typeof session.user !== typeof undefined && typeof session.user.id === typeof undefined))
) {
session.user = token.user
}
if (typeof token !== typeof undefined) {
session.token = token
}
console.log("Session: ", session)
return session
},
}
const options = {
session: {
strategy: "database",
maxAge: 7 * 24 * 60 * 60,
updateAge: 24 * 60 * 60,
// generateSessionToken: () => {
// return randomUUID?.() ?? randomBytes(32).toString("hex")
// }
},
jwt: {
// Customize the JWT encode and decode functions to overwrite the default behaviour of storing the JWT token in the session cookie when using credentials providers. Instead we will store the session token reference to the session in the database.
encode: async (token, secret, maxAge) => {
if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
// const cookies = new Cookies(req,res)
// const cookie = cookies.get('next-auth.session-token')
const cookie = getCookie("next-auth.session-token", { req: req });
console.log("pure Cookie: ", cookie);
if(cookie) return cookie
else return ''
}
// Revert to default behaviour when not in the credentials provider callback flow
return encode(token, secret, maxAge)
},
decode: async (token, secret) => {
if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
return null
}
// Revert to default behaviour when not in the credentials provider callback flow
return decode(token, secret)
}
},
debug: process.env.NODE_ENV === "development",
adapter,
secret: process.env.NEXTAUTH_SECRET,
providers: [
CredentialsProvider({
name: 'Credentials',
async authorize(credentials, req) {
const client = new MongoClient(MONGODB_URI, {},)
try {
await client.connect()
} catch (e) {
console.error(e)
}
const email = credentials.email
const password = credentials.password
//Get all the users
const users = client.db().collection('users')
//Find user with the email
const user = await users.findOne(
{
$or: [
{ email: email }, { username: email }
]
}
)
// //Not found - send error res
if (!user) {
client.close();
throw new Error('No user found with this credential')
}
console.log("Authorize User Credentials: ", user)
//Check hased password with DB password
const checkPassword = bcrypt.compareSync(password, user.password)
//Incorrect password - send response
if (!checkPassword) {
client.close();
throw new Error('Password doesnt match')
}
//Incorrect password - send response
if (!checkPassword) {
client.close();
throw new Error('Password doesnt match')
}
//Else send success response
userAccount = {
id: user._id,
name: user.name,
email: user.email,
};
client.close();
return userAccount;
},
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
allowDangerousEmailAccountLinking: true,
}),
FacebookProvider({
clientId: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET
}),
// ...add more providers here
],
callbacks: callbacks,
pages: {
signIn: '/signin',
signOut: '/signout',
error: '/auth/error', // Error code passed in query string as ?error=
verifyRequest: '/auth/verify-request', // (used for check email message)
// newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
},
}
return await NextAuth(req, res, options)
}
const Raven = require('raven')
const GithubStrategy = require('passport-github2').Strategy
const axios = require('axios');
const models = require('../../../db/models').models
const config = require('../../../../config')
const secrets = config.SECRETS
const debug = require('debug')('oauth:strategies:github')
const { generateReferralCode } = require('../../../utils/referral')
/**
* Authenticate _users_ using their Github Accounts
*/
module.exports = new GithubStrategy({
clientID: secrets.GITHUB_CONSUMER_KEY,
clientSecret: secrets.GITHUB_CONSUMER_SECRET,
callbackURL: config.SERVER_URL + config.GITHUB_CALLBACK,
passReqToCallback: true,
}, async function (req, token, tokenSecret, profile, cb) {
let profileJson = profile._json
console.log(profileJson);
try{
const config = {method:'get',headers:{'Authorization':`Bearer ${token}`}};
const result = await axios.get('https://api.github.com/user/emails',config);
let emailArr = result.data;
let primaryGithubEmail = emailArr.filter((email)=>{return email.primary;});
profileJson.email = primaryGithubEmail[0].email;
}catch(error){
console.log(error);
}
console.log('======== profile after request ============');
console.log(profileJson);
console.log('====================');
let oldUser = req.user
Raven.setContext({ extra: { file: 'githubstrategy' } })
try {
if (oldUser) {
debug('User exists, is connecting Github account')
/*
This means an already logged in users is trying to
connect Github to his account. Let us see if there
are any connections to his Github already
*/
const ghaccount = await models.UserGithub.findOne({ where: { id: profileJson.id } })
if (ghaccount) {
throw new Error('Your Github account is already linked with codingblocks account Id: ' + ghaccount.get('userId'))
} else {
await models.UserGithub.upsert({
id: profileJson.id,
token: token,
tokenSecret: tokenSecret,
username: profileJson.login,
userId: oldUser.id
})
const user = await models.User.findById(oldUser.id)
if (user) {
return cb(null, user.get())
} else {
return cb(null, false, { message: "Could not retrieve existing Github linked account" })
}
}
} else {
/*
This means either -
a. This is a new signup via Github
b. Someone is trying to login via Github
*/
let userGithub = await models.UserGithub.findOne({
include: [models.User],
where: { id: profileJson.id }
})
/*
If userGithub exists then
Case (a): login
*/
if (!userGithub) {
/*
If there is any user with verified email equal to the email comming from github strategy , then create a new entry in userGithub table and login that user
*/
const userWithVerifiedEmail = await models.User.findOne({
where: {
verifiedemail: profileJson.email
}
})
if (userWithVerifiedEmail) {
userGithub = await models.UserGithub.create({
id: profileJson.id,
token: token,
tokenSecret: tokenSecret,
username: profileJson.login,
userId: userWithVerifiedEmail.get('id'),
})
return cb(null, userWithVerifiedEmail.get());
}
/*
Case (b): New Signup
First ensure there aren't already users with the same email
id that comes from Github
*/
let existingUsers = [];
if (profileJson.email) {
existingUsers = await models.User.findAll({
include: [{
model: models.UserGithub,
attributes: ['id'],
required: false
}],
where: {
email: profileJson.email,
'$usergithub.id$': { $eq: null }
}
})
}
if (existingUsers && existingUsers.length > 0) {
let oldIds = existingUsers.map(eu => eu.id).join(',')
return cb(null, false, {
message: `
Your email id "${profileJson.email}" is already used in the following Coding Blocks Account(s):
[ ${oldIds} ]
Please log into your old account and connect Github in it instead.
Use 'Forgot Password' option if you do not remember password of old account`
})
}
/* Check if users with same username exist. Modify username accordingly */
const existCount = await models.User.count({ where: { username: profileJson.login } })
userGithub = await models.UserGithub.create({
id: profileJson.id,
token: token,
tokenSecret: tokenSecret,
username: profileJson.login,
user: {
username: existCount === 0 ? profileJson.login : profileJson.login + "-gh",
firstname: profileJson.name ? profileJson.name.split(' ')[0] : profileJson.login,
email: profileJson.email,
referralCode: generateReferralCode(profileJson.email).toUpperCase(),
photo: profileJson.avatar_url,
verifiedemail: profileJson.email,
marketing_meta: req.session.marketingMeta
}
}, {
include: [models.User],
})
req.visitor.event({
ea: 'successful',
ec: 'signup',
el: 'github'
}).send()
req.session.isNewSignup = true
if (!userGithub) {
return cb(null, false, { message: 'Authentication Failed' })
}
}
return cb(null, userGithub.user.get())
}
} catch (err) {
Raven.captureException(err)
cb(null, false, { message: err.message })
}
})
when I am running this code locally on my system , its working fine , but as soon as I am sending the code to the staging and production i am getting the error
{ Error: Request failed with status code 404
0|oneauth | at createError (/home/codingblocks/servers/khaate/node_modules/axios/lib/core/createError.js:16:15)
0|oneauth | at settle (/home/codingblocks/servers/khaate/node_modules/axios/lib/core/settle.js:17:12)
0|oneauth | at IncomingMessage.handleStreamEnd (/home/codingblocks/servers/khaate/node_modules/axios/lib/adapters/http.js:236:11)
0|oneauth | at IncomingMessage.emit (events.js:203:15)
0|oneauth | at IncomingMessage.EventEmitter.emit (domain.js:466:23)
0|oneauth | at IncomingMessage.wrapped (/home/codingblocks/servers/khaate/node_modules/newrelic/lib/transaction/tracer/index.js:188:22)
...............
I am not able to understand why this error is comming and I am also not able to get the user emails , but locally this code works fine and I am able to fetch all the emails associated with the github of the user.
I got an problem when migrate my js file jo tsx, what I'm doing is signin with credentials and custom the session user to my user data
// api/auth/[...nextauth].js
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
import { ConnectDatabase } from "../../../lib/db";
import { VertifyPassword } from "../../../lib/password";
import { getSelectedUser } from "../../../helpers/database";
import { MongoClient } from "mongodb";
import { NextApiRequest } from "next";
interface credentialsData {
data: string | number;
password: string;
}
export default NextAuth({
session: {
jwt: true,
},
callbacks: {
async session(session) {
const data = await getSelectedUser(session.user.email);
session.user = data.userData;
// inside data.userdata is a object
// {
// _id: '60a92f328dc04f58207388d1',
// email: 'user#user.com',
// phone: '087864810221',
// point: 0,
// role: 'user',
// accountstatus: 'false'
// }
return Promise.resolve(session);
},
},
providers: [
Providers.Credentials({
async authorize(credentials: credentialsData, req: NextApiRequest) {
let client;
try {
client = await ConnectDatabase();
} catch (error) {
throw new Error("Failed connet to database.");
}
const checkEmail = await client
.db()
.collection("users")
.findOne({ email: credentials.data });
const checkPhone = await client
.db()
.collection("users")
.findOne({ phone: credentials.data });
let validData = {
password: "",
email: "",
};
if (!checkEmail && !checkPhone) {
client.close();
throw new Error("Email atau No HP tidak terdaftar.");
} else if (checkEmail) {
validData = checkEmail;
} else if (checkPhone) {
validData = checkPhone;
}
const checkPassword = await VertifyPassword(
credentials.password,
validData.password
);
if (!checkPassword) {
client.close();
throw new Error("Password Salah.");
}
client.close();
// inside validData is a object
// {
// _id: '60a92f328dc04f58207388d1',
// email: 'user#user.com',
// phone: '087864810221',
// point: 0,
// role: 'user',
// accountstatus: 'false'
// }
return validData;
},
}),
],
});
// as default provider just return session.user just return email,name, and image, but I want custom the session.user to user data what I got from dababase
This in client side
// index.tsx
export const getServerSideProps: GetServerSideProps<{
session: Session | null;
}> = async (context) => {
const session = await getSession({ req: context.req });
if (session) {
if (session.user?.role === "admin") {
return {
redirect: {
destination: "/admin/home",
permanent: false,
},
};
}
}
return {
props: {
session,
},
};
};
But in client side I got warning
Property 'role' does not exist on type '{ name?: string; email?: string; image?: string;
actually my file still working fine, but when my file in js format, its not warning like that
can someone help me to fix it ?
Not sure if you found a workaround yet but you need to configure the jwt callback as well! Here is an example from a project of mine:
callbacks: {
async session(session, token) {
session.accessToken = token.accessToken;
session.user = token.user;
return session;
},
async jwt(token, user, account, profile, isNewUser) {
if (user) {
token.accessToken = user._id;
token.user = user;
}
return token;
},
},
To explain things. jwt function always runs before session, so whatever data you pass to jwt token will be available on session function and you can do whatever you want with it. In jwt function i check if there is a user because this only returns data only when you login.
I imagine by now you have this solved, but since I ran across this page with the same issue I figured I'd post my solution. Just in case someone else runs across it. I'm new to typescript/nextjs and didn't realize I simply had to create a type definition file to add the role field to session.user
I created /types/next-auth.d.ts
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
username: string;
email: string;
role: string;
[key: string]: string;
};
}
}
Then I had to add this to my tsconfig.json
"include": ["next-env.d.ts", "types/**/*.ts", "**/*.ts", "**/*.tsx"],
I have spent the night looking for solutions to this issue, it seems like a lot of people have it and the best advice is often "just switch to SPA mode", which is not an option for me.
I have JWT for authentication, using the JWTSessions gem for Rails.
On the frontend, I have Nuxt with nuxt-auth, using a custom scheme, and the following authorization middleware:
export default function ({ $auth, route, redirect }) {
const role = $auth.user && $auth.user.role
if (route.meta[0].requiredRole !== role) {
redirect('/login')
}
}
The symptom I have is as follows: if I log in and navigate around restricted pages, everything works as expected. I even have fetchOnServer: false for restricted pages, as I only need SSR for my public ones.
However, once I refresh the page or just navigate directly to a restricted URL, I get immediately redirected to the login page by the middleware. Clearly, the user that's authenticated on the client side is not being authenticated on the server side too.
I have the following relevant files.
nuxt.config.js
...
plugins: [
// ...
{ src: '~/plugins/axios' },
// ...
],
// ...
modules: [
'cookie-universal-nuxt',
'#nuxtjs/axios',
'#nuxtjs/auth'
],
// ...
axios: {
baseURL: process.env.NODE_ENV === 'production' ? 'https://api.example.com/v1' : 'http://localhost:3000/v1',
credentials: true
},
auth: {
strategies: {
jwtSessions: {
_scheme: '~/plugins/auth-jwt-scheme.js',
endpoints: {
login: { url: '/signin', method: 'post', propertyName: 'csrf' },
logout: { url: '/signin', method: 'delete' },
user: { url: '/users/active', method: 'get', propertyName: false }
},
tokenRequired: true,
tokenType: false
}
},
cookie: {
options: {
maxAge: 64800,
secure: process.env.NODE_ENV === 'production'
}
}
},
auth-jwt-scheme.js
const tokenOptions = {
tokenRequired: true,
tokenType: false,
globalToken: true,
tokenName: 'X-CSRF-TOKEN'
}
export default class LocalScheme {
constructor (auth, options) {
this.$auth = auth
this.name = options._name
this.options = Object.assign({}, tokenOptions, options)
}
_setToken (token) {
if (this.options.globalToken) {
this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, token)
}
}
_clearToken () {
if (this.options.globalToken) {
this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, false)
this.$auth.ctx.app.$axios.setHeader('Authorization', false)
}
}
mounted () {
if (this.options.tokenRequired) {
const token = this.$auth.syncToken(this.name)
this._setToken(token)
}
return this.$auth.fetchUserOnce()
}
async login (endpoint) {
if (!this.options.endpoints.login) {
return
}
await this._logoutLocally()
const result = await this.$auth.request(
endpoint,
this.options.endpoints.login
)
if (this.options.tokenRequired) {
const token = this.options.tokenType
? this.options.tokenType + ' ' + result
: result
this.$auth.setToken(this.name, token)
this._setToken(token)
}
return this.fetchUser()
}
async setUserToken (tokenValue) {
await this._logoutLocally()
if (this.options.tokenRequired) {
const token = this.options.tokenType
? this.options.tokenType + ' ' + tokenValue
: tokenValue
this.$auth.setToken(this.name, token)
this._setToken(token)
}
return this.fetchUser()
}
async fetchUser (endpoint) {
if (this.options.tokenRequired && !this.$auth.getToken(this.name)) {
return
}
if (!this.options.endpoints.user) {
this.$auth.setUser({})
return
}
const user = await this.$auth.requestWith(
this.name,
endpoint,
this.options.endpoints.user
)
this.$auth.setUser(user)
}
async logout (endpoint) {
if (this.options.endpoints.logout) {
await this.$auth
.requestWith(this.name, endpoint, this.options.endpoints.logout)
.catch(() => {})
}
return this._logoutLocally()
}
async _logoutLocally () {
if (this.options.tokenRequired) {
this._clearToken()
}
return await this.$auth.reset()
}
}
axios.js
export default function (context) {
const { app, $axios, redirect } = context
$axios.onResponseError(async (error) => {
const response = error.response
const originalRequest = response.config
const access = app.$cookies.get('jwt_access')
const csrf = originalRequest.headers['X-CSRF-TOKEN']
const credentialed = (process.client && csrf) || (process.server && access)
if (credentialed && response.status === 401 && !originalRequest.headers.REFRESH) {
if (process.server) {
$axios.setHeader('X-CSRF-TOKEN', csrf)
$axios.setHeader('Authorization', access)
}
const newToken = await $axios.post('/refresh', {}, { headers: { REFRESH: true } })
if (newToken.data.csrf) {
$axios.setHeader('X-CSRF-TOKEN', newToken.data.csrf)
$axios.setHeader('Authorization', newToken.data.access)
if (app.$auth) {
app.$auth.setToken('jwt_access', newToken.data.csrf)
app.$auth.syncToken('jwt_access')
}
originalRequest.headers['X-CSRF-TOKEN'] = newToken.data.csrf
originalRequest.headers.Authorization = newToken.data.access
if (process.server) {
app.$cookies.set('jwt_access', newToken.data.access, { path: '/', httpOnly: true, maxAge: 64800, secure: false, overwrite: true })
}
return $axios(originalRequest)
} else {
if (app.$auth) {
app.$auth.logout()
}
redirect(301, '/login')
}
} else {
return Promise.reject(error)
}
})
}
This solution is already heavily inspired by material available under other threads and at this point I am pretty much clueless regarding how to authenticate my users universally across Nuxt. Any help and guidance much appreciated.
In order for You not to lose Your authentication session in the system, You first need to save your JWT token to some storage on the client: localStorage or sessionStorage or as well as token data can be saved in cookies.
For to work of the application will be optimally, You also need to save the token in the store of Nuxt. (Vuex)
If You save Your token only in srore of Nuxt and use only state, then every time You refresh the page, Your token will be reset to zero, since the state will not have time to initialize. Therefore, you are redirected to the page /login.
To prevent this from happening, after you save Your token to some storage, You need to read it and reinitialize it in the special method nuxtServerInit(), in the universal mode his will be work on the server side the very first. (Nuxt2)
Then, accordingly, You use Your token when sending requests to the api server, adding to each request that requires authorization, a header of the Authorization type.
Since Your question is specific to the Nuxt2 version, for this version a working code example using cookies to store the token would be:
/store/auth.js
import jwtDecode from 'jwt-decode'
export const state = () => ({
token: null
})
export const getters = {
isAuthenticated: state => Boolean(state.token),
token: state => state.token
}
export const mutations = {
SET_TOKEN (state, token) {
state.token = token
}
}
export const actions = {
autoLogin ({ dispatch }) {
const token = this.$cookies.get('jwt-token')
if (isJWTValid(token)) {
dispatch('setToken', token)
} else {
dispatch('logout')
}
},
async login ({ commit, dispatch }, formData) {
const { token } = await this.$axios.$post('/api/auth/login', formData, { progress: false })
dispatch('setToken', token)
},
logout ({ commit }) {
this.$axios.setToken(false)
commit('SET_TOKEN', null)
this.$cookies.remove('jwt-token')
},
setToken ({ commit }, token) {
this.$axios.setToken(token, 'Bearer')
commit('SET_TOKEN', token)
this.$cookies.set('jwt-token', token, { path: '/', expires: new Date('2024') })
// <-- above use, for example, moment or add function that will computed date
}
}
/**
* Check valid JWT token.
*
* #param token
* #returns {boolean}
*/
function isJWTValid (token) {
if (!token) {
return false
}
const jwtData = jwtDecode(token) || {}
const expires = jwtData.exp || 0
return new Date().getTime() / 1000 < expires
}
/store/index.js
export const state = () => ({
// ... Your state here
})
export const getters = {
// ... Your getters here
}
export const mutations = {
// ... Your mutations here
}
export const actions = {
nuxtServerInit ({ dispatch }) { // <-- init auth
dispatch('auth/autoLogin')
}
}
/middleware/isGuest.js
export default function ({ store, redirect }) {
if (store.getters['auth/isAuthenticated']) {
redirect('/admin')
}
}
/middleware/auth.js
export default function ({ store, redirect }) {
if (!store.getters['auth/isAuthenticated']) {
redirect('/login')
}
}
/pages/login.vue
<template>
<div>
<!-- Your template here-->
</div>
</template>
<script>
export default {
name: 'Login',
layout: 'empty',
middleware: ['isGuest'], // <-- if the user is authorized, then he should not have access to the page !!!
data () {
return {
controls: {
login: '',
password: ''
},
rules: {
login: [
{ required: true, message: 'login is required', trigger: 'blur' }
],
password: [
{ required: true, message: 'password is required', trigger: 'blur' },
{ min: 6, message: 'minimum 6 length', trigger: 'blur' }
]
}
}
},
head: {
title: 'Login'
},
methods: {
onSubmit () {
this.$refs.form.validate(async (valid) => { // <-- Your validate
if (valid) {
// here for example: on loader
try {
await this.$store.dispatch('auth/login', {
login: this.controls.login,
password: this.controls.password
})
await this.$router.push('/admin')
} catch (e) {
// eslint-disable-next-line no-console
console.error(e)
} finally {
// here for example: off loader
}
}
})
}
}
}
</script>
! - You must have the following packages installed:
cookie-universal-nuxt
jsonwebtoken
jwt-decode
I think you will find my answer helpful. If something is not clear, ask!
For, an user verification now I hardcoded the username and password directly on my code. But I want this dynamically using database username and password. As, i'm new to hapi.js it seems quite difficult for me. This is my code :
app.js
const auth = require('hapi-auth-basic');
const hapi = require('hapi');
mongoose.connect('mongodb://localhost:27017/db', {
useNewUrlParser: true }, (err) => {
if (!err) { console.log('Succeeded.') }
else { console.log(`Error`)}
});
const StudentModel = mongoose.model('Student', {
username: String,
password: String
});
const user = {
name: 'jon',
password: '123'
};
const validate = async (request, username, password, h) => {
let isValid = username === user.name && password === user.password;
return {
isValid: isValid,
credentials: {
name: user.name
}
};
};
const init = async () => {
await server.register(auth);
server.auth.strategy('simple', 'basic', {validate});
server.auth.default('simple');
server.route({
method: 'GET',
path: '/',
handler: async (request, h) => {
return 'welcome';
}
});
}
I tried to do this by changing the validate as below :
const validate = async (request, username, password, h) => {
let isValid = username === request.payload.name && password === request.payload.password;
return {
isValid: isValid,
credentials: {
name: request.payload.name
}
};
};
but i got the type error "name" as it's natural. How can I modify this?
Here, fetch user and check in the validation method
const validate = async (request, username, password, h) => {
// fetch user here
const user = await StudentModel.findOne({username, password}).exec();
// user doesn't exist
if(!user) return {isValid: false}
// just make sure here user really exists
return {
isValid: true,
credentials: {
name: user.name
}
}
}