how to auto login/ slient login with refreshToken in next-auth - next.js

I'm struggling with next-auth for authorization.
In our project, two domains. so the main domain has a login page, after login set accessToken in redux and refresh Token in a cookie. But subDomain does not have a login page. Because main domain makes user redirect subDomain with refresh token(http only cookie). In a subDomain call API includes a refresh Token, and then the server gives a refresh token and access token. accessToken set headers. when accessToken is expired, call Api includes a refresh token again
this is our authorization process.
But I can not find the way by using next-auth in the subDomain. Because the page does not have a login. I want to make an auto-login with a refresh token. How to set up the [...nextauth].ts and other file settingss
Thank you!
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import axios from "axios";
axios.defaults.baseURL = `${process.env.NEXT_PUBLIC_API_URL}`;
export default NextAuth({
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {},
async authorize(credentials, req) {
const response = await axios.post("/api/auth/getRefreshToken");
return response.data;
},
}),
],
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
return true;
},
async redirect({ url, baseUrl }) {
return baseUrl;
},
async session({ session, token, user }) {
session.accessToken = token.accessToken;
return session;
},
async jwt({ token, account }) {
// Persist the OAuth access_token to the token right after signin
if (account) {
token.accessToken = account.access_token;
}
return token;
},
},
// pages: {
// signIn: "/login",
// },
});

Related

Next-auth with CredentialsProvider signup is partially working, but the Session returned to the client is null

I have the GoogleProvider working, and returning a valid session. But when I create a CredentialsProvider it isn't working.
The credentials provider successfully creates a user in the database with graphql. And I'm able to log the returned user at the bottom.
import { client } from '#/src/graphql/apollo-client'
import UserOperations from '#/src/graphql/operations/user'
import { CreateUserData, CreateUserInput } from '#/src/util/types'
import credentialsProvider from 'next-auth/providers/credentials'
export default CredentialsProvider({
name: '',
credentials: {
email: { label: 'Username', type: 'text', placeholder: 'email' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials, req) {
const signUp = UserOperations.Mutations.signUp
const response = await client.mutate<CreateUserData, CreateUserInput>({
mutation: signUp,
variables: {
email: credentials?.email!,
password: credentials?.password!
}
})
const { data } = response
const user = data
console.log('the user data returned is ', user)
if (user) {
return user
}
return null
}
})
I'm using that provider along side a GoogleProvider in [...nextauth].ts
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import { PrismaAdapter } from '#next-auth/prisma-adapter'
import { PrismaClient } from '#prisma/client'
import CredentialsProvider from './providers/credentials'
const prisma = new PrismaClient()
export default NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
credentialsProvider, //the credentials provider from above
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string
})
],
secret: process.env.JWT_SECRET,
callbacks: {
async session({ session, token, user }) {
//here the google provider returns values, but the credentials provider does not.
console.log('session ', session, ' user ', user)
return {
...session,
user: { ...session.user, ...user } // combine the session and db user
}
}
}
})
Inside of async session you can see I have another console log. When using the google provider I can log the session and user, but I'm getting session:null and user:null with the credentials provider.
So I'm not sure where session gets set with Credentials Provider or what other steps I may be missing.
Happy to provide any other files if they help, but I thought this would be simplest to start.
Help is much appreciated, thanks.
EDIT -
It looks like someone was able to use credentials provider along side Prisma adapter here. I tried adding the encode/ decode token block that they used, but still no luck.
I may be confused about how JWT fits into the picture.
I was able to get this to work!! I just use strategy 'jwt', and then in the jwt callback I had access to the user and token. So I could assign the user to user.token and then the token is available in the session callback.
So with other providers like google I could access user and session directly in the session callback. But with credentials I had to go through the token parameter.
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import { PrismaAdapter } from '#next-auth/prisma-adapter'
import { PrismaClient } from '#prisma/client'
import credentialsProvider from '../../../providers/credentials'
const prisma = new PrismaClient()
export default NextAuth({
adapter: PrismaAdapter(prisma),
session: {
strategy: 'jwt'
},
providers: [
credentialsProvider,
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string
})
],
secret: process.env.JWT_SECRET,
callbacks: {
jwt: async ({ token, user }) => {
user && (token.user = user)
return token
},
//whatever value we return here will be the value of the next-auth session
async session({ session, token, user }) {
return {
...session,
user: { ...session.user, ...user, ...token.user! } // combine the session and db user
}
}
}
})

