msal acquireTokenSilent fails with Silent authentication was denied - graph

I'm using msal-angular and I cant use MsalInterceptor since it handles each and every request while I would like it to handle only graph requests.
Therefore I'm trying to acquire a token by myself in my application.
I'm doing this like this:
configuration of MsalModule
MsalModule.forRoot({
auth: {
clientId: 'xxxxxx',
authority: 'https://login.microsoftonline.com/common',
redirectUri: 'my_url',
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: false, // set to true for IE 11
},
},
{
popUp: true,
consentScopes:[
'user.read',
"calendars.read",
"calendars.read.shared",
"calendars.readwrite",
"group.read.all",
"openid",
"profile"
],
}),
I'm doing a login like this:
this.msalService.loginPopup({scopes: [
'user.read',
"calendars.read",
"calendars.read.shared",
"calendars.readwrite",
"group.read.all",
"openid",
"profile"
]})
I'm intercepting the login succes like this
this.broadcastService.subscribe('msal:loginSuccess' ...
and I'm trying to get a token like this
const result: AuthResponse = await this.msalService.acquireTokenSilent({scopes: [
'user.read',
"calendars.read",
"calendars.read.shared",
"calendars.readwrite",
"group.read.all",
"openid",
"profile"
]}).catch((error) => {
console.log(error);
});
and I'm still getting this error: Silent authentication was denied. The user must first sign in and if needed grant the client application access to the scope 'user.read calendars.read calendars.read.shared calendars.readwrite group.read.all openid profile'.
here is my app configuration on Azure portal
I really don't understand where does the problem come from.
Thanks for your help
[EDIT]
I've been able to get an access token by calling acquireTokenPopup when acquireTokenSilent fails like this
const result: AuthResponse = await this.msalService.acquireTokenSilent({
scopes: [
'user.read',
"calendars.read",
"calendars.read.shared",
"calendars.readwrite",
"group.read.all",
"openid",
"profile"
]
}).catch((error) => {
if (error.name === "InteractionRequiredAuthError") {
return this.msalService.acquireTokenPopup({
scopes: [
'user.read',
"calendars.read",
"calendars.read.shared",
"calendars.readwrite",
"group.read.all",
"openid",
"profile"
]
})
}
});
return result.accessToken;
}
It works but keeps flasing a popup.
Could someone explain me the reason ?
Thank you

You must enforce the consent popup to show up.
Try this
this.msalService.loginPopup({ prompt: 'consent'});

I have the similar problem and just use following code to get the exception.
this.broadcastService.subscribe('msal:acquireTokenFailure', (payload) => {
console.log(payload);
});
InteractionRequiredAuthError: Silent authentication was denied. The user must first sign in and if needed grant the client application access to the scope 'api://1cfd57f1-ad48-4396-b95c-7fa782a178b6/access_as_user openid profile'.
In the document, it says:
InteractionRequiredAuthError: Error class, extends ServerError to
represent server errors, which require an interactive call. This error
is thrown by acquireTokenSilent if the user is required to interact
with the server to provide credentials or consent for
authentication/authorization. Error codes include
"interaction_required", "login_required", and "consent_required"
For error handling in authentication flows with redirect methods (loginRedirect, acquireTokenRedirect), you'll need to register the callback.
https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-handling-exceptions?tabs=javascript
So I add "this.msalService.loginRedirect()" to the callback method to solve my problem. I also not quite understand, why should grant the permission again, but when man check the implementation of "MsalGuard" (https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/src/msal-guard.service.ts), it executes the same code "loginRedirect", if the exception is thrown by acquiring token.
By the way, I have also added Client Application ID to my API "Expose an API" in to authorize the client without to be asked to consent. The exception is fixed in my app finally.

Related

next-auth x Microsoft Graph API: Where to get the accessToken

