I am implementing MSAL authentication for our SPA. I've followed various official and unofficial guides and so far this is my auth implementation:
loginMicrosoft() {
myMSALObj
.loginPopup(msalConfig.loginRequest)
.then((response) => {
console.log(response);
this.username = response.account.userName;
this.account = response.account;
})
.catch((error) => {
console.error(error);
});
},
readEvents() {
this.getTokenPopup(msalConfig.tokenRequest)
.then((response) => {
console.log("silent token!: ", response);
this.callMSGraph(
graphConfig.graphConfig.graphGetCalendarEventsEndpoint,
response.accessToken
);
})
.catch((error) => {
console.error(error);
});
},
createEvent() {
this.getTokenPopup(msalConfig.tokenRequest)
.then((response) => {
this.callMSGraphCreateEvent(
graphConfig.graphConfig.graphCreateCalendarEventEndpoint,
response.accessToken
);
})
.catch((error) => {
console.error(error);
});
},
getTokenPopup(request) {
request.account = this.account;
console.log(request);
return myMSALObj.acquireTokenSilent(request).catch((error) => {
console.warn(
"silent token acquisition fails. acquiring token using popup : ",
error
);
if (error instanceof Msal.InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return myMSALObj
.acquireTokenPopup(request)
.then((tokenResponse) => {
console.log(tokenResponse);
return tokenResponse;
})
.catch((error) => {
console.error(error);
});
} else {
console.warn(error);
}
});
},
async callMSGraph(endpoint, token) {
console.log("request made to Graph API at: " + new Date().toString());
const resp = await axios.post("/api/microsoft/get-events", { endpoint, token });
this.calendarEvents = resp.data.value;
console.log("vaste: ", resp);
},
async callMSGraphCreateEvent(endpoint, token) {
console.log("request made to Graph API at: " + new Date().toString());
const resp = await axios.post("/api/microsoft/create-event", {
endpoint,
token,
});
this.calendarEvents = resp.data.value;
console.log("vaste: ", resp);
},
Everything works as intended, until the access token is reaching expiry.
If AcquireTokenSilent is called 5 minutes before the expiration of after the expiration of the access token, I would expect it to return a new access token, using the hidden refresh token in the MSAL cache. Instead, I get the following error:
silent token acquisition fails. acquiring token using popup :
InteractionRequiredAuthError: Silent authentication was denied. The
user must first sign in and if needed grant the client application
access to the scope 'User.Read Calendars.ReadWrite openid
profile'.
It doesn't seem to be normal to ask for user sign-in every hour, and I cant seem to find any resources on this issue..
Any help is greatly appreciated!
EDIT:
I tried adding offline_access to my token request scopes. My scopes setup is following:
export const loginRequest = {
scopes: ["User.Read", "Calendars.ReadWrite", "offline_access"],
};
export const tokenRequest = {
scopes: ["User.Read", "Calendars.ReadWrite", "offline_access"],
forceRefresh: false, // Set this to "true" to skip a cached token and go to the server to get a new token
};
Now im getting the login popup with the following error every time i try to call the api:
InteractionRequiredAuthError: Silent authentication was denied. The user must first sign in and if needed grant the client application access to the scope 'User.Read Calendars.ReadWrite offline_access openid profile'.
Probably your scopes (in your app registration or in your msal config, depending on where you define your config and if you are using .default scope) do not include the request for the offline_access scope. Please include it, it is required if you want to (auto-) refresh your tokens. If you don't get a new consent prompt after adding this scope (user must agree, i.e. give consent to this), just reset your app consents in azure portal, or consent manually.
Related
I have implemented instagram oauth using passport and passport-instagram package in nodejs.
Below is my code.
require("dotenv").config();
const express = require("express");
const app = express();
const passport = require("passport");
const InstagramStrategy = require("passport-instagram").Strategy;
const ExpressSession = require("express-session");
app.use(passport.initialize());
app.use(
ExpressSession({
resave: false,
saveUninitialized: true,
secret: "mylittlesecret",
})
);
app.use(passport.session());
// Serialize user data to store in the session
passport.serializeUser(function(user, done) {
done(null, user);
});
// Deserialize user data from the session
passport.deserializeUser(function(obj, done) {
done(null, obj);
});
passport.use(
new InstagramStrategy({
clientID: "Instagram_client_id",
clientSecret: "Instagram_client_secret",
callbackURL: "https://d5d0-115-xxx-xx-179.ngrok.io/auth/instagram/callback",
},
function(accessToken, refreshToken, profile, cb) {
process.nextTick(function() {
return cb(null, profile);
});
}
)
);
// Instagram login route
app.get(
"/auth/instagram",
passport.authenticate("instagram", {
scope: [
"pages_show_list",
"user_media",
"user_profile",
"instagram_basic",
"instagram_content_publish",
"instagram_manage_comments",
"instagram_manage_insights",
"pages_read_engagement",
],
})
);
// Instagram callback route
app.get(
"/auth/instagram/callback",
passport.authenticate("instagram", {
failureRedirect: "/error",
}),
(req, res) => {
res.redirect("/profile");
}
);
app.get("/profile", (req, res) => {
res.send("Proile");
});
app.get("/error", (req, res) => {
res.send("error");
});
const PORT = process.env.PORT;
app.get("/", (req, res) => {
res.send("Home");
});
app.listen(PORT, () => {
console.log(`Server is running at ${PORT}`);
});
I have created a consumer facebook app in developers platform and added product of Instagram basic display api , in basic display api valid oauth redirect uri , i have entered : https://d5d0-115-xxx-xx-179.ngrok.io/auth/instagram/callback url which i have declared in my code and
and in deauthroize callback url : https://dd9b-115-xxx-xx-179.ngrok.io/auth/instagram/
I have also added tester insatgram account also
When i hit /auth/instagram route this is taking me to instagram login, after completing all the steps im recieving error like this,
InternalOAuthError: failed to fetch user profile
at /Users/vivekgupta/Desktop/AdosphereServices/instalogin/node_modules/passport-instagram/lib/strategy.js:80:28
at passBackControl (/Users/vivekgupta/Desktop/AdosphereServices/instalogin/node_modules/oauth/lib/oauth2.js:132:9)
at IncomingMessage.<anonymous> (/Users/vivekgupta/Desktop/AdosphereServices/instalogin/node_modules/oauth/lib/oauth2.js:157:7)
at IncomingMessage.emit (node:events:402:35)
at endReadableNT (node:internal/streams/readable:1343:12)
at processTicksAndRejections (node:internal/process/task_queues:83:21)
Please help me with this ,
Thank You In advance.
I am using passport local and google strategies along with express session and storing the session in a mongo database. The front end is Angular, i'm not sure thats relevant but it means my express server is running on (localhost) port 3000 and my angular app is on (localhost) port 4200
Everything works perfectly in the browser. I can login with the local or google strategy, the session is created in the database, the cookie is stored correctly, I can refresh the page and i'm still logged in which is great.
I'm also using Electron to convert my app to a desktop app. The local strategy still works fine (and the session persists correctly) but the google strategy doesn't work for Electron. I can login correctly through google, my passport serializeUser function is called, the session is created in the database and the cookie is stored. However it doesn't look like the deserializeUser function is ever called and when I refresh Electron req.user is empty so the user is not authenticated and I have to login again.
Using google sign in with Electron was a bit of a pain but the flow is:
user presses login button (in angular app)
this sends an IPC message to Electron
my electron app opens a new browser window pointing to the auth/google route and listens for the redirect
the auth/google/redirect route is hit and req.user shows the correct user data
I call req.login and pass the user data
the passport serializeUser function is called and runs successfully
but the deserializeUser function is not called on subsequent requests (whereas it is in the browser) and when I refresh Electron req.user is undefined so I am not authenticated and have to log in again...
I'll post the relevant sections of code below:
Electron Auth Window
export function createAuthWindow2() {
destroyAuthWin();
win = new BrowserWindow({
width: 900,
height: 700,
webPreferences: {
nodeIntegration: false,
enableRemoteModule: false
}
});
console.log('auth-process2, load page: ')
console.log('http://localhost:3000/api/auth/google/desktop')
win.loadURL('http://localhost:3000/api/auth/google/desktop', {userAgent: 'Chrome'})
const {session: {webRequest}} = win.webContents;
ipcMain.on('user', (event, user) => {
console.log('auth process2 user: ', user)
})
const filter = {
urls: [
'http://localhost:3000/api/auth/google/desktop/redirect*'
]
};
webRequest.onBeforeRequest(filter, async ({url}) => {
console.log('auth-process2 onBeforeRequest')
getUser(url)
});
win.on('authenticated', () => {
destroyAuthWin();
});
win.on('closed', () => {
win = null;
});
}
google-desktop route
router.get("/google/desktop", passport.authenticate("google-desktop", {
scope: ["profile", "email"]
})
)
Redirect route
router.get("/google/desktop/redirect", passport.authenticate("google-desktop", { session: false, failureMessage: true }), (req,res) => {
// console.log('auth.ts route, google/desktop/redirect failure messages: ', req.session.messages)
console.log('auth.ts route, google/desktop/redirect req.user: ', req.user)
req.login(req.user, (err) => {
console.log('auth.ts route, google/desktop/redirect req.login err: ', err)
console.log('auth.ts route, google/desktop/redirect req.login cookies: ', req.cookies)
if (err) {
res.status(401).json({ message: err })
}
else {
res.status(200).json({ user: req.user })
}
})
})
Passport serialize and deserialize user functions
// serialize the user to store the user ID in the session
passport.serializeUser((user, done) => {
console.log('passport serializeUser, user: ', user)
return done(null, user._id)
})
// gets user id from session, then get user from database
passport.deserializeUser((id, done) => {
console.log('passport deserializeUser, id: ', id)
User.findById(id, (err, user) => {
if (err) {
console.log('passport deserializeUser error: ', err)
return done(err)
}
console.log('passport deserializeUser success user: ', user)
return done(null, user)
})
})
Express session and passport initialization
app.use(cookieParser())
// Parsers for POST data
app.use(bodyParser.json()) // for parsing application/json
app.use(bodyParser.urlencoded({ extended: false }))
// set up CORS since server (port 3000) and angular application (port 4200) are running on different ports
// app.use(cors())
app.use(cors({origin: [
"http://localhost:4200",
"http://localhost:3000"
], credentials: true}))
// express session middleware
const expressSession = session({
secret: 'random secret', // secret should be randomly generated string pulled from environment variable
resave: false, // option to save/update session whenever user refreshes page, even if no data has changed
// saveUninitialized: true, // create session if user visits site (when set to true), or only when user logs in (when set to false)
store: new MongoStore({ mongooseConnection: mongoose.connection }),
cookie: {
maxAge: 2.628e+9, // one month
secure: false,
httpOnly: false, // to access cookie in javascript/angular we need to set httpOnly to false. But this will not get the socket.io id
sameSite: 'none'
}
})
// install express session middleware
app.use(expressSession)
// Passport
console.log('server/index initializing passport and session')
app.use(passport.initialize())
app.use(passport.session())
so i am trying to set up twitter oauth, either v1 or v2, to my web app. google oauthv2 is working just fine but when i try passport-twitter-oauth2 it says that there isn't any access toekn, and when i try basic oauthv1.1 auth it sends this error message "APIError: You currently have Essential access which includes access to Twitter API v2 endpoints only. If you need access to this endpoint, you’ll need to apply for Elevated access via the Developer Portal." even though i am not even accessing any endpoint at all i just need it to log my users in. for reference here is my passport.js file:
passport.use(new TwitterStrategy({
consumerKey: TWITTER_KEY,
consumerSecret: TWITTER_SECRET,
callbackURL: "http://localhost:8080/api/user/auth/twitter/callback",
passReqToCallback: true
},
(accessToken, refreshToken, profile, done) => {
console.log(profile);
return done(err, profile)
// User.findOrCreate({ twitterId: profile.id }, function (err, user) {
// return cb(err, user);
// });
}
));
user.get('/auth/twitter',passport.authenticate('twitter'));
user.get('/auth/twitter/callback',passport.authenticate('twitter', { failureRedirect: '/auth/error' }), (req, res) => {
res.redirect('/');
});
I'm trying to exchange the authorization code I got in the first step of the documentation for access token. Where I'm stuck is how to send a request for the token that contains the code I've just got with the first request.
This is my code:
auth: {
redirect: {
login: '/',
callback: '/auth'
},
strategies: {
wrike: {
scheme: 'oauth2',
endpoints: {
authorization: 'https://login.wrike.com/oauth2/authorize/v4',
token: 'https://login.wrike.com/oauth2/token',
logout: '/'
},
token: {
property: 'access_token',
type: 'Bearer',
maxAge: 1800
},
responseType: 'code',
grantType: 'authorization_code',
accessType: 'offline',
clientId: XXXX,
client_secret: YYYY
}
}
}
I can't figure it out how I should set up the redirect URI, in the client or in the server side? How should I do the second request? (This below)
POST https://login.wrike.com/oauth2/token
//Parameters:
client_id=<client_id>
client_secret=<client_secret>
grant_type=authorization_code
code=<authorization_code>
I think Edward is right. It doesn't seem to work. You can either do the custom schema which is what I am going to do, or you can do what I currently have which is something like this (of course ignore all the console.log and stuff like that):
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
const state = urlParams.get('state')
console.log('state', state)
console.log('stateStorage', window.localStorage.getItem(state))
if ((code && state) && state === window.localStorage.getItem('state')) {
this.$axios.post('http://publisher-local.co.uk:8080/oauth/token', {
grant_type: 'authorization_code',
client_id: 5,
redirect_uri: 'http://localhost:3000/auth',
code_verifier: window.localStorage.getItem('verifier'),
code
}).then(response => {
this.$auth.setUserToken(response.data.access_token)
this.$auth.fetchUser()
})
}
So basically after you are redirected back to your client after logging in on the server page just look for the details in the URL and make the request yourself.
I can obtain the OAuth2 token, but can't revoke it.
Reproducing the error:
let token_url = "https://api.twitter.com/oauth2/token";
let revoke_url = "https://api.twitter.com/oauth2/invalidate_token"
let auth = {
username: CLIENT_API_KEY,
password: CLIENT_API_SECRET_KEY
}
axios.post(token_url, "grant_type=client_credentials", { auth })
.then( response => {
let access_token = response.data["access_token"]
axios.post(revoke_url, `access_token=${access_token}`, { auth })
.then( response => {
console.log("REVOKE SUCCESS")
console.log(response)
})
.catch( e=> {
console.log("Failed to revoke")
console.log(e.response.data)
})
})
then I get
Failed to revoke
{ errors:
[ { code: 348,
message:
'Client application is not permitted to to invalidate this token.' } ] }
Tried googling but nothing came out of the code 348 and the message. The http error code is 401 (unauthorized).
I will never be able to revoke my OAuth2 token. Help.