Next-Auth with Provider.Credentials: How to implement when API is already returning a JWT Token? - jwt

I have a NextJS page where I try to implement Next-Auth.
I use credentials to login to my Rails API.
My API is returning (already) a JWT-Token. (so NextAuth must not create it)
How to implement the Provider.Credentials for [...nextauth].js in that case?
Flow "Diagram"
Next request ---> Next API (with Next-Auth) ---> Rails API (returning Token)
At the momemt I have these options:
providers: [
CredentialsProvider({
name: 'Email',
credentials: {
email: { label: "Email", type: "email", placeholder: "meine.email#domain.com" },
password: { label: "Passwort", type: "password" }
},
async authorize(credentials) {
// The 'url' is pointing to a Rails API endpoint which returns a JWT Token
const url = `${process.env.NEXT_PUBLIC_API_URL}/auth/login`;
const res = await fetch(url, {
method: 'POST',
body: JSON.stringify(credentials),
headers: {
"Content-Type": "application/json" }
})
const user = await res.json()
// If no error and we have user data, return it
if (res.ok && user) {
// I SAW EXAMPLES RETURNING {"email": "blah#tst.com"}
return user // MY CONTENT {token: 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0LCJyb2xl…0.OAGiwjj9O_NsH02lIjA2D4HYZkmTQ3_SqtKcVgaIul0'}
}
// Return null if user data could not be retrieved
return null
}
})
]
}
A session_token is set in the browser, but that content is something (random?) what I dont have set. Where does this content come from if not from my token?
My Rails API Token Content:
{
"user_id": 4,
"roles": [
"user"
],
"exp": 1631096219
}
Next-Auth API Token Content:
{
"iat": 1631009819,
"exp": 1633601819
}
Do I have to decode my API token and reassamble that within the Provider.Credentials function?
I implement Next-Auth to provide more Authentications like Twitter and Co, but as well to make use of "useSession" instead of building everything of my own (Wont reinventing the wheel).

Add Callbacks.
export default NextAuth({
providers: [
CredentialsProvider({
name: "Email",
credentials: {
email: {
label: "Email",
type: "email",
placeholder: "meine.email#domain.com",
},
password: { label: "Passwort", type: "password" },
},
async authorize(credentials) {
// The 'url' is pointing to a Rails API endpoint which returns a JWT Token
const url = `${process.env.NEXT_PUBLIC_API_URL}/auth/login`;
const res = await fetch(url, {
method: "POST",
body: JSON.stringify(credentials),
headers: {
"Content-Type": "application/json",
},
});
const user = await res.json();
// If no error and we have user data, return it
if (res.ok && user) {
// I SAW EXAMPLES RETURNING {"email": "blah#tst.com"}
return user; // MY CONTENT {token: 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0LCJyb2xl…0.OAGiwjj9O_NsH02lIjA2D4HYZkmTQ3_SqtKcVgaIul0'}
}
// Return null if user data could not be retrieved
return null;
},
}),
],
callbacks: {
async jwt({ token, user, account, isNewUser }) {// This user return by provider {} as you mentioned above MY CONTENT {token:}
if (user) {
if (user.token) {
token = { accessToken: user.token };
}
}
return token;
},
// That token store in session
async session({ session, token }) { // this token return above jwt()
session.accessToken = token.accessToken;
//if you want to add user details info
session.user = { name: "name", email: "email" };//this user info get via API call or decode token. Anything you want you can add
return session;
},
},
});

So the user that you return from the credentials authorize function will get stored in the JWT (JSON Web Token - which is passed around with all authenticated requests) which is a hashed/encoded (not encrypted by default!) version of the object.
You can use https://jwt.io to debug and see whats in your JWT. You paste into that site's debugger field your eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0LCJyb2xl... value and it'll show you the { iat: 49835898: sub: 47897: email: 'abc#xyz.com' }, etc. value.
If your rails API is returning a JWT to you in that fetch call in the authorize function, you can use a library like jwt-decode to decode it right there, and append any values from it which you would like to "persist" in your application to the user object you're returning from the authorize function in next-auth.

Related

next-auth question about credentials provider