NextAuth signIn pass parameter to callback

I am using NextAuth to enable users to sign up/in with their Google account and also to link their Google account to their current account on my site.
In order to differentiate between signing up and linking an account when already signed in, I want to pass an extra parameter to signIn that I can access in the signIn callback that will allow me to take the correct action. I have tried:
signIn("google", null, { linkAccount: "true" });
However, this is only passed into the signIn request as a query parameter and is not passed through to the callback. Is there any way I can make a custom argument accessible in the callback?
Edit: Including more code below.
Call to next-auth's signIn client API:
signIn("google", null { linkAccount: "true" });
[...nextauth.js]
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import axios from 'axios';
const authOptions = (req) => ({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
secret: "secret",
callbacks: {
async signIn({
user, account, profile, email, credentials
}) {
// GOAL: How can I specify to this endpoint that I am just linking an account?
let res = await axios.post('http://localhost:8000/users/third_party_sign_in', {
third_party_id: user.id,
email: user.email,
type: account.provider
justLink: true|false
}, { withCredentials: true })
let path;
if (res.data.action === "login") {
path = `/?action=${res.data.action}&id=${res.data.user_id}&email=${user.email}&third_party=${account.provider}`
} else if (res.data.action === "create") {
path = `/?action=${res.data.action}&name=${user.name}&email=${user.email}&third_party=${account.provider}&third_party_id=${user.id}`
}
return path;
},
async redirect({ url }) {
return Promise.resolve(url)
}
},
});
function testNextApiRequest(req) {
if (req.query.nextauth
&& req.query.nextauth.length === 2
&& req.query.linkAccount) {
/// logs for signIn API call but not for callback
console.log("QUERY PARAMS: ", req.query);
}
}
export default (req, res) => {
testNextApiRequest(req);
return NextAuth(req, res, authOptions(req));
}
I also spent a lot of time on this trying to figure out how to get a param in a callback when using the signIn function.
Here's the solution
call signIn like you were doing
signIn("google", null, { linkAccount: "true" });
Now in [...nextauth].ts you want to parse req.query BEFORE passing it to next-auth like so
authOptions is just a function that returns next-auth callbacks and config.
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
console.log(req.query); // This will have your linkAccount param
return await NextAuth(req, res, authOptions(req, res));
}
Now that you have access to the params you can do whatever logic you want. This will only work for some callbacks like redirect. Trying to get the params in the session callback is still proving to be impossible for me.
It isn't great, honestly it's pretty bad if you do db queries you'll be slowing down all the requests but I think this is currently the best way to do it. Hope it helps!
More discussion here https://github.com/nextauthjs/next-auth/discussions/901

Next-Auth Signin without user consent when app is already authorized

I have a Next application that let's the user signup using their discord account. The signup part is working. Now I am wondering how can I let the user sign-in using the discord account without going through the authorize part again, if they had already done the signup.
In the /pages/api/auth/[...nextauth].ts file
export default (req: NextApiRequest, res: NextApiResponse<any>) => {
if (req.query.firstName && req.query.lastName) {
firstName = req.query.firstName;
lastName = req.query.lastName;
}
return NextAuth(req, res, {
providers: [
DiscordProvider({
clientId: process.env.DISCORD_CLIENT_ID,
clientSecret: process.env.DISCORD_CLIENT_SECRET,
authorization: { params: { scope: DISCORD_SCOPES } },
}),
],
session: { strategy: "jwt" },
callbacks: {
async session({ session, token, user }) {
session.jwt = token.jwt;
session.id = token.id;
return session;
},
async jwt({ token, user, account }) {
}
}
});
}
Using above logic I am saving the user data to strapi after signup.
How to let the user sign-in with discord without getting a new access token and going through authorization
If the user already authorized your app to log in. You can use the following in your front-end client.
import { signIn } from "next-auth/react";
signIn("discord", { callbackUrl: '', redirect: true }, { prompt: "none" });
References:
https://next-auth.js.org/getting-started/client#additional-parameters

Google OAuth Refresh Tokens not returning Valid Access Tokens

