NextAuth - AccessToken not refreshed with MongoDB and Coinbase - mongodb

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

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
}

node-oidc-provider 7.12.0 (JWT, authorization_code) invalid_token error on UserInfo endpoint (/me)

i have some issues getting the UserInfo endpoint working using JWT AccessTokens, it works fine with default settings when commenting out the resourceIndicators section.
I can get the access token using PostMan without issues, but when posting on the UserInfo (/me) endpoint the Bearer AccessToken, i got an invalid_token error.
here is my code:
const {Provider} = require('oidc-provider');
let hostname = process.env.HOSTNAME;
if (hostname === undefined) {
hostname = "http://localhost"
}
const port = process.env.PORT || 3000;
if (port !== 80 && port !== 443) {
hostname = hostname + ':' + port
}
const users = [
{
"id": "user1",
"email": "user1#example.com",
"authentication_method_reference": "mfa"
}
]
const clients = [
{
"client_id": "client-1",
"client_secret": "client-1-secret",
"redirect_uris": [
"http://localhost:3000"
]
}
]
async function findAccount (ctx, id) {
// This would ideally be just a check whether the account is still in your storage
let account = users.find(user => {
return user.id === id;
})
if (!account) {
return undefined;
}
return {
accountId: id,
async claims() {
return {
sub: id,
email: account.email,
amr: [account.authentication_method_reference]
};
},
};
}
const configuration = {
clients: clients,
conformIdTokenClaims: false,
features: {
devInteractions: {
enabled: true
},
resourceIndicators: {
defaultResource: (ctx, client, oneOf) => {
return hostname;
},
enabled: true,
getResourceServerInfo: (ctx, resourceIndicator, client) => {
console.log('get resource server info', client);
return ({
audience: resourceIndicator,
scope: 'openid',
accessTokenTTL: 2 * 60 * 60,
accessTokenFormat: 'jwt',
});
},
useGrantedResource: (ctx, model) => { return true; }
}
},
claims: {
openid: [
'sub',
'email',
'amr'
]
},
cookies: {
keys: 'super,secret'.split(',')
},
pkce: {
required: () => false
},
// Used to skip the 'approval' page
async loadExistingGrant(ctx) {
const grantId = (ctx.oidc.result
&& ctx.oidc.result.consent
&& ctx.oidc.result.consent.grantId) || ctx.oidc.session.grantIdFor(ctx.oidc.client.clientId);
if (grantId) {
// keep grant expiry aligned with session expiry
// to prevent consent prompt being requested when grant expires
const grant = await ctx.oidc.provider.Grant.find(grantId);
// this aligns the Grant ttl with that of the current session
// if the same Grant is used for multiple sessions, or is set
// to never expire, you probably do not want this in your code
if (ctx.oidc.account && grant.exp < ctx.oidc.session.exp) {
grant.exp = ctx.oidc.session.exp;
await grant.save();
}
return grant;
} else {
const grant = new ctx.oidc.provider.Grant({
clientId: ctx.oidc.client.clientId,
accountId: ctx.oidc.session.accountId,
});
grant.addOIDCScope('openid');
grant.addResourceScope(hostname, 'openid');
await grant.save();
return grant;
}
},
extraTokenClaims: async (ctx, token) => {
return findAccount(ctx, token.accountId).then(account => {
return account.claims()
})
},
findAccount: findAccount
};
const oidc = new Provider(hostname, configuration);
function handleServerError(ctx, err) {
console.log(err);
}
function handleGrantErrors({headers: {authorization}, oidc: {body, client}}, err) {
console.log(err);
}
function handleAccessToken(token) {
console.log(token);
}
oidc.on('grant.error', handleGrantErrors);
oidc.on('introspection.error', handleGrantErrors);
oidc.on('revocation.error', handleGrantErrors);
oidc.on('server_error', handleServerError);
oidc.on('access_token.issued', handleAccessToken);
oidc.listen(port, () => {
console.log(`oidc-provider listening on port ${port}.`)
})
I tried different configurations without success, the generated JWT AccessToken looks fine to me (see bellow), but i'm unable to query the UserInfo endpoint with it.
{
"sub": "user1",
"email": "user1#example.com",
"amr": [
"mfa"
],
"jti": "-7gURc8Y1SXqOXhWR691i",
"iat": 1668777371,
"exp": 1668784571,
"scope": "openid",
"client_id": "client-1",
"iss": "http://localhost:3000",
"aud": "http://localhost:3000"
}
Thanks in advance.
As per the module documentation's userinfo feature.
Its use requires an opaque Access Token with at least openid scope that's without a Resource Server audience.
In essence, this implementation's userinfo endpoint will not work with JWT Access Tokens that are issued for a particular resource server. This is because the userinfo endpoint is a resource for the client and if it was callable with an access token that was sent to a resource server, that resource server can turn around and query userinfo which is not the intended use of the userinfo endpoint.
In cases when JWT Access Tokens are issued the client will get all scope requested userinfo claims in the ID Token it receives, removing the need to call userinfo.

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

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

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);

