MSAL User context cached token in browser are not changing when Logged in User change - msal.js

I am using the acquireTokenSilent function with the scope that i have and with the login hint.
Creating the Msal.UserAgentApplication as below
function getUserAgentApp(clientId: string): Msal.UserAgentApplication {
path = //somepath
const redirectUri = ${location.protocol}//${location.hostname}${path}${ location.port ? ":" + location.port : "" }/authdialog.html;
const msalConfig: Msal.Configuration = {
auth: {
clientId: `${clientId}`,
authority: "https://login.microsoftonline.com/common",
redirectUri: redirectUri,
navigateToLoginRequestUrl: false,
},
cache: {
cacheLocation: "localStorage", // Needed to avoid "User login is required" error.
storeAuthStateInCookie: true, // Recommended to avoid certain IE/Edge issues.
},
};
return new Msal.UserAgentApplication(msalConfig);
}
on calling the acquireTokenSilent with scope and loginHint, for the first user, this token is cached in local storage, but when i changed the user in the same browser ( logout and login with different user) , loginhint is changed but still the function acquireTokenSilent is returning the token from cache( of previous user) .
I dont want to call logout explicitly with UserAgentApplication object as it would block our customer, also calling forceRefresh would work but that will not change the cache(local storage).
Is there a way I can silently clear local storage and update the token in cache of new user.?

Related

Chrome Extensions - token from "launchWebAuthFlow" expires after an hour and need user interaction to get new one

I am using launchWebAuthFlow in a service worker to authenticate users who choose to backup the extension settings in their Google Drive.
When a user clicks the login button, it sends a message to the service worker (MV3, perviously "background script"), who does this:
const redirectUrl = await browser.identity.launchWebAuthFlow({
'url': _createAuthEndpoint(),
'interactive': true
})
const url = new URL(redirectUrl);
const urlParams = new URLSearchParams(url.hash.slice(1));
const params = Object.fromEntries(urlParams.entries());
await browser.storage.local.set({googleToken: params.access_token});
Helper method to construct auth url:
function _createAuthEndpoint() {
const redirectURL = browser.identity.getRedirectURL();
const { oauth2 } = browser.runtime.getManifest();
const clientId = oauth2.client_id;
const authParams = new URLSearchParams({
client_id: clientId,
response_type: 'token',
redirect_uri: redirectURL,
scope: 'openid ' + oauth2.scopes.join(' '),
});
return `https://accounts.google.com/o/oauth2/auth?${authParams.toString()}`;
}
It works well for about an hour, after that the token get invalidated and I need to get a new token. If i try to use launchWebAuthFlow with interactive: false I get an error "user interaction required"
Is there a way to have the token refresh without user interaction?

msal.js v2.3 acquireTokenSilent returning empty access token

