MSAL Node / Electron - PublicClientApplication.acquireTokenByCode() accessing Tokens despite clearing accounts - node.js

I'm trying to clear all access Tokens from Main.Ts in an Electron App without success.
On the Node side of the App, I have a login function that implements the Auth Code Flow with PKCE - which works fine:
let login = async () => {
try {
const authResult = await getTokenInteractive(authCodeUrlParams);
mainWindow.webContents.send(IpcMessages.XXX-XXXX, authResult);
} catch (error) {
console.log(error);
}
};
let getTokenInteractive = async (
tokenRequest: AuthorizationUrlRequest
): Promise\<AuthenticationResult\> =\> {
try {
...
// Generate PKCE Challenge and Verifier before request
const cryptoProvider = new CryptoProvider();
const { challenge, verifier } = await cryptoProvider.generatePkceCodes();
...
// Get Auth Code URL
const authCodeUrl = await clientApplication.getAuthCodeUrl(
localAuthCodeUrlParams
);
const authCode = await listenForAuthCode(authCodeUrl, authWindow);
...
// This works fine
const authResult: AuthenticationResult =
await clientApplication.acquireTokenByCode({
...authCodeRequest,
code: authCode,
codeVerifier: verifier,
});
return authResult;
} catch (error) {
throw error;
}
};
The logout function also clears the accounts:
const logout = async () =\> {
try {
console.log("LOGOUT RECEIVED");
const accounts: AccountInfo\[\] = await clientApplication
.getTokenCache()
.getAllAccounts();
if (accounts && accounts.length \> 0) console.log(accounts);
await clientApplication.getTokenCache().removeAccount(accounts\[0\]);
// Test to see if accounts are cleared returns '[]'
const checkAccounts: AccountInfo[] = await clientApplication
.getTokenCache()
.getAllAccounts();
console.log(checkAccounts);
mainWindow.webContents.send(IpcMessages.USER_LOGGED_OUT);
} catch (err) {
console.log("Error in main.logout ", err);
}
};
Unfortunately, acquireTokenByCode still accesses the Token (?? How) when the user is redirected to a login page.
ps I've also checked local storage, Cookies etc in the browser and they are never populated (ie always remain empty).
I've seen numerous answers suggesting using a logout URL:
https://learn.microsoft.com/en-us/answers/questions/263335/msal-sign-out-does-not-appear-to-clear-cache.html
https://learn.microsoft.com/en-us/answers/questions/994601/logout-does-not-clear-session.html
However, these solutions down't work with Azure B2C. Also, redirecting using the logout endpoint using Electron.shell(openExternal([url])) also does nothing.
It's unbelievable that MS hasn't implemented a simple logout solution.
Question: Does anyone know how to stop tokens from being acquired WITHOUT having to open a native browser window or populate the Browser Window with a "Successfully signed out. You can now close this window" prompt ?
Thanks in advance.

Related

JWT Silent Refreshing based on Redis - MERN Stack

I've been struggling so long with silent refreshing to make it the most transparent to client.
I've implemented so many strategies so far and still I have no clue how solve it fully.
When I perform silent refreshing I put my recent generated refresh token in redis db for further access token generation so as to it carries out refreshing for both everytime when there is a need to refresh.
Due to React Strict Mode in dev mode, request is being made twice so first time it can be pulled out from db and showed normally but consecutive request acts like value in db doesn't exist and it spits null value.
My middleware is written in express js, I put my code beneath:
const authRefresh = async (req, res) => {
try {
const refreshToken = await req.cookies['refresh-token'];
const accessToken = await req.cookies['access-token'];
if (!refreshToken) {
return res.status(401).json({ status: 'error', message: "Refresh token not found" })
}
if (!accessToken || accessToken.includes('mockup'))
jwt.verify(refreshToken, process.env.JWT_REFRESH, async (err, userProperties) => {
if (err) {
return res.status(401).json(err)
}
const redisValue = await redisClient.get(refreshToken);
if (redisValue) {
const tokens = await generateToken(userProperties)
//setting tokens in httpOnly cookies
res.cookie('access-token', tokens.accessToken,
cookiesTime.access);
res.cookie('refresh-token', tokens.refreshToken,
cookiesTime.refresh);
await redisClient.del(redisValue);
await redisClient.set(tokens.refreshToken, tokens.refreshToken);
await redisClient.expire(tokens.refreshToken, time_refresh);
return res.status(200).json('Tokens set again')
}
else {
return res.status(401).json('No refresh token in db');
}
})
else return res.status(202).json('No need to refresh an access token')
} catch (err) {
return res.status(401).json({ status: 'error', message: err })
}
}
PS. I want to mention I've tried rearrange redis methods from asynchronous to synchronous and backwards. It seems the same for my action.

