google OAuthCallbackError signin?error=OAuthCallback - oauth-2.0

I am adding next-auth with google provider to my next.js project in developement mode, but I am geeting this error on the browser URL : http://localhost:3000/api/auth/signin?error=OAuthCallback
and Try signing in with a different account message on the screen.
and
after signing in with signIn('google') everytime the following error appears in my terminal in vscode:
[next-auth][warn][NEXTAUTH_URL]
https://next-auth.js.org/warnings#nextauth_url
data unauthenticated
[next-auth][error][OAUTH_CALLBACK_ERROR]
https://next-auth.js.org/errors#oauth_callback_error JWT expired, now 1675094480, exp 1675094221 {
error: RPError: JWT expired, now 1675094480, exp 1675094221
at Client.validateJWT (D:\React_prj\ChatApp\frontend\node_modules\openid-client\lib\client.js:956:15)
at Client.validateIdToken (D:\React_prj\ChatApp\frontend\node_modules\openid-client\lib\client.js:745:60)
at Client.callback (D:\React_prj\ChatApp\frontend\node_modules\openid-client\lib\client.js:488:18)
at runMicrotasks (<anonymous>)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async oAuthCallback (D:\React_prj\ChatApp\frontend\node_modules\next-auth\core\lib\oauth\callback.js:129:16)
at async Object.callback (D:\React_prj\ChatApp\frontend\node_modules\next-auth\core\routes\callback.js:52:11)
at async AuthHandler (D:\React_prj\ChatApp\frontend\node_modules\next-auth\core\index.js:201:28)
at async NextAuthHandler (D:\React_prj\ChatApp\frontend\node_modules\next-auth\next\index.js:24:19)
at async D:\React_prj\ChatApp\frontend\node_modules\next-auth\next\index.js:60:32
at async Object.apiResolver (D:\React_prj\ChatApp\frontend\node_modules\next\dist\server\api-utils\node.js:372:9) at async DevServer.runApi (D:\React_prj\ChatApp\frontend\node_modules\next\dist\server\next-server.js:488:9)
at async Object.fn (D:\React_prj\ChatApp\frontend\node_modules\next\dist\server\next-server.js:751:37)
at async Router.execute (D:\React_prj\ChatApp\frontend\node_modules\next\dist\server\router.js:253:36)
at async DevServer.run (D:\React_prj\ChatApp\frontend\node_modules\next\dist\server\base-server.js:384:29)
at async DevServer.run (D:\React_prj\ChatApp\frontend\node_modules\next\dist\server\dev\next-dev-server.js:743:20)
at async DevServer.handleRequest (D:\React_prj\ChatApp\frontend\node_modules\next\dist\server\base-server.js:322:20) {
name: 'OAuthCallbackError',
code: undefined
},
providerId: 'google',
message: 'JWT expired, now 1675094480, exp 1675094221'
}
everytime on the browser I see the following image (Try signing in with a different account.)even trying with another account I get this image in the browser
enter image description here
also, I use prisma as the adapter as well
the following are my codes:
[...nextauth].ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { PrismaAdapter } from '#next-auth/prisma-adapter';
import prisma from '../../../lib/prismadb';
export default NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
],
});
prismadb.ts
import { PrismaClient } from '#prisma/client';
declare global {
var prisma: PrismaClient | undefined;
}
const client = globalThis.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalThis.prisma = client;
export default client;
schema.prisma
datasource db {
provider = "mongodb"
url = env("MONGODB_URI")
}
generator client {
provider = "prisma-client-js"
}
model Account {
id String #id #default(auto()) #map("_id") #db.ObjectId
userId String
type String
provider String
providerAccountId String
refresh_token String? #db.String
access_token String? #db.String
expires_at Int?
token_type String?
scope String?
id_token String? #db.String
session_state String?
user User #relation(fields: [userId], references: [id], onDelete: Cascade)
##unique([provider, providerAccountId])
}
model Session {
id String #id #default(auto()) #map("_id") #db.ObjectId
sessionToken String #unique
userId String
expires DateTime
user User #relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String #id #default(auto()) #map("_id") #db.ObjectId
name String?
email String? #unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
}
model VerificationToken {
id String #id #default(auto()) #map("_id") #db.ObjectId
identifier String
token String #unique
expires DateTime
##unique([identifier, token])
}
and .env.local
GOOGLE_CLIENT_ID=46255473************od0rt849b.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-*************I7SNBuJ6
MONGODB_URI =mongodb://localhost/chatapp
I tried many othersolution provided on the google and other providers but I cannot fix this issue.
please help
thx
I tried to login with next-auth with google as the provider and prisma as the adapter, but I get callback error