next-auth credentials provider only has methods for signIn and signOut, which has led me to think I might be missing something about the purpose of the credentials provider in general.
shouldn't there also be a log in method?
The way it is, in order to do login and signup separately, I can only use signIn for both.
That would almost be ok because I could conditionally check if a user has an account inside of the CredentialsProvider method, and then login or signup conditionally.
The problem is that then the login form also works as a sign up form if the user were to to enter the wrong username for instance... it would just see that account doesn't exist and sign them up.
Obviously that is very bad.
Here is the CredentialsProvider method that gets called when you use 'signIn'
export default CredentialsProvider({
name: '',
credentials: {
email: { label: 'Username', type: 'text', placeholder: 'email' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials, req) {
if (!credentials?.email || !credentials?.password) {
throw new Error('Invalid Credentials')
}
//conditionally query or signup mutation here depending on if the credentials exists
const signIn = UserOperations.Mutations.signIn
await client.query<SignUpResponse, SignUpInput>({
query: signIn,
variables: {
email: credentials.email,
password: credentials.password
}
})
const signUp = UserOperations.Mutations.signUp
const response = await client.mutate<SignUpResponse, SignUpInput>({
mutation: signUp,
variables: {
email: credentials.email,
password: credentials.password
}
})
const { data } = response
const user = data?.signUp
if (user) {
return user
}
return null
}
})
And you then you call it from your login component or sign up component like this:
await signIn('credentials', {
redirect: false,
email,
password,
callbackUrl: `${window.location.origin}`
})
But I don't see any way to distinguish where it's being called from. Isn't the point of credentials to be able to have login and sign up separate?
I figured it out. The idea is to use CredentialsProvider twice, once for signup and once for login.
You can specify which provider gets called with the id property in the provider.
const signUp = CredentialsProvider({
id: 'sign-up-provider',
credentials: {
email: { label: 'Username', type: 'text', placeholder: 'email' },
password: { label: 'Password', type: 'password' }
},
//authorize will be called whether this is signUp or signIn
async authorize(credentials, req) {
if (!credentials?.email || !credentials?.password) {
throw new Error('Invalid Credentials')
}
const signUp = UserOperations.Mutations.signUp
const response = await client.mutate<SignUpResponse, SignUpInput>({
mutation: signUp,
variables: {
email: credentials.email,
password: credentials.password
}
})
const { data } = response
const user = data?.signUp
if (user) {
return user
}
return null
}
})
export default signUp
and then instead of passing in 'credentials' to signIn you can pass in the custom id.
await signUp('sign-up-provider', {
redirect: false,
email,
password,
callbackUrl: `${window.location.origin}`
})
Do the same for a login provider, and then add both providers in the nextAuth providers array.

nextauth v4 credentials provider, adding the raw token to the session

I am new to nextauth credentials provider, and I have been following different tutorials on youtube and searching for answers here.
I have a web application using next.js and in it I have a bunch of rest apis to get data from mongodb. I have secured the api by accessing the token. I have used Postman to test the apis, and they work when I pass the raw token to in the Authorization header.
I need to get the raw token into the session object for the session call back in next-auth, so I then can call the apis from client side pages.
Any help would be appreciated.
In [...nextauth].js:
export default NextAuth({
providers: [
// Google Provider
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET
}),
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET
}),
CredentialsProvider({
id: "credentials",
name: "Credentials",
async authorize(credentials, req) {
console.log("In Authorization");
connectMongo().catch((error) => {
error: "Connection Failed...!";
});
// check user existance
const user = await Users.findOne({ email: credentials.email });
if (!user) {
throw new Error("No user found with this email");
}
// compare()
const checkPassword = await compare(
credentials.password,
user.password
);
// incorrect password
if (!checkPassword || user.email !== credentials.email) {
throw new Error("Email or Password don't match");
}
// check if user is enabled
if (user.active === AccountStatus.DISABLED) {
throw new Error(
"Account has been disabled. Please contact support to re-enable your account"
);
}
// Value returned will go into token property
//console.log("Returnng User Object", user);
return user;
}
})
],
session: {
strategy: "jwt",
maxAge: 60 * 60 * 24
},
callbacks: {
async jwt({ token, user, account, profile, isNewUser }) {
if (user) token.user = user;
if (account) token.accessToken = account.access_token;
return token;
},
async session({ session, token, user, account }) {
// Send properties to the client, like an access_token from a provider.
const { password, ...tokenPwdRemoved } = token.user;
session.user = tokenPwdRemoved;
return session;
}
},
pages: {
signIn: "/login"
}
});
Take a look at the Session callback:
callbacks: {
async session({ session, token, user }) {
// Send properties to the client, like an access_token from a provider.
session.accessToken = token.accessToken
return session
}
}
Keep in mind security concerns relating to the token and session.
Session callback
The session callback is called whenever a session is checked. By
default, only a subset of the token is returned for increased
security. If you want to make something available you added to the
token through the jwt() callback, you have to explicitly forward it
here to make it available to the client.

Paypal Order API capture payment in Angular and NestJS

My stack is NestJS and Angular12, I am using the OrderAPI v2.
I succesfully implemented the order flow using an SDK button, but, since I have several payment systems that are activated by a single button "Pay now", I need to avoid SDK in my front end. Follows the methods I use to create and capture payments, and they works with the SDK button.
async createOrder(value: number): Promise<any> {
const accessToken = await this.generateAccessToken();
const url = this.baseUrl+`/v2/checkout/orders`;
const body = {
intent: "CAPTURE",
return_url: process.env.CLIENT+"/success",
purchase_units: [
{
amount: {
currency_code: "EUR",
value: value.toFixed(2)
}
}
]
}
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`
}
const obs = this.httpService.post(url, JSON.stringify(body),{headers: headers});
const response = await firstValueFrom(obs);
return response.data;
}
async capturePayment(order: CreateOrderDto, orderId: string): Promise<any> {
const accessToken = await this.generateAccessToken();
const url = this.baseUrl+`/v2/checkout/orders/${orderId}/capture`;
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`
}
const obs = this.httpService.post(
url,
{},
{
headers: headers
}
)
const response = await firstValueFrom(obs);
if (response.data.success) await this.orderService.createOrder(order)
return response.data;
}
When calling the createOrder() function I return the url of the approvation, and in my Front-end I do the redirect to the approve page of Paypal. The problem is that when approving the transaction on the approve url the user is shown a infinite loading page.
Is there something I am missing?