I am upgrading an app using msal.js v1.3 to v2.3 and I'm having a problem retreiving the access token once I get my id token.
I initialize the handleRedirectPromise in my constructor. Then, when the user clicks the login button, I call loginRedirect and pass in an object that has the openid scope and the scope from my separately registered api. This works well, the id token comes back and I call acquireTokenSilent to retreive my access token. I pass an object that has my registered api's scope and account from the loginRedirect call into this function.
The problem is that the authorization response from the acquireTokenSilent has an empty access token. The result from the token endpoint looks like:
client_info: "xx"
id_token: "xx"
not_before: 1602895189
refresh_token: "xx"
refresh_token_expires_in: 1209600
scope: ""
token_type: "Bearer"
It doesn't have an access token, but it does specifiy the token type as Bearer
There is no access token in the response and it looks like the scopes property returning is empty. Here is my code:
private msalConfig: Msal.Configuration = {
auth: {
clientId: environment.clientID,
authority: 'https://<tenant>.b2clogin.com/<tenant>.onmicrosoft.com/B2C_1_DefaultSignInSignUp',
knownAuthorities: ['<tenant>.b2clogin.com'],
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
}
};
private loginRequest: Msal.RedirectRequest = {
scopes: ['openid' , 'offline_access', 'https://<tenant>.onmicrosoft.com/api/read' ]
};
private accessTokenRequest: Msal.SilentRequest = {
scopes: ['https://<tenant>.onmicrosoft.com/api/read'] ,
account: null
};
constructor() {
const _this = this;
this.msalInstance = new Msal.PublicClientApplication(this.msalConfig);
this.aquireSilent = (request: Msal.SilentRequest): Promise<Msal.AuthenticationResult> => {
return _this.msalInstance.acquireTokenSilent(request).then(
access_token => {
_this.cacheExpiration(access_token.expiresOn);
_this.isLoggedIn$.next(true);
return access_token;
},
function (reason) {
console.error(reason);
},
);
};
this.msalInstance
.handleRedirectPromise()
.then((tokenResponse: Msal.AuthenticationResult) => {
if (tokenResponse !== null) {
const id_token = tokenResponse.idToken;
const currentAccounts = this.msalInstance.getAllAccounts()
this.accessTokenRequest.account = currentAccounts[0];
this.aquireSilent(this.accessTokenRequest)
}
})
.catch(error => {
console.error(error);
});
}
public login() {
this.msalInstance.loginRedirect(this.loginRequest);
}
Why is the access token not coming back from the token endpoint? Does it have to do with the scopes returning empty? I tried removing the scopes and putting in invalid entries and an error gets raised so I know my request going out is at least valid. Also, just to verify, I have 2 app registrations in AAD, one I created for my spa that has code flow and my older registration I have for my api with an exposed api and scope.
acquireTokenSilent will return an access token only if there is already an entry for that token in the cache. So if for some reason the token was never obtained previously (via loginRedirect, for instance), it will not be able to acquire it silently.
That seems to be the issue in your case. You are mixing scopes for different resources in your loginRequest, and that's perhaps causing the issue in the new version of the library (access tokens are issued per-resource-per-scope(s). See this doc for more) Try modifying your loginRequest object like this:
private loginRequest: Msal.RedirectRequest = {
scopes: ['openid', 'offline_access' ],
extraScopesToConsent:['https://<tenant>.onmicrosoft.com/api/read']
};
Also, the recommended pattern of usage with acquireTokenSilent is that you should fall back to an interactive method (e.g. acquireTokenRedirect) if the acquireTokenSilent fails for some reason.
So I would modify it as:
this.aquireSilent = (request: Msal.SilentRequest): Promise<Msal.AuthenticationResult> => {
return _this.msalInstance.acquireTokenSilent(request).then(
access_token => {
// fallback to interaction when response is null
if (access_token === null) {
return _this.msalInstance.acquireTokenRedirect(request);
}
_this.cacheExpiration(access_token.expiresOn);
_this.isLoggedIn$.next(true);
return access_token;
},
function (reason) {
if (reason instanceof msal.InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return _this.msalInstance.acquireTokenRedirect(request);
} else {
console.warn(reason);
}
},
);
};
A similar issue is discussed here

Returning email address as part of AAD B2C msal.js code