Related

React MSAL access token has invalid signature

Setup
msal (in another file. Passed using MsalProvider):
const msalInstance = new PublicClientApplication({
auth: {
clientId: <B2C-Application-ID>,
authority: "https://login.microsoftonline.com/<tenant-directory-id>",
redirectUri: "http://localhost:3000",
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: false,
}
});
Import:
import * as msal from "#azure/msal-browser";
import {EventType, InteractionStatus} from "#azure/msal-browser";
import React, {createContext, FC, useState} from "react";
import {useIsAuthenticated, useMsal} from "#azure/msal-react";
import {AuthenticationContextType} from "../#types/authentication";
import {EndSessionRequest} from "#azure/msal-browser/dist/request/EndSessionRequest";
import jwtDecode, {JwtPayload} from "jwt-decode";
Variables:
const {instance, accounts, inProgress} = useMsal();
const isAuthenticated = useIsAuthenticated();
const [token, setToken] = useState<string | null>(null);
Login:
function loginRedirect() {
instance.loginRedirect({
scopes: ["User.Read"],
prompt: "select_account"
});
}
Acquire token:
function getToken(): string | null {
if (token) {
const decodedJwt = jwtDecode<JwtPayload>(token);
if (decodedJwt.exp && decodedJwt.exp * 1000 > Date.now()) {
return token; // Token is still valid
}
}
// If token is not available or not valid anymore, acquire a new one
if (instance.getActiveAccount() && inProgress === InteractionStatus.None) {
const accessTokenRequest = {
scopes: ["User.Read"],
account: accounts[0]
}
instance.acquireTokenSilent(accessTokenRequest)
.then(response => {
console.log(`access token: ${response.accessToken}`);
console.log(`id token: ${response.idToken}`);
setToken(response.accessToken);
return response.accessToken;
})
.catch(err => {
if (err instanceof msal.InteractionRequiredAuthError) {
return instance.acquireTokenPopup(loginRequest)
.then(response => {
setToken(response.accessToken);
return response.accessToken;
})
.catch(err => {
console.log(err);
})
} else {
console.log(err);
}
})
} else {
console.error("No account logged in to acquire token");
}
return null;
}
Problem
I acquire two tokens (ID and access) from msal (see console logs). The ID token is being validated successfully (on my API and jwt.io) but my access token is not (neither on my API nor jwt.io). Referring to this microsoft documentation I should use the access token to validate against an API.
As far as I can see, jwt.io does fetch the public key correctly from https://sts.windows.net/<tenant-directory-id>/discovery/v2.0/keys. This means this solution is either outdated, or doesn't solve my problem. To go sure I also tried to copy&paste the public key, which didn't work either.
I also found this solution which didn't work for me either. Changing the scopes leads to an endless login loop.
Versions:
"#azure/msal-browser": "^2.28.3",
"#azure/msal-react": "^1.4.7",
"jwt-decode": "^3.1.2",
1. Scope
For requesting B2C access tokens you have to specify a valid scope. These are also set in Azure (Azure AD B2C -> App registrations -> your application -> Manage -> API permissions). There you have to specify a scope. While acquiring the tokens you have to specify these scopes like this:
const accessTokenRequest = {
scopes: ["https://<tenant-name>.onmicrosoft.com/<app-id>/<scope>"],
}
await instance.acquireTokenSilent(accessTokenRequest)
.then(response => {
setIdToken(response.idToken);
setAccessToken(response.accessToken);
})
.catch(async err => {
if (err instanceof msal.InteractionRequiredAuthError) {
await instance.acquireTokenPopup(accessTokenRequest)
.then(response => {
setIdToken(response.idToken);
setAccessToken(response.accessToken);
})
.catch(err => {
console.log(err);
})
} else {
console.log(err);
}
})
tenant-name you can find this in the Application ID URI
app-id is your Application (client) ID
your-scope could be something like Subscriptions.Read
A full example for a scope could be:
https://mydemo.onmicrosoft.com/12345678-0000-0000-0000-000000000000/Subscriptions.Read
2. Invalid token version
For me the problem was 1. Scope but maybe this does not solve the problem for others. Here is something else to try:
Following this article, the sts url is used vor the v1 endpoint. The documentation claims:
The endpoint used, v1.0 or v2.0, is chosen by the client and only impacts the version of id_tokens. Possible values for accesstokenAcceptedVersion are 1, 2, or null. If the value is null, this parameter defaults to 1, which corresponds to the v1.0 endpoint.
This means that the used endpoint (v2.0 in my case) affected only the id-token, which made it validate successfully. The access token was still v1 thus with no validated signature.
Solution
To change the version, accessTokenAcceptedVersion needs to be set to 2 inside the Manifest. It is located at portal.azure.com -> Azure AD B2C -> App registrations -> your application -> Manage -> Manifest:
{
...
"accessTokenAcceptedVersion": 2,
...
}
Save the changes and wait. For me it took several hours to wait for the change to be applied. And I had to apply solution 1. Scope as well. After that, the iss of new tokens should be https://login.microsoftonline.com/<tenant-directory-id>/v2.0 instead of the sts-uri