finding the user while assigning new access token

I have a website where when user logsIn, they are assigned an access and a refresh token. When the access token is expried, a request to the server is made and checks if the refresh token is present in the global array in the database. If it is, a new access token is assigned to the user.
But I wanted to ask if should also check for the user by the information given by the refresh token when it is decoded. Or it is not necessary.
Please suggest me good practice and also tell me if something is wrong with my process.
routes.post("/newAccessToken", async (req, res) => {
const token = req.headers.cookie?.split("=")[1];
try {
const existingToken = await refreshTokens.findOne({
tokens: { $in: [token] },
});
if (existingToken) {
const email = await jwt.verify(token, process.env.RefreshTokenSecret);
if (email) {
const user = await userSchema.findOne({ email });
if (user) {
const newAccessToken = await jwt.sign(
{ email },
process.env.AccessTokenSecret
);
res.json({ newAccessToken });
}
} else res.json({ message: "token is invalid" });
} else res.json({ message: "No token found" });
} catch (error) {
console.log(error);
}
});

How to get data from next auth signIn Google provider in custom signIn page?

I need to get the data from the custom signIn page in order to write a user to the sanity database. But these signIn data is only obtained in [...nextauth].js file.
Code:
[...nextauth].js
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
export default NextAuth({
// Configure one or more authentication providers
providers: [
GoogleProvider({
clientId: "xxxxxx",
clientSecret: "xxxxx",
}),
// ...add more providers here
],
secret: "something",
pages: {
signIn: '/auth/signin',
},
callbacks: {
async session({ session, token, user }) {
session.user.username = session.user.name
.split(' ')
.join('')
.toLocaleLowerCase()
session.user.uid = token.sub
return session
},
},
})
And these session data can be used inside components using useSession from next-auth.
But while trying to get the data to my custom signIn page, session is undefined.
import { getProviders, signIn as signIntoProvider } from "next-auth/react";
import { sanityClient } from "../../sanity";
import { useSession } from 'next-auth/react';
function signIn({users}) {
const { data: session } = useSession()
const onSubmit = (data) => {
fetch("/api/createUser", {
method: "POST",
body: JSON.stringify(data),
})
.then((resp) => {
console.log(resp);
})
.catch((err) => {
console.log(err);
});
};
const checkIfUserThere = async () => {
let newUser = session.user.email;
for (const data of users) {
if (data.email === newUser) {
return true;
}
}
return false;
};
useEffect(() => {
(async () => {
const userExists = await checkIfUserThere();
if(userExists === false ){
onSubmit(session.user); //write to sanity database
}
})();
}, []);
return (
<>
<button
className="rounded-lg bg-blue-500 p-3 text-white"
onClick={() =>
signIntoProvider("google", { callbackUrl: "/" })
}
>
Sign in with Google
</button>
</>
);
}
The above code is for a custom signIn page.
What is expected :
Once the user clicks the sign-in with the Google button, the session data must be added to the sanity database. But in my case, session here is undefined.
A simple way to do this is to write the logic inside the [...nextAuth].js file.
To solve the task of popularizing a document in Sanity from a Google authentication, you must first establish a connection to your Sanity project. Note that this import comes from the 'npm i #sanity/client' package or 'yarn add #sanity/client' and is not a reference to the configuration located in the sanity.js file. To do this, you can import the #sanity/client library and set up a configuration to connect to your project:
import sanityClient from "#sanity/client";
const config = {
dataset: "DATASET_NAME",
projectId: "PROJECT_ID",
useCdn: 'CDN'
token: "YOUR_TOKEN_SANITY",
};
export const client = sanityClient(config);
After setting up authentication with Google, you must set up a callback to run every time a user authenticates. This callback should look in Sanity to see if the user already exists, and if not, create a new user document in Sanity with the authenticated user's information:
const populateSanityUser = async (user) => {
const sanityUser = await client.fetch(
`*[_type == "users" && email == "${user.email}"]{ //check if the email exists
email
}`
);
if (sanityUser.length > 0) { //if exists
return sanityUser;
} else { //if not, create a new user with Google user session data
try {
await client.create({
_type: "user",
name: user.name,
email: user.email,
urlImage: user.image,
... // another field in your document
});
return user;
} catch (error) {
return error;
}
}
};
export default NextAuth({
...authOptions,
callbacks: {
async signIn(user) {
const isAllowedToSignIn = true; //optional
if (isAllowedToSignIn) {
const sanityUser = await populateSanityUser(user.user);
return sanityUser;
} else {
return false;
}
},
},
});
Important: Make sure that when you pass the user coming from NextAuth callback function you use user.user, as it comes nested with more data.
More information about callbacks in NextAuth here: https://next-auth.js.org/configuration/callbacks