I have a Firebase application that authenticates a user and returns an access token that I can then use to access the Google Calendar and Sheets API. I also save the refreshToken. Sample code for authenticated token:
firebase
.signInWithGoogle()
.then(async (socialAuthUser) => {
let accessToken = socialAuthUser.credential.accessToken // token to access Google Sheets API
let refreshToken = socialAuthUser.user.refreshToken
this.setState({accessToken, refreshToken})
})
After 1 hour, the accessToken expires. Firebase auth provides a refresh token on the user object after sign-in
I use that refresh token to re-authenticate and get a new access_token by posting to:
https://securetoken.googleapis.com/v1/token?key=firebaseAppAPIKey
That new access token does not work for Google APIs anymore, and it doesn't have the authorized scopes anymore. I also try sending it to
https://www.googleapis.com/oauth2/v1/tokeninfo?access_token="refreshToken"
It gives me the error "Invalid token". When I use the original token from firebase, it works just fine.
Anyone else encountering a similar issue? I haven't figured out a way to refresh the original access token with the correct access scopes without making the user sign-out and sign-in again.
Thanks!
I was finally able to solve it after many attempts.
Posted detailed solution on Medium: https://inaguirre.medium.com/reusing-access-tokens-in-firebase-with-react-and-node-3fde1d48cbd3
On the client, I used React with the Firebase library, and on the server I used Node.js with the packages google-apis and the firebase-admin skd package linked to the same Firebase project.
Steps:
(CLIENT) Send a request to the server to generate an authentication link
(SERVER) Generate Auth Link and send it back to the client using the getAuthLink() from googleapis. Sign in with Google and handle the redirect.
(SERVER) On the redirect route, use the code from Google on the query string to authenticate the user and get his user credentials. Use these credentials to check if the user is registered on Firebase.
(SERVER) If the user is registered, get the access and refresh tokens using the oauth2.getTokens(code), update refresh token on the user profile in the database. If the user is not registered, create a new user with firebase.createUser(), also create the user profile on the database with the refresh token.
(SERVER) Use firebase.createCustomToken(userId) to send an id_token back to client and authenticate.
(SERVER) Use a res.redirect({access_token, referesh_token, id_token}) to send credentials back to client.
(CLIENT) On the client, use the signInWithCustomToken(id_token) to authenticate, also restructure the query to obtain access_token and refresh_token to send API calls.
(CLIENT) Set an expiration date for the access token. On each request, check if the current date is higher than the expiration date. If it is, request a new token to https://www.googleapis.com/oauth2/v4/token with the refresh token. Otherwise use the access_token stored.
Most stuff happens when handling the Google Redirect after authentication. Here's an example of handling auth and tokens on the backend:
const router = require("express").Router();
const { google } = require("googleapis");
const { initializeApp, cert } = require("firebase-admin/app");
const { getAuth } = require("firebase-admin/auth");
const { getDatabase } = require("firebase-admin/database");
const serviceAccount = require("../google-credentials.json");
const fetch = require("node-fetch");
initializeApp({
credential: cert(serviceAccount),
databaseURL: "YOUR_DB_URL",
});
const db = getDatabase();
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
"http://localhost:8080/handleGoogleRedirect"
);
//post to google auth api to generate auth link
router.post("/authLink", (req, res) => {
try {
// generate a url that asks permissions for Blogger and Google Calendar scopes
const scopes = [
"profile",
"email",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/calendar",
];
const url = oauth2Client.generateAuthUrl({
access_type: "offline",
scope: scopes,
// force access
prompt: "consent",
});
res.json({ authLink: url });
} catch (error) {
res.json({ error: error.message });
}
});
router.get("/handleGoogleRedirect", async (req, res) => {
console.log("google.js 39 | handling redirect", req.query.code);
// handle user login
try {
const { tokens } = await oauth2Client.getToken(req.query.code);
oauth2Client.setCredentials(tokens);
// get google user profile info
const oauth2 = google.oauth2({
version: "v2",
auth: oauth2Client,
});
const googleUserInfo = await oauth2.userinfo.get();
console.log("google.js 72 | credentials", tokens);
const userRecord = await checkForUserRecord(googleUserInfo.data.email);
if (userRecord === "auth/user-not-found") {
const userRecord = await createNewUser(
googleUserInfo.data,
tokens.refresh_token
);
const customToken = await getAuth().createCustomToken(userRecord.uid);
res.redirect(
`http://localhost:3000/home?id_token=${customToken}&accessToken=${tokens.access_token}&userId=${userRecord.uid}`
);
} else {
const customToken = await getAuth().createCustomToken(userRecord.uid);
await addRefreshTokenToUserInDatabase(userRecord, tokens);
res.redirect(
`http://localhost:3000/home?id_token=${customToken}&accessToken=${tokens.access_token}&userId=${userRecord.uid}`
);
}
} catch (error) {
res.json({ error: error.message });
}
});
const checkForUserRecord = async (email) => {
try {
const userRecord = await getAuth().getUserByEmail(email);
console.log("google.js 35 | userRecord", userRecord.displayName);
return userRecord;
} catch (error) {
return error.code;
}
};
const createNewUser = async (googleUserInfo, refreshToken) => {
console.log(
"google.js 65 | creating new user",
googleUserInfo.email,
refreshToken
);
try {
const userRecord = await getAuth().createUser({
email: googleUserInfo.email,
displayName: googleUserInfo.name,
providerToLink: "google.com",
});
console.log("google.js 72 | user record created", userRecord.uid);
await db.ref(`users/${userRecord.uid}`).set({
email: googleUserInfo.email,
displayName: googleUserInfo.name,
provider: "google",
refresh_token: refreshToken,
});
return userRecord;
} catch (error) {
return error.code;
}
};
const addRefreshTokenToUserInDatabase = async (userRecord, tokens) => {
console.log(
"google.js 144 | adding refresh token to user in database",
userRecord.uid,
tokens
);
try {
const addRefreshTokenToUser = await db
.ref(`users/${userRecord.uid}`)
.update({
refresh_token: tokens.refresh_token,
});
console.log("google.js 55 | addRefreshTokenToUser", tokens);
return addRefreshTokenToUser;
} catch (error) {
console.log("google.js 158 | error", error);
return error.code;
}
};
router.post("/getNewAccessToken", async (req, res) => {
console.log("google.js 153 | refreshtoken", req.body.refresh_token);
// get new access token
try {
const request = await fetch("https://www.googleapis.com/oauth2/v4/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
refresh_token: req.body.refresh_token,
grant_type: "refresh_token",
}),
});
const data = await request.json();
console.log("google.js 160 | data", data);
res.json({
token: data.access_token,
});
} catch (error) {
console.log("google.js 155 | error", error);
res.json({ error: error.message });
}
});
module.exports = router;
For anyone who comes across this now, there is a much easier way at this point.
I was able to solve this by implementing a blocking function that simply saved the refreshToken and exiry date to firestore. You can then query this from your frontend to get the tokens there as well.
Be sure to enable the refreshToken in the firebase settings, otherwise the blocking function won't have access to it.
https://firebase.google.com/docs/auth/extend-with-blocking-functions
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import {
AuthEventContext,
AuthUserRecord,
} from "firebase-functions/lib/common/providers/identity";
admin.initializeApp();
exports.beforeSignIn = functions.auth
.user()
.beforeSignIn((user: AuthUserRecord, context: AuthEventContext) => {
// If the user is created by Yahoo, save the access token and refresh token
if (context.credential?.providerId === "yahoo.com") {
const db = admin.firestore();
const uid = user.uid;
const data = {
accessToken: context.credential.accessToken,
refreshToken: context.credential.refreshToken,
tokenExpirationTime: context.credential.expirationTime,
};
// set will add or overwrite the data
db.collection("users").doc(uid).set(data);
}
});