get access_token from next_auth to use it with googleapis

How to get access_token from next_auth to use it with googleapis,
lets say i am creating a crud app that store the data in google drive, I am using nextjs and next-auth for OAuth implementation for google. i found this blog so i implemented it. but it logs undefined.
src/pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import jwt from 'next-auth/jwt'
const secret = process.env.SECRET
export default NextAuth({
// Configure one or more authentication providers
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
authorization:{
params:{
scope:"openid https://www.googleapis.com/auth/drive.file"
}
}
}),
],
secret: process.env.SECRET,
callbacks: {
jwt: ({token, user, account, profile, isNewUser})=> {
console.log({token,user,account,profile})
if (account?.accessToken) {
token.accessToken = account.accessToken;
}
return token;
},
session: async ({session, user,token}) => {
session.user = user;
session.token = token;
return session
}
},
});
and I created a route with nextjs to get the access token
import {getToken,decode} from 'next-auth/jwt'
const handler = async(req, res)=> {
const secret = process.env.SECRET
const token = await getToken({ req, secret });
const accessToken = token.accessToken;
console.log(accessToken)
}
export default handler
any help would be great. thanks
the google's token is stored in account.access_token not account.accessToken. so the jwt callback must be
callbacks: {
jwt: ({token, account })=> {
if (account?.access_token) {
token.access_token = account.access_token;
}
return token;
},
},
and it is better not to expose tokens on clients side which I done in session callback. it is insecure.
As stated in the documentation, you must forward any data you want to be available in the token, such is your accessToken value:
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.
So, you just have to add this to your session callback:
session.accessToken = token.accessToken;

Microsoft.Graph.GraphServiceClient allows me to create a user but does not allow me to change password

I have a system comprised of an Angular SPA hosted in Azure and some Azure Functions for the APIs. In an administrative app, I created an application that allows admin users to create new user accounts including specifying a password. These new accounts are able to log into the line of business app that I created as well. There is a requirement where we need to allow the same people who created the account to reset a password. For some reason, the code that I wrote to set the password does not work. It seems odd that a user can create an account, including setting the password, but for some reason the same user can't set the password independent of creating the user account. FYI, there are no emails, these are user accounts, so giving the ability to request a password reset is not an option.
Here is the error:
{
"error": {
"code": "Authorization_RequestDenied",
"message": "Access to change password operation is denied.",
"innerError": {
"date": "2021-01-19T21:58:35",
"request-id": "a1bc5b50-83e9-47ae-97c7-bda4f524fa0e",
"client-request-id": "a1bc5b50-83e9-47ae-97c7-bda4f524fa0e"
}
}
}
Here is my code:
//this is the method that works
public async Task<Microsoft.Graph.User> CreateUserAsync(string givenName,
string surname, string displayName, string userPrincipalName, string issuer,
string signInType, string initialPassword, GraphServiceClient graphClient)
{
var user = new Microsoft.Graph.User {
AccountEnabled = true,
GivenName = givenName,
Surname = surname,
DisplayName = displayName,
Identities = new List<ObjectIdentity>() {
new ObjectIdentity {
Issuer = issuer,
IssuerAssignedId = userPrincipalName,
SignInType = signInType
}
},
PasswordProfile = new PasswordProfile {
ForceChangePasswordNextSignIn = false,
Password = initialPassword
}
};
return await graphClient.Users
.Request()
.AddAsync(user);
}
//This one does not work, returns: Access to change password operation is denied.
public async Task<Microsoft.Graph.User> SetPasswordAsync(
string userName, string currentPassword, string newPassword, GraphServiceClient graphClient)
{
await graphClient.Users[userName].ChangePassword(currentPassword, newPassword).Request().PostAsync();
return something here;
}
Seems this is a permission issue here. The function:graphClient.Users[].ChangePassword()is based on reset password rest API,as the official doc indicated, only delegated permission works here and permission :UserAuthenticationMethod.ReadWrite.All is needed.
After I granted this permission to my app:
it works perfectly for me:
Let me know if you have any further questions.