CouchDB (nano) randomly returns `You are not allowed to access this db.`

I've integrated a user authentication and it already works very well. (Authenticates and returns a JWT in cookie.)
But for some reasons AFTER I logged in with my user and I want to establish a connection to the database I get a 403. The request itself isnt even protected. No verification if user is logged in or anything. Its a public request.
The funny part is, if I restart the node express application the exact same request goes through.
So it seems that anything within the process.
My login function:
const login = async (username, password) => {
try {
const response = await connection.auth(username, password)
if (!response.ok) {
return [401, null]
}
const token = jwt.sign({ sub: username }, secret, { expiresIn: '1h' })
return [null, token]
} catch (error) {
return [error, null]
}
}
This one gets called right after and I run into the catch block
try {
const conn = await nano('http://admin:password#127.0.0.1:5984')
const db = await conn.use('test')
[…]
} catch (error) {
// I LAND HERE
console.error(error)
return [error, null]
}
}
Okay I figured it out myself.
Looks like 403 was sent because it overrides your current session. What I did was basically authenticate again right after "login" was successful but this time I used admin and password again. Kinda ugly but it works.
const response = await connection.auth(username, password)
if (!response.ok) {
return [401, null]
}
await connection.auth('admin', 'password') // < server credentials

Delete User and Logout that user from all Devices

I wanted to implement a feature in my app. Where an Admin can delete the user. So basically the delete is working fine but somehow i cannot logout the logged in user. Let me explain it more briefly, Suppose there is a User A which is currently using my app and the admin decided to remove that user from the app so they can't no longer access the features of the app. To remove the user i can call an API and delete that user but if i completely delete the user it loses all the access to the API's call coz user with the certain ID isn't available anymore and the app breaks coz the API call will fail for that deleted User. So I was wondering is there anyway to logout the user after admin deletes it.
The Frontend is on ReactJs and Backend is on NodeJs. And i am using JWT for authentication. Any help will be appreciated and if this question isn't clear enough please let me know so i can explain it more.
In backend in every protected route you should verify the token and token should contain user id or email using that you will verify the token. After deleting the user throw error with no user found and in frontend make sure if there are the error no user found then it will delete the JWT token.
What comes into my mind is to put a middleware between your requests and server. By doing so, instead of trying to log out from all devices, we will not allow any action if user does not exist; in this very example, we will prevent the user to delete a place and toast a message on the front end. I will share an example of that, but you need to tweak the code according to your needs.
Http Error Model
class HttpError extends Error {
constructor(message, errorCode) {
super(message);
this.code = errorCode;
}
}
module.exports = HttpError;
Middleware
const HttpError = require('../models/http-error');
module.exports = (req, res, next) => {
try {
// Check user if exists
User.findById(req.userData.userId).exec(function (error, user) {
if (error) {
throw new Error('Authentication failed!');
}
else {
return next();
}
});
}
catch (error) {
return next(new HttpError('Authentication failed!', 403));
}
};
Route
const express = require('express');
const router = express.Router();
const checkAuth = require('../middleware/check-auth');
router.use(checkAuth);
// Put after any routes that you want the user to be logged in
router.delete('/:placeId', placesControllers.deletePlace); //e.x.
...
module.exports = router;
E.x. controller (with MongoDB)
const deletePlace = async (req, res, next) => {
const placeId = req.params.placeId;
let foundPlace;
try {
foundPlace = await Place.findById(placeId).populate('userId').exec();
}
catch (error) {
return next(new HttpError('Could not find the place, please try again', 500));
}
// Delete place
res.status(200).json({message: 'Deleted place'});
};
FRONT END PART
import toastr from 'toastr';
....
try {
const response = await fetch(url, {method, body, headers});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message);
}
}
catch(error) {
// handle the error, user not found
console.log(error.message);
toastr.error(error.message, 'Error', {
closeButton: true,
positionClass: 'toast-top-right',
timeOut: 2000,
extendedTimeOut: 1,
});
}

unable to get a response with people.connections.list

