NextAuth signIn pass parameter to callback - next.js

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

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

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

How can I built a HOC for guarding routes if I am using httpOnly cookie?

I am storing my token in a httpOnly cookie, but when I want to built a HOC for guarding the routes there is no way to access the cookie directly inside a component, I have to do it inside the server side,
I tried to do something like this but it doesn't work:
import Cookie from "cookies";
const withAuth = (Page) => {
Page.getServerSideProps = async ({ req, res }) => {
const cookie = new Cookie(req, res);
const token = cookie.get("token");
if (!token)
return {
redirect: {
permanent: false,
destination: "/login",
},
};
return {
props: {
token,
},
};
};
return Page;
};
export default withAuth;
The getServerSideProps function only works in pages, not components.
The following snippet should help you create a HOC for authentication. This example uses the concepts of closures. I'll call this one withAdministrator.jsx.
// withAdministrator.jsx
export default (GetServerSidePropsFunction) => async (ctx) => {
// 1. Check if there is a token.
const token = ctx.req.cookies?.jwt || null;
// 2. Perform an authorized HTTP GET request to the private API to get user data.
// In here, assume that 'getAuth' is a function to perform authorized GET request using the token value in the 'Authorization' header.
const { data } = await getAuth(`${process.env.PRIVATE_API_URL}/api/v1/users/user`, token);
// 3. If there is no user, or the user is not an admin, then redirect to unauthorized.
if (!data || data.role !== 'admin') {
return {
redirect: {
destination: '/unauthorized',
permanent: false,
},
};
}
// 4. Return via closure: 'GetServerSidePropsFunction'.
return await GetServerSidePropsFunction(ctx);
};
You'll call it like this. Let's say you want to access the /admin route.
export const getServerSideProps = withAdministrator(() => {
return {
props: {},
};
});
const Admin = () => {
return (
<YourComponent />
);
};
You can do anything you want inside the returned function. For example, you might want to fetch data after authenticating the user.
Further reading: Data fetching in Next.js.

Get current users access token from Firebase in React Native

I am trying to get the Firebase authentication access token within a React Native application so that I can authenticate my API calls to a custom server. The Firebase documentation says I should get this token by using auth().currentUser.getIdToken(); however currentUser returns null.
I've tried to use getIdToken() in multiple areas of the application. I know the access token is generated as I can see it in the logs while using expo (user.stsTokenManager.accessToken).
Why is currentUser returning null and how can I get the accessToken?
You need to wrap user.getIdToken() inside of firebase.auth().onAuthStateChanged for user to be available. You can then use jwtToken in your header to authenticate your API calls. You need to import your Firebase configuration file for this to work.
let jwtToken = firebase.auth().onAuthStateChanged(function(user) {
if (user) {
user.getIdToken().then(function(idToken) { // <------ Check this line
alert(idToken); // It shows the Firebase token now
return idToken;
});
}
});
Just putting await before will work too just like this:
await auth().currentUser.getIdToken();
getIdToken returns a promise
firebase.auth()
.signInWithCredential(credential)
.then(async data => {
const jwtToken = await data.user?.getIdToken();
console.log(jwtToken);
})
Hook example
Unfortunately, its not reliable to directly get the token. You first have to listen to the authentication state change event which fires upon initialization since its asynchronous.
import {auth} from '../utils/firebase'
import {useState, useEffect} from 'react'
export default function useToken() {
const [token, setToken] = useState('')
useEffect(() => {
return auth().onAuthStateChanged(user => {
if (user) {
user.getIdToken(true)
.then(latestToken => setToken(latestToken))
.catch(err => console.log(err))
}
})
}, [])
return token
}
then use like so in your functional component
const token = useToken()
useEffect(() => {
if (token) {
// go wild
}
}, [token])

OAuth2 fails to return auth token using simple-oauth2 and Firebase Functions for Spotify Authentication

I have been working on a oauth2 flow for spotify by following this similar tutorial by the Firebase team for Instagram HERE
I am able to submit my credentials and return the user code and state in the url, but when I run the method to submit the code to return an auth token, the auth token that I print to console in the Firebase functions returns: Auth Token Error Not Found. Here's my workflow:
Here's the Spotify docs
FIRST, I have a function to configure my spotifyOAuth:
function spotifyOAuth2Client() {
// Spotify OAuth 2 setup
const credentials = {
client: {
id: functions.config().spotify.clientid,
secret: functions.config().spotify.clientsecret,
},
auth: {
tokenHost: 'https://accounts.spotify.com',
authorizePath: '/authorize'
},
};
return require('simple-oauth2').create(credentials);
}
I use that function in this Firebase function that is called using https://us-central1-<my project string>.cloudfunctions.net/redirect:
exports.redirect = functions.https.onRequest((req, res) => {
const oauth2 = spotifyOAuth2Client();
cookieParser()(req, res, () => {
const state = req.cookies.state || crypto.randomBytes(20).toString('hex');
console.log('Setting verification state:', state);
res.cookie('state', state.toString(), {
maxAge: 3600000,
secure: true,
httpOnly: true,
});
const redirectUri = oauth2.authorizationCode.authorizeURL({
redirect_uri: OAUTH_REDIRECT_URI,
//scope: OAUTH_SCOPES,
state: state,
});
console.log('Redirecting to:', redirectUri);
res.redirect(redirectUri);
});
});
The code above returns a url string with the proper parameters, the following code block is where my code breaks, I have another cloud function that runs after being redirected from the res.redirect(redirectUri) above. And when I try to run the getToken() method, it appears to not return anything because I hit the catch block instead? This is where I observe the Auth Token Error Not Found.
const oauth2 = spotifyOAuth2Client();
try {
return cookieParser()(req, res, async () => {
console.log('Received verification state:', req.cookies.state);
console.log('Received state:', req.query.state);
if (!req.cookies.state) {
throw new Error('State cookie not set or expired. Maybe you took too long to authorize. Please try again.');
} else if (req.cookies.state !== req.query.state) {
throw new Error('State validation failed');
}
console.log('Received auth code:', req.query.code);
console.log(OAUTH_REDIRECT_URI);
// Get the access token object (the authorization code is given from the previous step).
const tokenConfig = {
code: req.query.code,
redirect_uri: 'http://localhost:8100/popup'
};
// Save the access token
try {
const result = await oauth2.authorizationCode.getToken(tokenConfig)
const accessToken = oauth2.accessToken.create(result);
console.log('inside try');
console.log(result);
console.log(accessToken);
} catch (error) {
console.log('Access Token Error', error.message);
}
I've double checked my spotify client/secret credentials in the config, what is going wrong with this OAuth2 flow?
Resolved my issue, I was not using the correct endpoints:
const credentials = {
client: {
id: functions.config().spotify.clientid,
secret: functions.config().spotify.clientsecret,
},
auth: {
tokenHost: 'https://accounts.spotify.com',
authorizePath: '/authorize',
tokenPath: '/api/token'
},
};

Resources