Google Rest API: "Invalid JWT: Failed Audience Check"? I am using audience field API suggests

I am attempting to connect to Google's REST API (for Google Calendars) using a JWT. I have followed the instructions up to and including here. When I send a post request with my JWT (with grant_type and assertion in the body per previous StackOverflow posts), I get an error:
{
"error": "invalid_grant",
"error_description": "Invalid JWT: Failed audience check. The right audience is https://www.googleapis.com/oauth2/v4/token"
}
My code is short so I'll post it in full (sans private details) below. I'm using the jaguar_jwt library and modeled my JWT generation off of their sample code. I have create a service account and I believe followed all instructions up until the aforementioned point in Google's documentation.
import 'package:http/http.dart' as http;
import 'package:jaguar_jwt/jaguar_jwt.dart';
String generateJWT() {
final String privateKey = "-----BEGIN PRIVATE KEY-----.********=\n-----END PRIVATE KEY-----\n";
final claimSet = new JwtClaim(
issuer: 'google-api#**********.iam.gserviceaccount.com',
audience: <String>['https://www.googleapis.com/oauth2/v4/token'],
otherClaims: <String,dynamic>{
"scope":"https://www.googleapis.com/auth/calendar",
"access_type": "offline"
},
maxAge: const Duration(minutes: 60));
String token = issueJwtHS256(claimSet, privateKey);
// print(token);
return token;
}
void sendJWT(String jwt) async {
var client = new http.Client();
try {
String googleUri = "https://www.googleapis.com/oauth2/v4/token";
var requestBody = {'grant_type':'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion':'$jwt',
"access_type": "offline"
};
await client.post(googleUri, body: requestBody)
.then((value) => print(value.body));
} finally {
client.close();
}
}
main(List<String> arguments) {
sendJWT(generateJWT());
}

Why is OAuth2 with Gmail Nodejs Nodemailer producing "Username and Password not accepted" error

