ServerError: AADSTS50058: A silent sign-in request was sent but none of the currently signed in user(s) match the requested login hint" - msal.js

SSO fails "ServerError: AADSTS50058: A silent sign-in request was sent but none of the currently signed in user(s) match the requested login hint"
when I use same account for both work and personal azure account.
I have 2 AAD accounts (one is with my work account and the other one is personal account but both attached with same email and both are using same credentials). When I use msal.js library for single sign on application. It takes me to my work account where it asks me to validate the credentials (using standard pop up dialog) by giving full email address and does not authenticate properly even if give right credentials. As I need to login using my personal account
I expect this should validate using my ad alias#company.com credentials. I tried with different account option in the dialog, but it fails and shows up same full email account.
How can I use my adalias#company.com as a default user id?
Here are the piece of the code I am trying to use.
var msalConfig = {
auth: {
clientId: 'xxxxxxxxxx', // This is your client ID
authority: "https://login.microsoftonline.com/{tenantid}" // This is tenant info
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
var graphConfig = {
graphMeEndpoint: "https://graph.microsoft.com/v1.0/me"
};
var requestObj = {scopes: ["user.read", "email"]};
// Is there a way to change here to get the required user id?
var myMSALObj = new Msal.UserAgentApplication(msalConfig);
// Register Callbacks for redirect flow
myMSALObj.handleRedirectCallbacks(acquireTokenRedirectCallBack,
acquireTokenErrorRedirectCallBack);
myMSALObj.handleRedirectCallback(authRedirectCallBack);
function signIn() {
myMSALObj.loginRedirect(requestObj).then(function (loginResponse) {
// Successful login
acquireTokenPopupAndCallMSGraph();
}).catch(function (error) {
// Please check the console for errors
console.log(error);
});
}
Here is the error message I get:
ServerError: AADSTS50058: A silent sign-in request was sent but none of the
currently signed in user(s) match the requested login hint
The expected result is seamless login to other application.

If you want to provide a login_hint to indicate the user you are trying to authenticate try:
var requestObj = {scopes: ["user.read", "email"], loginHint: "adalias#company.com"};
Reference https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/FAQs#q10-how-to-pass-custom-state-parameter-value-in-msaljs-authentication-request-for-example-when-you-want-to-pass-the-page-the-user-is-on-or-custom-info-to-your-redirect-uri

Related

Are the scopes defined when 'exposing an API' in an Azure AD Application Registration for information purposes only? If not, how are they enforced?

Desired Behaviour
I am setting up a Node.js web application using Azure AD B2C authentication and authorisation.
It is a confidential, server side, client (i.e. - not a Single Page Application).
The desired behaviour is:
Authenticate and authorise a user via login using Azure ADB2C
Based on a successful login, allow users to call routes in the Node web app
My specific question is at the bottom of this post, but is essentially:
Given that, if an accessToken is present in req.session, my
application will return the result from calling ANY endpoint I choose
(even if it is not related to any of the 'scopes' defined when
'exposing the api'), are the scopes that are defined when 'exposing an
API' essentially 'for information purposes only' - both for
application admins and end users? Or should they somehow be enforced in each relevant Express route handler?
Research
I have done extensive reading and video watching around the topic, including:
GitHub Repositories:
active-directory-b2c-msal-node-sign-in-sign-out-webapp
active-directory-b2c-javascript-nodejs-webapi
learn.microsoft.com articles:
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-overview
https://learn.microsoft.com/en-au/azure/active-directory-b2c/configure-a-sample-node-web-app
https://docs.microsoft.com/azure/active-directory-b2c/enable-authentication-in-node-web-app
https://docs.microsoft.com/azure/active-directory-b2c/configure-authentication-in-sample-node-web-app-with-api
https://docs.microsoft.com/azure/active-directory-b2c/enable-authentication-in-node-web-app-with-api
Videos:
Identity for Developers Playlist by Microsoft Security
OAuth 2.0 and OpenID Connect (in plain English) by OktaDev
What I've Tried
I have been able to implement a basic prototype consisting of:
Node.js web application (a confidential, server side, client, not a Single Page App)
Sign Up, Sign In and Edit Profile User flows
2 x Application Registrations in Azure - one for the Web App and one for the Web App API
I have 'exposed the API' in the Web App API registration, eg: :
https://my-tenant-name.onmicrosoft.com/my-api-uri-thing/tasks.read
https://my-tenant-name.onmicrosoft.com/my-api-uri-thing/tasks.write
I have 'added permissions' to the Web App registration (to access the Web App API registration), eg:
MY-APP-API (2)
tasks.read, Delegated, Admin Consent Required - Yes, Granted for MY-APP
tasks.write, Delegated, Admin Consent Required - Yes, Granted for MY-APP
Microsoft Graph (2)
offline_access, Delegated, Admin Consent Required - No, Granted for MY-APP
openid, Delegated, Admin Consent Required - No, Granted for MY-APP
So the workflow is as follows (based on this code):
User clicks on a '/signin' link
The relevant Express route handler passes through the required scopes to getAuthCode():
app.get('/signin',(req, res)=>{
//Initiate a Auth Code Flow >> for sign in
//Pass the api scopes as well so that you received both the IdToken and accessToken
getAuthCode(process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY,apiConfig.webApiScopes, APP_STATES.LOGIN, res);
});
Source
The value of the scopes parameter is:
['https://${process.env.TENANT_NAME}.onmicrosoft.com/my-api-uri-thing/tasks.read','https://${process.env.TENANT_NAME}.onmicrosoft.com/my-api-uri-thing/tasks.write']
Upon successful sign in, the user is redirected to /redirect
The relevant Express route handler passes through a tokenRequest object to get an accessToken which is then added to req.session:
app.get('/redirect',(req, res)=>{
if (req.query.state === APP_STATES.LOGIN) {
// prepare the request for calling the web API
tokenRequest.authority = process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY;
tokenRequest.scopes = apiConfig.webApiScopes;
tokenRequest.code = req.query.code;
confidentialClientApplication.acquireTokenByCode(tokenRequest)
.then((response) => {
req.session.accessToken = response.accessToken;
req.session.givenName = response.idTokenClaims.given_name;
console.log('\nAccessToken:' + req.session.accessToken);
res.render('signin', {showSignInButton: false, givenName: response.idTokenClaims.given_name});
}).catch((error) => {
console.log(error);
res.status(500).send(error);
});
}else{
res.status(500).send('We do not recognize this response!');
}
});
User calls a 'protected API'
The relevant Express route handler checks if req.session contains an accessToken value
If the accessToken is present, it makes an http request to the desired endpoint using axios and passes through the accessToken as the Bearer token in the headers of the request.
The 'protected content' is then returned
app.get('/api', async (req, res) => {
if(!req.session.accessToken){
//User is not logged in and so they can only call the anonymous API
try {
const response = await axios.get(apiConfig.anonymousUri);
console.log('API response' + response.data);
res.render('api',{data: JSON.stringify(response.data), showSignInButton: true, bg_color:'warning'});
} catch (error) {
console.error(error);
res.status(500).send(error);
}
}else{
//Users have the accessToken because they signed in and the accessToken is still in the session
console.log('\nAccessToken:' + req.session.accessToken);
let accessToken = req.session.accessToken;
const options = {
headers: {
//accessToken used as bearer token to call a protected API
Authorization: `Bearer ${accessToken}`
}
};
try {
const response = await axios.get(apiConfig.protectedUri, options);
console.log('API response' + response.data);
res.render('api',{data: JSON.stringify(response.data), showSignInButton: false, bg_color:'success', givenName: req.session.givenName});
} catch (error) {
console.error(error);
res.status(500).send(error);
}
}
});
Question
Given that, if an accessToken is present in req.session, my application will return the result from calling ANY endpoint I choose (even if it is not related to any of the 'scopes' defined when 'exposing the api'), are the scopes that are defined when 'exposing an API' essentially 'for information purposes only' - both for application admins and end users?
Or should I be coding in a conditional statement in each relevant Express route handler that says:
IF the required scope for accessing this content is present in this access token,
THEN you can access this content
and therefore 'enforces' the scope that has been defined and consented to by the user.
With my current level of understanding, I am assuming that these scopes ARE just for information purposes only, because I haven't seen any examples where the scopes are enforced through code.

Unable to get username in Azure B2C after successful authentication

I have a React SPA that communicates with the backend API (Azure Function App). I've created an app registration for both the SPA and the Azure Function App following the steps outlined here. Both app registrations are hosted in a separate directory from the Azure Function app since I'm using AD B2C. I'm able to successfully authenticate the user and make requests to the backend. I'm using PKCE as the auth protocol and MSAL.js to manage the authentication flow.
I've configured a standard signup/signin policy for which I'm using Local Account as the Identity provider and username as the user id.
Here's what the login screen looks like:
Here's the relevant code from the SPA which handles auth:
const { instance, accounts, inProgress,} = useMsal();
if (accounts.length > 0) {
msalInstance
.acquireTokenSilent({
account: accounts[0],
scopes: [
"https://APP_URI/user_impersonation",
],
})
.then((token) => {
console.log("token res is", token);
console.log("access token is", token);
})
.catch((err) => {
console.log("err is", err);
});
Here's the return value from calling acquireTokenSilent:
{
"authority":"https://APP_NAMEb2c.b2clogin.com/APP_NAMEb2c.onmicrosoft.com/b2c_1_flow/",
"uniqueId":"581776f4-6e16-454a-a6ae-ecb49f7f04aa",
"tenantId":"",
"scopes":[
"https://APP_NAMEb2c.onmicrosoft.com/reg_api/user_impersonation"
],
"account":{
"homeAccountId":"581776f4-6e16-454a-a6ae-ecb49f7f04aa-b2c_1_flow.07232d62-7285-4737-97eb-87f0f9b7c38e",
"environment":"APP_NAMEb2c.b2clogin.com",
"tenantId":"",
"username":"testUser#gmail.com",
"localAccountId":"581776f4-6e16-454a-a6ae-ecb49f7f04aa",
"name":"unknown",
"idTokenClaims":{
"exp":1663191498,
"nbf":1663187898,
"ver":"1.0",
"iss":"https://APP_NAMEb2c.b2clogin.com/07232d62-7285-4737-97eb-87f0f9b7c38e/v2.0/",
"sub":"581776f4-6e16-454a-a6ae-ecb49f7f04aa",
"aud":"473fe4d9-260b-46ad-9ad1-f4c4a4f211e6",
"nonce":"65c7ec69-2837-4bdf-b9e3-ae38dbb19c48",
"iat":1663187898,
"auth_time":1663187896,
"name":"unknown",
"emails":[
"testUser#gmail.com"
],
"tfp":"B2C_1_flow",
"at_hash":"qOHPceVj3fEhGGlRq6xh4g"
}
},
"idToken":"TD_TOKEN",
"idTokenClaims":{
"exp":1663191498,
"nbf":1663187898,
"ver":"1.0",
"iss":"https://APP_NAMEb2c.b2clogin.com/07232d62-7285-4737-97eb-87f0f9b7c38e/v2.0/",
"sub":"581776f4-6e16-454a-a6ae-ecb49f7f04aa",
"aud":"473fe4d9-260b-46ad-9ad1-f4c4a4f211e6",
"nonce":"65c7ec69-2837-4bdf-b9e3-ae38dbb19c48",
"iat":1663187898,
"auth_time":1663187896,
"name":"unknown",
"emails":[
"testUser#gmail.com"
],
"tfp":"B2C_1_flow",
"at_hash":"qOHPceVj3fEhGGlRq6xh4g"
},
"accessToken":"ACCESS_TOKEN",
"fromCache":true,
"expiresOn":"2022-09-14T21:38:18.000Z",
"correlationId":"9c71acbb-7ed4-4beb-a282-71ec7d924bd8",
"extExpiresOn":"2022-09-14T21:38:18.000Z",
"familyId":"",
"tokenType":"Bearer",
"state":"",
"cloudGraphHostName":"",
"msGraphHost":"",
"fromNativeBroker":false
}
As you can see, the username property has the emailAddress as it's value and not the actual username.
I've not been able to find concrete guidance on how to get the username. The one resource I found said that UserPrincipleName(UPN) is an optional claim and to add this value in the authToken I should add UPN as an optional claim in the token configuration tab, which is not available in B2C AD. I would love to get some guidance on what I'm doing wrong as getting the username should not be this hard, right ?
Edit 1: I can confirm that the username has been set; in the image below the username is denoted by User Principle Name:
The username property in MSAL's AccountInfo object is populated by the email claim in the ID token. The email claim will be an array, and if there are multiple emails, MSAL will only use the first one as username.
To receive UserPrincipleName (UPN) in the ID token, you'll need to set the user attributes in your B2C tenant. Unfortunately this doesn't seem to be possible with standard user-flows, so you'll need to build a custom policy and sign-in with that instead. See for more: User profile attributes

msal.js 2.0 tokenResponse null after loginRedirect

I am developing an Angular 10 app that utilizes Azure B2C for policy and user management. I set up my app registration in Azure Active Directory as a singlepage app without the implicit option checked. I am using msal.js 2.0 #azure/msal-browser to log into B2C and retrieve id and access tokens using code flow. I set up my configuration, created the msal object, defined the redirect promise, then later call loginRedirect with the appropriate user scopes. The page redirects properly.
However, after I sign in the tokenResponse comes back as null. I have tried altering the authority and scopes, but it always comes back as null. How do I get the handleRedirectPromise to return a valid token response?
Here's my code:
private msalConfig: Msal.Configuration = {
auth: {
clientId: xxxx-xx-xx-xx-xxxxx,
authority: 'https://login.microsoftonline.com/common',
redirectUri: 'https://localhost:4200'
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false
},
};
private loginRequest: Msal.RedirectRequest = {
scopes: ['user.read'],
};
const msalInstance = new Msal.PublicClientApplication(this.msalConfig);
msalInstance
.handleRedirectPromise()
.then((tokenResponse: Msal.AuthenticationResult) => {
let accountObj = null;
if (tokenResponse !== null) {
accountObj = tokenResponse.account;
const id_token = tokenResponse.idToken;
const access_token = tokenResponse.accessToken;
console.log('id_token', id_token);
console.log('access_token', access_token);
}
})
.catch(error => {
authStore.loginError$.next(true);
console.error(error);
});
msalInstance.loginRedirect(this.loginRequest);
Edit:
I have also tried authority: `https://<tenant-name>.b2clogin.com/<tenant-name>.onmicrosoft.com/<policy-name> and https://login.microsoftonline.com/tfp/{tenant}.onmicrosoft.com/B2C_1_SiupIn for the authority in the msalConfig object as well as scopes: ['openid'] in the loginRequest. When I use this I get the following error in the browser when I try to log in:
zone-evergreen.js:1068 GET https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://<tenant>.b2clogin.com/<tenant>.onmicrosoft.com/b2c_1_defaultsigninsignup/oauth2/v2.0/authorize 400 (Bad Request)
core.js:4197 ERROR Error: Uncaught (in promise): ClientAuthError: endpoints_resolution_error: Error: could not resolve endpoints. Please check network and try again. Detail: ClientConfigurationError: untrusted_authority: The provided authority is not a trusted authority. Please include this authority in the knownAuthorities config parameter.
ClientAuthError: endpoints_resolution_error: Error: could not resolve endpoints. Please check network and try again. Detail: ClientConfigurationError: untrusted_authority: The provided authority is not a trusted authority. Please include this authority in the knownAuthorities config parameter.
The way you set up the redirect flow seems correct. You first have to call the handleRedirectPromise() (which registers it), and then call the loginRedirect(). At page load handleRedirectPromise() will return null, and after sign-in it should return the token.
There are issues with your configuration, however.
You need to designate your domain as a knownAuthority, like:
auth: {
clientId: 'xxxx-xx-xx-xx-xxxxx',
authority: 'https://<tenant-name>.b2clogin.com/<tenant-name>.onmicrosoft.com/<policy-name>',
knownAuthorities: ['<your-tenant-name>.b2clogin.com']
redirectUri: 'https://localhost:4200'
},
User.Read is a MS Graph API scope. You cannot use it with B2C. Only the OIDC scopes are allowed i.e. use openid instead.
See this for more.
The problem was with my angular app. I had my app redirecting the base url to my /home route. So whenever you open the base route the app is redirected. From this route, the request is made. I added the redirect uri for the /home route to my AAD app registration, commented out the redirectUri in my b2c configuration and set navigateToLoginRequestUrl to true.

Change from address while sending email via gmail API

I want to send a mail via the gmail API. I have a function that works so far, but my problem is that I don't know how to change the from address. My mails are always send as the user I authorized the API access with.
So I want my mails sent from from.mail#example.com in the following code:
function sendSampleMail(auth, cb) {
let gmailClass = google.gmail('v1');
let emailLines = [];
emailLines.push('From: from.mail#example.vom');
emailLines.push('To: to.mail#example.com');
emailLines.push('Content-type: text/html;charset=iso-8859-1');
emailLines.push('MIME-Version: 1.0');
emailLines.push('Subject: this would be the subject');
emailLines.push('');
emailLines.push('And this would be the content.<br/>');
emailLines.push('The body is in HTML so <b>we could even use bold</b>');
const email = emailLines.join('\r\n').trim();
let base64EncodedEmail = new Buffer.from(email).toString('base64');
base64EncodedEmail = base64EncodedEmail.replace(/\+/g, '-').replace(/\//g, '_');
gmailClass.users.messages.send(
{
auth: auth,
userId: 'me',
resource: {
raw: email
}
},
cb
);
}
I don't know if it's even possible to send with different from-mails via the google API. I could not find any information about that and no solutions.
You can't send emails (or make any request) as a different user from the authenticated user because you don't have any permissions to impersonate it.
If you're a G Suite administrator, you can use a service account [1][2] with domain wide delegation [3], which will grant you access to impersonate any gmail account from your domain. You can create a service account on Cloud Platform after selecting a project, from where you'll be able to download the credentials JSON file. And use the JSON Web Tokens methods [4] to authorize your application using the service account JSON.
[1] https://cloud.google.com/iam/docs/service-accounts
[2] https://cloud.google.com/iam/docs/understanding-service-accounts
[3] https://developers.google.com/admin-sdk/directory/v1/guides/delegation
[4] https://github.com/googleapis/google-auth-library-nodejs#json-web-tokens

In the groups claims of an AAD access token, why do I get a GUID that is not the objectId of any group in the AAD tenant?

Using a modified version of the Microsoft MSAL quickstart for node.js (original here), I successfully received an access token for the Azure Storage API using the implicit flow. The token included a groups claim, but one of the GUIDs in the claim does not seem to correlate with any group in the tenant. After removing the user from every group, the claim still contains that GUID (and as expected no others anymore):
"groups": [
"2cb3a5e8-4606-4407-9a97-616246393b5d"
],
A Google search for that GUID didn't result in any hits, so I'm assuming it is not a well-known GUID of some sort.
Why do I get this "unknown" GUID in a group claim?
The AAD tenant involved is a very small tenant, exclusively used by me for learning AAD and authentication. As such, it only contains a single group. The user involved is not a member of this single group.
I've looked at user page in the Azure Portal, which indeed shows that the user is "not a member of any groups". Azure CLI also show that the user isn't a member of any group:
$ az ad user get-member-groups --upn jurjen#stupendous.org
[]
$
The full list of groups in this tenant contains just a single group, and as you can see its ObjectID does not match the GUID I get in the claim:
$ az ad group list --query [].objectId --output tsv
b1cc46de-8ce9-4395-9c7c-e4e90b3c0036
$
I've also created another application registration and have it expose a dummy API. When using that dummy API as scope I again successfully receive
an access token, but this one again includes the same unknown GUID as the single group claim.
Here are the hopefully relevant bits of the code.
As mentioned above, first I retrieved an access token for Azure Storage:
var requestObj = {
scopes: ["https://storage.azure.com/user_impersonation"]
};
... but I get the exact same result with a dummy API:
var requestObj = {
scopes: ["api://7c7f72e9-d63e-44b6-badb-dd0e43df4cb1/user_impersonation"]
};
This bit logs the user in:
function signIn() {
myMSALObj.loginPopup(requestObj).then(function (loginResponse) {
//Successful login
showWelcomeMessage();
acquireTokenPopup();
}).catch(function (error) {
//Please check the console for errors
console.log(error);
});
}
The token is acquired here. I'm aware that callMSGraph won't work here given the scope of the token. I get the token from the browser console log and decode it using jwt.ms.
function acquireTokenPopup() {
//Always start with acquireTokenSilent to obtain a token in the signed in user from cache
myMSALObj.acquireTokenSilent(requestObj).then(function (tokenResponse) {
console.log("Access Token from cache: " + JSON.stringify(tokenResponse.accessToken));
callMSGraph(graphConfig.graphMeEndpoint, tokenResponse.accessToken, graphAPICallback);
}).catch(function (error) {
console.log(error);
// Upon acquireTokenSilent failure (due to consent or interaction or login required ONLY)
// Call acquireTokenPopup(popup window)
if (requiresInteraction(error.errorCode)) {
myMSALObj.acquireTokenPopup(requestObj).then(function (tokenResponse) {
console.log("Access Token after interaction: " + JSON.stringify(tokenResponse.accessToken));
callMSGraph(graphConfig.graphMeEndpoint, tokenResponse.accessToken, graphAPICallback);
}).catch(function (error) {
console.log(error);
});
}
});
}
You will also get directoryRole ids in the groups(got from the access token). You can request https://graph.microsoft.com/v1.0/me/memberOf to check the details. Here is the graph explorer.

Resources