Refresh expired JWT token

this is my middleware in backend
const jwt = require('jsonwebtoken');
const User = require('../models/user');
module.exports = (req, res, next) => {
const auth = req.get('Authorization');
if (!auth) {
return res.status(401).json({
"response": {
"message": 'Not authorized user',
}
})
}
const token = auth.split(' ')[1];
let decode;
try {
decode = jwt.verify(token, process.env.JWT_SECRET_KEY)
} catch (err) {
return res.status(401).json({
"response": {
"message": 'Invalid token access',
}
})
}
if (!decode) {
return res.status(500).json({
"response": {
"message": 'Error token access',
}
})
}
let userId = decode.id;
User.findById(userId)
.then(user => {
req.user = user;
next();
}).catch(err => {
console.log(err);
res.status(401).json({
"response": {
"message": `Error on authorization`,
}
})
})
}
I need integration of auto refresh token on each call when expire, and how can do for resave in localstorage in my client side?
Have my front-end developed with vue and vuex. Use this localStorage.setItem('access_token', token); code for save in localstorage.
Below is the logic that I did in my own web app
When user logs in you need to create access and refresh token
After you receive both tokens keep them in localStorage or wherever is safe
You need to create a refreshToken route(/refresh-token) to call when your access token expired
Define a middleware to check tokens and use it in secured routes
When you get an 401 error call /refresh-token route to refresh the token
config file
{
"secret": "secret",
"refreshTokenSecret": "refresh-secret",
"port": 8080,
"tokenLife": 900,
"refreshTokenLife": 86400
}
login route(app.js)
router.get('/login',(req, res) => {
const postData = req.body;
const user = {
"email": postData.email,
"name": postData.name
}
const token = jwt.sign(user, config.secret, { expiresIn: config.tokenLife})
const refreshToken = jwt.sign(user, config.refreshTokenSecret, { expiresIn:
config.refreshTokenLife})
const response = {
"status": "Logged in",
"token": token,
"refreshToken": refreshToken,
}
tokenList[refreshToken] = response
res.status(200).json(response);
}
refresh token route(app.js)
router.post('/refresh-token', (req,res) => {
const postData = req.body
if((postData.refreshToken) && (postData.refreshToken in tokenList)) {
const user = {
"email": postData.email,
"name": postData.name
}
const token = jwt.sign(user, config.secret, { expiresIn: config.tokenLife})
const response = {
"token": token,
}
// update the token in the list
tokenList[postData.refreshToken].token = token
res.status(200).json(response);
} else {
res.status(404).send('Invalid request')
}
})
tokenMiddleware.js
const jwt = require('jsonwebtoken')
const config = require('./config')
module.exports = (req,res,next) => {
const token = req.body.token || req.query.token || req.headers['x-access-token']
if (token) {
jwt.verify(token, config.secret, function(err, decoded) {
if (err) {
return res.status(401).json({"error": true, "message": 'Unauthorized access.' });
}
req.decoded = decoded;
next();
});
} else {
return res.status(403).send({
"error": true,
"message": 'No token provided.'
});
}
}
use middleware to protect routes
router.get('/secured-route', tokenMiddleware, async (req, res) => {
res.send("secured");
});