OAuth2 is producing "Username and Password not accepted" error when try to send email with Gmail+ Nodejs+Nodemailer
Code - Nodejs - Nodemailer and xoauth2
var nodemailer = require("nodemailer");
var generator = require('xoauth2').createXOAuth2Generator({
user: "", // Your gmail address.
clientId: "",
clientSecret: "",
refreshToken: "",
});
// listen for token updates
// you probably want to store these to a db
generator.on('token', function(token){
console.log('New token for %s: %s', token.user, token.accessToken);
});
// login
var smtpTransport = nodemailer.createTransport({
service: 'gmail',
auth: {
xoauth2: generator
}
});
var mailOptions = {
to: "",
subject: 'Hello ', // Subject line
text: 'Hello world ', // plaintext body
html: '<b>Hello world </b>' // html body
};
smtpTransport.sendMail(mailOptions, function(error, info) {
if (error) {
console.log(error);
} else {
console.log('Message sent: ' + info.response);
}
smtpTransport.close();
});
issues:
I used Google OAuth2 playground to create the tokens, https://developers.google.com/oauthplayground/
It looks to grab a valid accessToken ok, using the refreshToken, (i.e. it prints the new access token on the screen.) No errors until it tries to send the email.
I added the optional accessToken: but got the same error. ( "Username and Password not accepted")
I am not 100% sure about the "username", the docs say it needs a "user" email address - I guess the email of the account that created to token, but is not 100% clear. I have tried several things and none worked.
I have searched the options on the gmail accounts, did not find anything that looks wrong.
Also, when I did this with Java, it needed the google userID rather than the email address, not sure why this is using the email address and the Java is using the UserId.
nodemailer fails with a "compose" scope
The problem was the "scope"
it fails with:
https://www.googleapis.com/auth/gmail.compose
but works ok if I use
https://mail.google.com/
Simply just do the following:
1- Get credentials.json file from here https://developers.google.com/gmail/api/quickstart/nodejs press enable the Gmail API and then choose Desktop app
2- Save this file somewhere along with your credentials file
const fs = require('fs');
const readline = require('readline');
const {google} = require('googleapis');
// If modifying these scopes, delete token.json.
const SCOPES = ['https://mail.google.com'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = 'token.json';
// Load client secrets from a local file.
fs.readFile('credentials.json', (err, content) => {
if(err){
return console.log('Error loading client secret file:', err);
}
// Authorize the client with credentials, then call the Gmail API.
authorize(JSON.parse(content), getAuth);
});
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
* #param {Object} credentials The authorization client credentials.
* #param {function} callback The callback to call with the authorized client.
*/
function authorize(credentials, callback) {
const {client_secret, client_id, redirect_uris} = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
// Check if we have previously stored a token.
fs.readFile(TOKEN_PATH, (err, token) => {
if(err){
return getNewToken(oAuth2Client, callback);
}
oAuth2Client.setCredentials(JSON.parse(token));
callback(oAuth2Client);
});
}
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
* #param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
* #param {getEventsCallback} callback The callback for the authorized client.
*/
function getNewToken(oAuth2Client, callback) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
console.log('Authorize this app by visiting this url:', authUrl);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question('Enter the code from that page here: ', (code) => {
rl.close();
oAuth2Client.getToken(code, (err, token) => {
if (err) return console.error('Error retrieving access token', err);
oAuth2Client.setCredentials(token);
// Store the token to disk for later program executions
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) return console.error(err);
console.log('Token stored to', TOKEN_PATH);
});
callback(oAuth2Client);
});
});
}
function getAuth(auth){
}
3 - Run this file by typing in your terminal: node THIS_FILE.js
4- You'll have token.json file
5- take user information from credentials.json and token.json and fill them in the following function
const nodemailer = require('nodemailer');
const { google } = require("googleapis");
const OAuth2 = google.auth.OAuth2;
const email = 'gmail email'
const clientId = ''
const clientSecret = ''
const refresh = ''
const oauth2Client = new OAuth2(
clientId,
clientSecret,
);
oauth2Client.setCredentials({
refresh_token: refresh
});
const newAccessToken = oauth2Client.getAccessToken()
let transporter = nodemailer.createTransport(
{
service: 'Gmail',
auth: {
type: 'OAuth2',
user: email,
clientId: clientId,
clientSecret: clientSecret,
refreshToken: refresh,
accessToken: newAccessToken
}
},
{
// default message fields
// sender info
from: 'Firstname Lastname <your gmail email>'
}
);
const mailOptions = {
from: email,
to: "",
subject: "Node.js Email with Secure OAuth",
generateTextFromHTML: true,
html: "<b>test</b>"
};
transporter.sendMail(mailOptions, (error, response) => {
error ? console.log(error) : console.log(response);
transporter.close();
});
If your problem is the scopes, here is some help to fix
Tried to add this as an edit to the top answer but it was rejected, don't really know why this is off topic?
See the note here: https://nodemailer.com/smtp/oauth2/#troubleshooting
How to modify the scopes
The scopes are baked into the authorization step when you get your first refresh_token. If you are generating your refresh token via code (for example using the Node.js sample) then the revised scope needs to be set when you request your authUrl.
For the Node.js sample you need to modify SCOPES:
// If modifying these scopes, delete token.json.
-const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly'];
+const SCOPES = ['https://mail.google.com'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
And then the call to oAuth2Client.generateAuthUrl will produce a url that will request authorization from the user to accept full access.
from the Node.js sample:
function getNewToken(oAuth2Client, callback) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});

Resources