How do I correctly configure and code a call to AAD B2C using msal.js that returns email address in the response?
Background
I'm looking to write a javascript integration into Shiny, the R dashboard solution, which needs JavaScript integrations of authentication solutions. The dashboard must authenticate against Azure Active Directory B2C. Shiny essentially works as a SPA application.
AAD B2C config
I have an AAD B2C user flow:
name: B2C_1_signup_signin
identity providers: email signup
user attributes: email address
application claims:
email addresses
identity provider
I have an AAD B2C Application:
name: bigdashboard
app id: a0cfc440-c766-43db-9ea8-40a1efbe22ac
include web app / web api: yes
allow implicit flow: yes
app id uri: https://lduceademo.onmicrosoft.com/big
include native client: no
api access:
Access the user's profile: (All available options selected)
Acquire an id_token for users (openid)
Acquire a refresh_token for users (offline access)
bigdashboard:
read (read)
Access this app on behalf of the signed-in user (user_impersonation)
published scopes:
read
user_impersonation
Additionally, I've used the App Registrations (preview) to add some api permissions for Microsoft Graph and all have been granted admin consent.
Microsoft Graph:
User.Read
email
offline_access
openid
profile
Current JavaScript
Code amended from the following samples:
AAD B2C JS MSAL SPA
The MSAL.js lib v1.1.3 is being used to support the below bespoke code.
// The current application coordinates were pre-registered in a B2C tenant.
var appConfig = {
b2cScopes: ["profile","email","openid", "https://lduceademo.onmicrosoft.com/big/read"]
};
// configuration to initialize msal
const msalConfig = {
auth: {
clientId: "a0cfc440-c766-43db-9ea8-40a1efbe22ac", //This is your client ID
authority: "https://lduceademo.b2clogin.com/lduceademo.onmicrosoft.com/B2C_1_signup_signin", //This is your tenant info
validateAuthority: false
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
// instantiate MSAL
const myMSALObj = new Msal.UserAgentApplication(msalConfig);
// request to signin - returns an idToken
const loginRequest = {
scopes: appConfig.b2cScopes
};
// request to acquire a token for resource access
const tokenRequest = {
scopes: appConfig.b2cScopes
};
// signin and acquire a token silently with POPUP flow. Fall back in case of failure with silent acquisition to popup
function signIn() {
myMSALObj.loginPopup(loginRequest).then(function (loginResponse) {
getToken(tokenRequest).then(updateUI);
}).catch(function (error) {
console.log(error);
});
}
//acquire a token silently
function getToken(tokenRequest) {
return myMSALObj.acquireTokenSilent(tokenRequest).catch(function(error) {
console.log("aquire token popup");
// fallback to interaction when silent call fails
return myMSALObj.acquireTokenPopup(tokenRequest).then(function (tokenResponse) {
}).catch(function(error){
console.log("Failed token acquisition", error);
});
});
}
// updates the UI post login/token acqusition
function updateUI() {
const userName = myMSALObj.getAccount().name;
console.log(myMSALObj.getAccount());
console.log("User '" + userName + "' logged-in");
$('.signin').toggleClass('hidden', true);
$('.signout').toggleClass('hidden', false);
Shiny.setInputValue('message', userName);
}
// signout the user
function logout() {
// Removes all sessions, need to call AAD endpoint to do full logout
myMSALObj.logout();
}
Current response
From this I get back an Account object that shows up in the console like:
accountIdentifier: "ddc90829-f331-4214-8df1-0cf6052f4b61"
environment: "https://lduceademo.b2clogin.com/c1138a05-4442-4003-afc7-708629f4554c/v2.0/"
homeAccountIdentifier: "ZGRjOTA4MjktZjMzMS00MjE0LThkZjEtMGNmNjA1MmY0YjYxLWIyY18xX3NpZ251cF9zaWduaW4=.YzExMzhhMDUtNDQ0Mi00MDAzLWFmYzctNzA4NjI5ZjQ1NTRj"
idToken:
aud: "a0cfc440-c766-43db-9ea8-40a1efbe22ac"
auth_time: 1575368495
exp: 1575372095
iat: 1575368495
iss: "https://lduceademo.b2clogin.com/c1138a05-4442-4003-afc7-708629f4554c/v2.0/"
nbf: 1575368495
nonce: "0933fc11-e24f-4ce2-95e2-0afe9bcc1d72"
sub: "ddc90829-f331-4214-8df1-0cf6052f4b61"
tfp: "B2C_1_signup_signin"
ver: "1.0"
name: undefined
sid: undefined
userName: undefined
Partly, my test account wasn't great - it was a native ie didn't sign up AAD B2C account, but querying email address should be performed like:
function updateUI() {
// query the account
const account = msal.getAccount();
// first email address
const username = account.idTokenClaims.emails[0];
// situation specific code
$('.signin').toggleClass('hidden', true);
$('.signout').toggleClass('hidden', false);
Shiny.setInputValue('message', username);
}

UserPool.getCurrentUser( ) returns null [backend]

Am trying to set up a federated identity in order to get credentials for identity. But when i try to do the getCurrentUser() am getting response as null. And another thing about this is, am trying this on backend side. So will it work in the backend? And why am getting a null response when trying getCurrentUser ?? Any idea?
var data = {
UserPoolId: userPoolId,
ClientId: appClientId,
};
var userPool = new AmazonCognitoIdentity.CognitoUserPool(data);
console.log(userPool);
var cognitoUser = userPool.getCurrentUser();
console.log(cognitoUser);
The log response of userPool is
CognitoUserPool {
userPoolId: 'us-east-6_hxxxx2U',
clientId: '`6heh4h8h848h4884h05',
client:
Client {
endpoint: 'https://cognito-idp.us-east-1.amazonaws.com/',
userAgent: 'aws-amplify/0.1.x js' },
advancedSecurityDataCollectionFlag: true,
storage:
{ [Function: MemoryStorage]
setItem: [Function: setItem],
getItem: [Function: getItem],
removeItem: [Function: removeItem],
clear: [Function: clear] } }
The log response of cognitoUser is NULL
So why is response null, while am giving right values as input?
There are few potential causes:
Use getCurrentUser() instead of getCurrentUser(Data)
If you didn’t sign in a user at backend, it wouldn’t be possible to get the current user. If users are signed in at front end, you can use a function to send user’s id_token to backend and use it to sign in the use at backend.
About the second point:
The id_token contains a part called payload which contains the user’s username and other attributes. For details: https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html#amazon-cognito-user-pools-using-the-id-token
When you use the id_token you should verify the signature before allowing further actions for the user. Codes for verifying can be found in https://github.com/awslabs/aws-support-tools/tree/master/Cognito/decode-verify-jwt
And you can add you code for actions here:
.....
// and the Audience (use claims.client_id if verifying an access token)
if (claims.aud != app_client_id) {
callback('Token was not issued for this audience');
}
//add your code here
callback(null, claims);
}).
catch(function() {
callback('Signature verification failed');
});
And the user information should be in the claims.
Its because the code I have posted here is for the frontend. There is an article https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html which says clearly how should we try to authenticate a user.
So after the correct auth flow, we will pass the required data by using cognitoidentity.getCredentialsForIdentity() [refer offical sdk doc]