⚠️ I forgot a process.exit(0) in the main thread, so the app was terminated before the callback was executed. This code sample works like a charm.
Here is the code from googleapis nodejs client I have issue on:
First thing first, I would like to get the list of contacts for one user using a nodejs application.
Set up a OAuth2Client
So I set up a OAuth2Client with this code:
const {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
} = require("./keys.json");
const REDIRECT_URL = "http://localhost:3000/oauth2callback";
const oAuth2Client = new OAuth2Client(CLIENT_ID, CLIENT_SECRET, REDIRECT_URL);
Then, using a temporary server, I ask for token using the user's credentials:
function getGoogleCode() {
return new Promise((resolve, reject) => {
// Open an http server to accept the oauth callback. In this simple example, the
// only request to our webserver is to /oauth2callback?code=<code>
const server = http
.createServer(async (req, res) => {
if (req.url.indexOf("/oauth2callback") > -1) {
// acquire the code from the querystring, and close the web server.
const { code } = querystring.parse(url.parse(req.url).query);
res.end(
`Authentication successful! Please return to the console. [code: ${code}]`
);
server.close();
resolve(code);
}
reject(new Error("oops", req, res));
})
.listen(3000, () => {
// open the browser to the authorize url to start the workflow
// Generate the url that will be used for the consent dialog.
const authorizeUrl = oAuth2Client.generateAuthUrl({
access_type: "offline",
scope: ["https://www.googleapis.com/auth/contacts.readonly"]
});
opn(authorizeUrl);
});
});
}
Then I finish to set up my client:
const code = await getGoogleCode();
const { tokens } = await oAuth2Client.getToken(code);
oAuth2Client.setCredentials(tokens);
When everything's fine
I managed to get a response with the low level API:
const personFields = ["emailAddresses", "names"];
const url = `https://people.googleapis.com/v1/people/me/connections?personFields=${personFields.join(
","
)}`;
const res = await oAuth2Client.request({ url });
console.log(chalk.gray(JSON.stringify(res.data.connections, null, 2)));
Everything is working like a charm, but, I would like to use the high level API from the same library
google.people API
As described in API Explorer, I build the code below:
const personFields = ["emailAddresses", "names"];
people.people.connections.list(
{ resourceName: "people/me", personFields },
(res, err) => {
console.log("error: ", err);
console.log("res1: ", res);
}
);
No error, no res1, nothing.
⚠️ I forgot a process.exit(0) in the main thread, so the app was terminated before the callback was executed. This code sample works like a charm.
Not sure why you aren't getting logging. But you probably should return the res in the callback, otherwise calling await on the callback will return undefined.
Also make sure to set personFields in the people.people.connections.list call.

do something 'before' login in loopback

I am pretty new to loopback and here is what I am doing:
I am using standard login route provided by the loopback to log in the users - extended base Users to my own model say orgadmin.
With prebuilt route /api/orgadmin/login, I can easily login.
Now, I have a flag in orgadmins say 'status' which can be either 'active' or 'inactive' based on which I have to defer user login.
I was thinking something with remote hooks like beforeRemote as below but it doesn't work:
//this file is in the boot directory
module.exports = function(orgadmin) {
orgadmin.beforeRemote('login', function(context, user, next) {
console.log(user)
// context.args.data.date = Date.now();
// context.args.data.publisherId = context.req.accessToken.userId;
next();
});
};
So what is the best way to accomplish this?
The user attribute will only be available if the request is coming with a valid access token. The attribute is unused for unauthenticated requests, which login is.
Here's a possible alternative:
module.exports = (OrgAdmin) => {
OrgAdmin.on('dataSourceAttached', () => {
const { login } = OrgAdmin;
OrgAdmin.login = async (credentials, include) => {
const accessToken = await login.call(OrgAdmin, credentials, include);
const orgAdmin = await OrgAdmin.findById(accessToken.userId);
if (orgAdmin.status !== 'active') {
OrgAdmin.logout(accessToken);
const err = new Error('Your account has not been activated');
err.code = 'NOT_ACTIVE_USER';
err.statusCode = 403;
throw err
}
return accessToken;
};
});
};
The above code overrides the login method and does the following:
Login the user, using loopback's built-in login
Take the response of login, which is an access token, and use it to get the user.
If the user is active, return the access token, satisfying the expected successful response of login.
If the user is not active, remove the access token that was created (which is what logout does), and throw an error.

Resources