I'm building an email marketing automation tool using NextJS, next-auth and Microsoft Graph API. I'm using next-auth's Azure AD B2C provider to authenticate users, and I've been following their docs.
Within the Configuration (Advanced) section of the docs, I've followed the steps to setup an Azure AD api app to communicate with the Microsoft Graph API (to send email on our user's behalf). Now, when a user signs up, an access_token (jwt) is added to my accounts db table. Here it is decoded:
{
"iss": "https://something.b2clogin.com/b03...f94/v2.0/",
"exp": 1664588154,
"nbf": 1664584554,
"aud": "6eb...c5b",
"idp_access_token": "EwB...QI=",
"idp": "live.com",
"name": "Will Despard",
"sub": "1f7...d6c",
"emails": [
"willdespard#outlook.com"
],
"tfp": "B2C_1_signupsignin",
"scp": "mail.send",
"azp": "ff8...f5d",
"ver": "1.0",
"iat": 1664584554
}
The problem is, there is no example of how to setup the Microsoft Graph JS Client with next-auth. For example, according to Microsoft, to create a Microsoft Graph API client, you must do the following:
import { Client } from '#microsoft/microsoft-graph-client';
const client = Client.init({
authProvider: (done) =>
done(
null,
accessToken // WHERE DO WE GET THIS FROM?
),
});
const sendMail = {
message: {
subject: 'Meet for lunch?',
body: { contentType: 'Text', content: 'The new cafeteria is open.' },
toRecipients: [
{ emailAddress: { address: 'william.cm.despard#gmail.com' } },
],
},
};
const userDetails = await client.api('/me/sendMail').post(sendMail);
However, the following is unclear:
Where are we meant to get the accessToken used in this example from? I've tried using the idp_access_token in the decoded accessToken on my accounts db table (above), but this doesn't seem to work.
I'm assuming the accessToken we use to communicate with Microsoft Graph API is going to expire after a short amount of time. How do we handle getting a new token?
Help/code examples would be much appreciated!
I would try it like this. First, it looks that for graph access you should be looking for Azure AD provider, not Azure AD B2C that is a service that provides identity providers. I.e. looks like you need this one: https://next-auth.js.org/providers/azure-ad
To use Microsoft Graph to send mail you'll also need to request a non-default scope with "Send Mail" grant from your user. Means, when authorizing your app the user will be asked to consent that your app will send emails on behalf of him. Also you'll need to save the graph access token you get from the authentication flow. Something like this:
import AzureADProvider from "next-auth/providers/azure-ad"
export const authOptions: NextAuthOptions = {
providers: [
....
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
authorization: {
params: {
scope:
"openid email profile Mail.Send",
},
},
// tenantId: process.env.AZURE_AD_TENANT_ID,
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
}
return token
},
Please note that if you do not specify tenantId that would mean that your application will be available for users from any tenant, but that in turn would mean that you must be a verified publisher (i.e. must have a valid MPN ID associated with your app). If you do specify a tenantId, then your app will only work for users from that specified tenant.
Later on, you could just use the token from the API:
import { getToken } from 'next-auth/jwt';
import { Client } from '#microsoft/microsoft-graph-client';
// some API function
export default async function handler(req, res) {
const token = await getToken({ req })
if (token) {
const accessToken = token.accessToken;
const client = Client.init({
authProvider: (done) =>
done(null, accessToken)
});
const sendMail = {
message: {
subject: 'Meet for lunch?',
body: { contentType: 'Text', content: 'The new cafeteria is open.' },
toRecipients: [
{ emailAddress: { address: 'william.cm.despard#gmail.com' } },
],
},
};
const userDetails = await client.api('/me/sendMail').post(sendMail);
...

Unable to add chrome-extension "url" to firebase whitelisted domains

I've been loosely following the boilerplate in quickstart-js. I don't want to rely on Chrome's identify provider but rather want users to be able to sign in to my extension with their Google login using a popup so I haven't gone through the song and dance of requesting identity permissions in my manifest.json. My file is as follows:
{
"manifest_version": 2,
"name": "Firebase Auth in Chrome Extension Sample",
"description": "This sample shows how to authorize Firebase in a Chrome extension using a Google account.",
"version": "2.1",
"icons": {
"128": "firebase.png"
},
"browser_action": {
"default_icon": "firebase.png",
"default_popup": "credentials.html"
},
"background": {
"page": "background.html"
},
"content_security_policy":"script-src 'self' https://apis.google.com https://www.gstatic.com/ https://*.firebaseio.com https://www.googleapis.com; object-src 'self'"
}
I have baseline code that is similar to what's in quickstart-js. The relevant portion in my credentials.js is here:
/**
* Start the auth flow and authorizes to Firebase.
*/
async function startAuth() {
await firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION);
const provider = new firebase.auth.GoogleAuthProvider();
const res = await firebase.auth().signInWithPopup(provider);
}
// Starts the sign-in process.
function startSignIn() {
document.getElementById('quickstart-button').disabled = true;
if (firebase.auth().currentUser) {
firebase.auth().signOut();
} else {
startAuth();
}
}
window.onload = function() {
initApp();
};
This seems like it should work but constantly receive the following message:
Uncaught (in promise) Error: This chrome extension ID (chrome-extension://cckmbfklaloiadcphibealkhpncehpng) is not authorized to run this operation. Add it to the OAuth redirect domains list in the Firebase console -> Auth section -> Sign in method tab.
According to the official docs, I should be able to whitelist my Chrome extension's ID in the Firebase control panel. I'm repeatedly given a success message but the extension "url" doesn't show up in my list of Authorized Domains and I keep getting the error message.
Is there somewhere else I need to add the Chrome Extension url?
This seems to have just been a regression. I reached out to Firebase support, got an answer a few days later, but by that point the bug was fixed.

Firebase.auth error with 400 bad request

I found a strange error while I developing system using Firebase with service url contains user data.
User data is below.
{
"uid": "kt9Hcp2FbYbBvvIeSHHa1RbvHcv2",
"displayName": "Anonymous 901",
"photoURL": null,
"email": null,
"emailVerified": false,
"identifierNumber": null,
"isAnonymous": true,
"providerData": [
],
"apiKey": "MyApiKeyString",
"appName": "MyAppName",
"authDomain": "my.auth.domain",
"stsTokenManager": {
"apiKey": "MyApiKeyString",
"refreshToken": "refreshTokenString",
"accessToken": "accessTokenString",
"expirationTime": 1532451863076
},
"redirectEventId": null
}
I encode the above anonymous user data and include it in the service url.
( http://myserviceurl?userdata=encodedUserData )
Inside the system receives that url, firebase creates a user object with that user data contained in the url.
The purpose of this url is to use specific user's information in any browser.
However, when I call that service url, sometimes system creates user object well, sometimes got error -
400 Bad request errors with
https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccountInfo?key=MyApiKeyString
And error data is below,
{
"error": {
"code": 400,
"message": "TOKEN_EXPIRED",
"errors": [
{
"message": "TOKEN_EXPIRED",
"domain": "global",
"reason": "invalid"
}
]
}
}
Few hours later it works well, I changed nothing though.
I could not find the exact error point, but I suspect error occurs while observing authentication state or before this step.
Here is code snipets
#bind
private makeUserLoadingPromise(): Promise<void> {
let unSubscribe: () => void;
return new Promise<void>((resolve, _reject) => {
const onInitialized = this.makeOnInitializedAuthStateChanged(resolve);
unSubscribe = this.auth.onAuthStateChanged(onInitialized);
}).then(() => {
unSubscribe();
this.auth.onAuthStateChanged(this.onAuthStateChanged);
});
}
#bind
private makeOnInitializedAuthStateChanged(resolve: () => void) {
return (user: firebase.User | null) => {
this.user = user;
resolve();
};
}
#bind
private onAuthStateChanged(user: firebase.User | null) {
this.user = user;
}
Or maybe it relates with expirationTime?
I couldn't find any hints about this situation.
Any advice would be appreciated.
It is not clear what you are doing, but it appears that you are using the API incorrectly and insecurely. The plain user object contains a refresh token that is indefinite. Passing it around via URL is a really bad idea.
First don't rely on internal implementations, it is subject to change.
To get the user's information on your backend, the right way to do it, is to get the user's ID token using officially supported API, eg user.getIdToken(), then pass it to your server.
On your server, you verify it via the Firebase Admin SDK: admin.auth().verifyIdToken(idToken). Then you know this is a real authenticated user. If you need the full user info, you can then look it up using the decoded user id in the token: admin.auth().getUser(decodedIdToken.sub).

Not able to maintain user authentication session firebase

I am using email & password authentication to logging in user, from my firebase dashboard i have set session expiration time to 2 months . However when i am closing my app from background and then after reopening of app i am getting var user = ref.getAuth(); as null
Does firebase does't take care of this? How to keep user logged in for a long period of time?
Below is the piece of code i am using to login user. I am using react-native
ref.authWithPassword({
email : 'username',
password : 'password'
}, function(error, authData) {
if (error) {
console.log("Login Failed!", error);
} else {
navigatorReference.push({name:'myFeed'})
console.log("Authenticated successfully with payload:", authData);
}
});
Firebase should take care of this. Double check your configuration in the Login & Auth tab in your App Dashboard to make sure you have that setup properly.
You could also try passing along the configuration like so...
ref.authWithPassword({
email : 'username',
password : 'password'
}, function(error, authData) { /* Your Code */ }, {
remember: "default"
});

node.js, passport-wordpress: The required "redirect_uri" parameter is missing

Trying to create a demo using passport-wordpress
https://www.npmjs.org/package/passport-wordpress
passport-wordpress allows you to login to a node.js app using your credentials at wordpress.com
I set up my Wordpress app at developer.wordpress.com/apps:
OAuth Information
Client ID <removed>
Client Secret <removed>
Redirect URL http://greendept.com/wp-pass/
Javascript Origins http://wp-node2.herokuapp.com
Type Web
Request token URL https://public-api.wordpress.com/oauth2/token
Authorize URL https://public-api.wordpress.com/oauth2/authorize
Authenticate URL https://public-api.wordpress.com/oauth2/authenticate
In my node.js app:
var CLIENT_ID = <removed>;
var CLIENT_SECRET = <removed>;
passport.use(new WordpressStrategy({
clientID: CLIENT_ID,
clientSecret: CLIENT_SECRET
},
function(accessToken, refreshToken, profile, done) {
User.findOrCreate({ WordpressId: profile.id }, function (err, user) {
return done(err, user);
});
}
When I try to authorize, it goes to this URL (as one line, I've divided into two here for readability):
https://public-api.wordpress.com/oauth2/authorize?
response_type=code&redirect_uri=&client_id= removed
I can see that the redirect_uri is missing in that URL, so it's not surprising that I get this error:
Invalid request, please go back and try again.
Error Code: invalid_request
Error Message: The required "redirect_uri" parameter is missing.
Not sure where or how in my code I should be submitting the redirect_uri.
You need to pass a callback url as option.
From passport-wordpress
The strategy requires a verify callback, which accepts these credentials and
calls done providing a user, as well as options specifying a client ID,
client secret, and callback URL.
And from lib/strategy.js
Examples:
passport.use(new WordpressStrategy({
clientID: '123-456-789',
clientSecret: 'shhh-its-a-secret',
callbackURL: 'https://www.example.net/auth/wordpress/callback'
},
function(accessToken, refreshToken, profile, done) {
User.findOrCreate(..., function (err, user) {
done(err, user);
});
}
));

Resources