Refresh token for Google APIs to keep user logged in

I am developing an App on Actions on Google that uses Google Calendar. At the start of a user's first interaction, they are prompted to link their Google account for access to the calendar. But then for nearly every future interaction, the user has to authenticate again.
My code to get the access token from the user data is below. I'm not sure if there's a way to get the refresh token to add to the credentials. I've seen you should include "access_type": "offline" in the request in other answers, but I'm not sure where I'd put it in my code. Any help would be greatly appreciated!
const googleAuth = require('google-auth-library');
function getAuth(app) {
var accessToken = app.getUser().accessToken;
var clientId = CLIENT_ID;
var clientSecret = CLIENT_SECRET;
var redirectUrl = 'https://oauth-redirect.googleusercontent.com/r/my_app_name';
var auth = new googleAuth();
var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);
var token = {
"access_token": accessToken,
"refresh_token": "",
"access_type": "offline", //Not sure if these attributes should be here
"approval_prompt": "force",
"immediate": true,
"token_type": "Bearer"
};
oauth2Client.credentials = token;
return oauth2Client;
}
calendar.freebusy.query ({
auth: getAuth(app), //need a Google OAuth client here for API request
headers: { "content-type" : "application/json" },
resource:{
items: [
{
id: "primary"
}
],
timeMin: startDate.format(),
timeMax: endDate.format(),
timeZone: timeZoneId
}, function(err, response) {
console.log('Success!');
}
});
Not sure if this helps, but on the very first interaction a user has to select an account and agree to the calendar permission, but on subsequent interactions they just have to tap "link account to Google" and it automatically authenticates them.
EDIT:
This is a screenshot of the prompt I get on every new interaction with the app. It was my understanding that account linking is only done on the very first interaction with the app, then the accounts are linked for good and the user need not be prompted again. I am not receiving any errors, I was just wondering if and how I can modify my current code to not prompt the user to link their account on every single interaction.

Resources