How do I properly keep user logged in, in nextjs

I am storing the token in a http only cookie and in redux and I am protecting my routes via getServerSideProps by checking req.cookies.token and if there is no token I redirect to login, the problem is if the user refreshes the page he can visit the protected routes because the token is still there, but to get the token on the client side I have to send a request to api/checkAuth which will return the data (token) in the cookie, this request I am sending from _app.js so I don't have to do in each page individually ( otherwise I could just sent the token from getServerSideProps to the page as props) but the problem is this request sent from _app.js takes time to finish while the user is allowed to visit the page and when I have request inside useEffect my token is not available immediately so I have to use token as dependency in the useEffect array and only send request if token exists, but this solution doesn't seem good to me, what is the correct way to do it?
Here is the way I am guarding routes:
const withAuth = (GetServerSidePropsFunction) => async (ctx) => {
const token = ctx.req.cookies?.token || null;
if (!token) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
return await GetServerSidePropsFunction(ctx);
};
export default withAuth;
And this is the request in _app.tsx that is being sent to get the token:
useEffect(() => {
let token = (store as any).user?.token;
if (!token) {
fetch("/api/checkAuth", {
method: "POST",
body: "",
})
.then((res) => {
return res.json();
})
.then((cookieData) => {
if (cookieData.token)
store.dispatch(setUser(token: cookieData.token }));
});
}
}, []);

Resources