Azure AD When to use API Permissions vs Expose an API - javascript

I am currently developing a react web app that will use Microsoft's MSAL package to authenticate users to ensure only users within our tenant may access the Api.
I've built a http function app called TARGET_APP with a python function that accesses our data and returns it. I registered it to our Azure AD enterprise applications.
Now according to the documentation for proper "On Behalf Of" calls to work I am to register another app to represent my react client app, called CALLER_APP I registered this as well, and set up the scopes I need which include email, user.read, and the TARGET_APP's exposed Api.
Example of my CALLER_APP permissions here:
However when attempting to authorize with the CALLER_APP from the client, via MSAL with the scopes in the image, I get a prompt saying "Admin consent required"
Snippet from my authentication flow (handleLogin is the initiating function called) :
const msalConfig = {
auth: {
authority: "https://login.microsoftonline.com/MY_TENANT/",
clientId: "CALLER_APP_CLIENT_ID",
redirectUri,
postLogoutRedirectUri: redirectUri
},
cache: {
cacheLocation: "localStorage"
}
}
// NOTE I have subbed out my actual caller scope with "CALLER_APP_SCOPE" for this post
const loginRequest = {
scopes: ["CALLER_APP_SCOPE", "user.read", "email"]
};
async function handleLogin(instance) {
const loginUrl = await getLoginUrl(instance, loginRequest);
const loginResult = await launchWebAuthFlow(instance, loginUrl);
// Acquire token
const { accessToken } = await acquireToken(instance, loginRequest);
console.log(accessToken)
}
/**
* Generates a login url
*/
async function getLoginUrl(instance, request) {
return new Promise((resolve, reject) => {
instance.loginRedirect({
...request,
onRedirectNavigate: (url) => {
resolve(url);
return false;
}
}).catch(reject);
});
}
/**
* Generates a login url
*/
async function launchWebAuthFlow(instance, url) {
return new Promise((resolve, reject) => {
chrome.identity.launchWebAuthFlow({
interactive: true,
url
}, (responseUrl) => {
// Response urls includes a hash (login, acquire token calls)
if (responseUrl.includes("#")) {
instance.handleRedirectPromise(`#${responseUrl.split("#")[1]}`)
.then(resolve)
.catch(reject)
} else {
// Logout calls
resolve();
}
})
})
}
/**
* Attempts to silent acquire an access token, falling back to interactive.
*/
async function acquireToken(instance, request) {
return instance.acquireTokenSilent(request).then((response) => {
console.log(response.accessToken);
}).catch(async (error) => {
console.error(error);
storage.set({'loggedState': false});
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Most of this code is taken directly from the documentation,
Calling the handleLogin function initiates the authentication flow successfully, however when I log in with my Microsoft credentials I receive the "App needs permission to access resources in your organisation that only an admin can grant" popup.
I double checked my scopes and ensured none require admin consent, as well as I have gone into the enterprise apps user consent and permissions settings and enabled user consent on low impact scopes as you can see here:
Enterprise Setting:
The "3 permissions classified as low impact" are the 3 scopes described above (email, user.read, allow-caller)
However,
If I go to the "Expose an API" blade instead for the CALLER_APP and make a scope there, and use that scope in the MSAL call instead, authentication goes through fully, I get a bearer token, and I am able to use the API for what I need.
This method is not mentioned in the documentation, nor any of the readings I've looked into though.
I was wondering if I could get help in understand why I shouldn't use "Expose an API" for my case, as well as why it requires admin consent?

Usually the permissions in the API permissions are selected where
user.read , email are graph permissions and when you mention
User.Read while calling msal it indirectly means
https://graph.microsoft.com/User.read which is the basic permission
to sign in user to read users profile and mail.
But the scope for calling your web api is created by you and it has different AppId or say App ID URI for different applications and its scope needs to be defined uniquely for that App to access that.
So actual scopes for that app to access the Api are exposed in expose an api blade which is the scope of the App to access.
NOTE:Actual full value/string of the Scope is the concatenation of your web API's Application ID URI and Scope name of scope. The
App ID URI acts as the prefix for the scopes you'll reference in your
API's code, and it must be globally unique.
For example,
if your web API's application ID URI is https://contoso.com/ and the
scope name is Employees.Read.All, the full scope is:
https://contoso.com/Employees.Read.All or
api://<application-client-id>/allow-caller in your case.
And coming to the point that it is asking admin consent is , when
there is no scope that actually means full string scope
api:///allow-caller , only mentioning
allow-caller is totally different scope and this new scope may
require consent from admin as it is not exposed for that particular
API.
Also you can add a client application in expose an api blade in case you don’t want to see the admin consent as the "authorized client applications" is used when you basically want to preauthorize users without admin consent being required to access that api ,If not it will prompt users for consent if needed.
Please check the below image:
References:
quickstart-configure-app-expose-web-api(github)
azure-expose an Api vs Api-permissions(stackOverflow)

Related

endpoints_resolution_error in msal react

I was trying to acquire token from our Microsoft tenant. I have no knowledge about the Azure AD or whatsoever, because I only tasked to develop front end for our Microsoft Dynamics App in React. I only got some of the credential like tenant id, client id, client secret and resource.
I used MSAL Node library and function ConfidentialClientApplication() to acquire the token
But when I check it in the Ms. Edge's console log it throw an error
{"errorCode":"endpoints_resolution_error","errorMessage":"Error: could
not resolve endpoints. Please check network and try again. Detail:
ClientAuthError: openid_config_error: Could not retrieve endpoints.
Check your authority and verify the .well-known/openid-configuration
endpoint returns the required endpoints. Attempted to retrieve
endpoints from: verify
url","subError":"","name":"ClientAuthError","correlationId":""}
When I click the veryfy url (Cannot show you the url because it might contain sensitive information)
It shows all the metadata of the open id so I thought maybe it's normal.
But why is the error endpoints_resolution_error throwed when everything is normal?
Here is some snapshot of my code
const config = {
auth: {
clientId: clientID
authority: "https://login.microsoftonline.com/{tenantID}/",
clientSecret: clientSecret,
knownAuthorities: ["login.microsoftonline.com"],
protocolMode: "OIDC"
}
};
// Create msal application object
const cca = new msal.ConfidentialClientApplication(config);
// With client credentials flows permissions need to be granted in the portal by a tenant administrator.
// The scope is always in the format "<resource>/.default"
const clientCredentialRequest = {
scopes: ["resource/.default"], // replace with your resource
};
cca.acquireTokenByClientCredential(clientCredentialRequest).then((response) => {
console.log("Response: ", response);
}).catch((error) => {
console.log(JSON.stringify(error));
});
I've tried changing the authority and the protocol mode several times, but same result

How To Setup Custom Claims In My React Website For a Login Page

I want to set up custom claims to a certain number of users let's say 5 users would be admins on my website. I want these 5 users to be able to log in through the login page which would redirect them to the dashboard.
but I still don't fully understand the concept of the custom claims and how to use them and firebase documentation is limited with examples.
In their example they show that I can pass a uid that I want to assign a custom claim to, but how is this supposed to be a variable when i want certain users uid's from my firestore database Users collection to be admins and have a custom claim, in other words, where would I put this code or how would I assign a custom claim to more than one user at a time and how and where would this code be executed.
if anyone can give me an example of how I would make this work.
here is what I did:
created a firebaseAdmin.js file:
var admin = require("firebase-admin");
// lets say for instance i want these two users to be admins
//2jfow4fd3H2ZqYLWZI2s1YdqOPB42
//2jfow4vad2ZqYLWZI2s1YdqOPB42 what am i supposed to do?
admin
.auth()
.setCustomUserClaims(uid, { admin: true })
.then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
I honestly don't know what to do from here.
Custom Claims can only be set from a privileged server environment via the Firebase Admin SDK. The easiest ways are either using a Node.js script (running the Admin SDK) or a Cloud Function (which also uses the Admin SDK).
Let's look at the example of a Callable Cloud Function that you call from your front-end (and in which you could check the UID of the user who is calling it, i.e. a Super Admin).
exports.setAdminClaims = functions.https.onCall(async (data, context) => {
// If necessary check the uid of the caller, via the context object
const adminUIDs = ['2jfow4fd3H2ZqYLWZI2s1YdqOPB42', '767fjdhshd3H2ZqYLWZI2suyyqOPB42'];
await Promise.all(adminUIDs.map(uid => admin.auth().setCustomUserClaims(uid, { admin: true })));
return { result: "Operation completed" }
});
A Node.js script would be similar:
#!/usr/bin/node
const admin = require('firebase-admin');
admin.initializeApp({
credential: admin.credential.cert(".....json") // See remark on the private key below
});
const adminUIDs = ['2jfow4fd3H2ZqYLWZI2s1YdqOPB42', '767fjdhshd3H2ZqYLWZI2suyyqOPB42'];
Promise.all(adminUIDs.map(uid => admin.auth().setCustomUserClaims(uid, { admin: true })))
.then(() => {
console.log("Operation completed")
})
You must generate a private key file in JSON format for your service account , as detailed in the doc.
Then, when the Claims are set, you can access these Claims in your web app, and adapt the UI (or the navigation flow) based on the fact the user has (or not) the admin claim. More detail here in the doc.

Request had insufficient authentication scopes javascript

I already have a project to query google calendar apis.
I wanted to go further by querying google mail apis.
In my project I have activated mail API
I have added discoveryDocs in my javascript app like this
[
"https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",
"https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"
]
And scopes like this
"https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events https://mail.google.com/"
Initialization of my client is done like this:
initClient(): void {
gapi.client.init({
apiKey: 'ma_api_key',
clientId: 'my_client_id.apps.googleusercontent.com',
discoveryDocs: [
"https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",
"https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"
],
scope: 'https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events https://mail.google.com/',
}).then(() => {
console.log('ok')
});
}
I still can get my calendars and events but I can not get my labels with this code:
getMails(): void {
console.log(gapi);
gapi.client.gmail.users.labels.list({
'userId': 'me'
}).then(function(response) {
var labels = response.result.labels;
console.log(labels);
});
}
What am I missing please ?
Thanks
Request had insufficient authentication scopes javascript
Means that the user who you have authentication with has authorized your application to use some scopes but they are not the scopes, but you are trying to use a method which requires addental scopes then the user has authorized your application for.
This error normally occurs when you authorize your application once then change the scopes and run it again, if your application still has a session var or cookies from the previous authorization request then your application will run without requesting access of the user and adding the additional scopes.
You need to revoke the access token or force the application show the consent screen again

Why can't I use `allAuthenticatedUsers` for my Firebase Cloud Function?

When deploying Firebase Functions using the Firebase CLI, they are configured so that the Cloud Functions Invoker permission is granted to allUsers. With such a setting the code below functions as expected.
The Cloud Functions Invoker permission can also be granted to allAuthenticatedUsers. However, when I implement this change for addMessage, I only ever get a UNAUTHENTICATED error response using the code below.
Why won't allAuthenticatedUsers work for this Firebase Cloud Function?
Note: This Q&A is a result of a now-deleted question posted by Furkan Yurdakul, regarding why allAuthenticatedUsers wasn't working with his Firebase Callable Function for his Firebase app
MWE based on the documentation, with addMessage defined here:
firebase.auth().signInAnonymously() // for the sake of the MWE, this will normally be Facebook, Google, etc
.then((credential) => {
// logged in successfully, call my function
const addMessage = firebase.functions().httpsCallable('addMessage');
return addMessage({ text: messageText });
})
.then((result) => {
// Read result of the Cloud Function.
const sanitizedMessage = result.data.text;
alert('The sanitized message is: ' + sanitizedMessage);
})
.catch((error) => {
// something went wrong, keeping it simple for the MWE
const errorCode = error.code;
const errorMessage = error.message;
if (errorCode === 'auth/operation-not-allowed') {
alert('You must enable Anonymous auth in the Firebase Console.');
} else {
console.error(error);
}
});
Simply put, if the ID token passed to a Cloud Function represents a Google account (that used Google Sign-In through Firebase or Google itself), it works, otherwise, it doesn't.
Think of allAuthenticatedUsers as allAuthenticatedGoogleUsers instead of allAuthenticatedFirebaseUsers.
Background Information
For Callable Firebase Functions used with the Firebase Client SDKs, you will normally grant allUsers the permission to call it (the default setting Firebase CLI deployed functions).
A valid authenticated client request for a Google Cloud Functions must have an Authorization: Bearer ID_TOKEN header (preferred) or ?access_token=ID_TOKEN. Here, ID_TOKEN is a signed-in Google user's ID token as a JWT.
When Firebase Client SDKs call a Callable Function, they set the Authorization header for you with the current user's ID token (if the user is signed in, here). This is done so that the user's authentication token can be used in the context parameter of onCall() functions. Importantly though, a Firebase user's ID token doesn't always represent a Google user which makes it incompatible with allAuthenticatedUsers.
Because of this, you will have to gate your callable function in your code by checking context.auth and it's properties like below.
export const addMessage = functions.https.onCall((data, context) => {
if (!context.auth) {
// Throwing a HttpsError so that the client gets the error details.
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called while authenticated.'
);
}
// a valid user is logged in
// do work
});
Addendum on 403 Forbidden Errors
If your function is consistently throwing a 403 error after being deployed, this is likely because you are using an outdated copy of the Firebase CLI, as highlighted in the documentation:
Caution: New HTTP and HTTP callable functions deployed with any Firebase CLI lower than version 7.7.0 are private by default and throw HTTP 403 errors when invoked. Either explicitly make these functions public or update your Firebase CLI before you deploy any new functions.

Unable to restrict access to AWS API even after calling globalsignout Method using javascript

I am using AWS API gateway for API's and cognito UserPool's for security. After Authenticating the user we will get tokens and I am using that token to authorise my API.
Now, I am trying to enable signout to cognito authorised users using javascript. Used the below code.
if (cognitoUser != null) {
cognitoUser.globalSignOut({
onFailure: e => console.log(e),
onSuccess: r =>
console.log('Logout success: ' + r)
})}
I am getting response as success but still I am able to access my API with the previous tokens.Please suggest me how to inactivate all the tokens issued to that cognito user.
The id token, which API Gateway uses to authenticate API calls, stays valid for a while.
I would test for the access token. It should expire right after you call global sign out.
The key word is should above. Please see this issue. It’s an on-going struggle to get AWS to implement an immediate revocation. Here’s a relevant quote:
I worked with AWS Cognito team to get this taken care and got released as a fix through CLI as following.
aws cognito-identity update-identity-pool --identity-pool-id --identity-pool-name --allow-unauthenticated-identities --cognito-identity-providers ProviderName=,ClientId=,ServerSideTokenCheck=<true|false>
By setting the ServerSideTokenCheck to true on a Cognito Identity
Pool, that Identity Pool will check with Cognito User Pools to make
sure that the user has not been globally signed out or deleted before
the Identity Pool provides an OIDC token or AWS credentials for the
user. Now we are running into another issue of this Token being cached
in API Gateway for 10mins which would let that OID token still be
active for 10mins even though the User has globally signed out.
Here's what I mean by test for the accessToken (I have had success with method #2):
1.) you could develop a custom authorizer for API Gateway;
2.) you could perform a check at the start of your lambda functions or on your servers, using:
const AWS = require('aws-sdk');
const awsConfig = require('./awsConfig');
const cognito = new AWS.CognitoIdentityServiceProvider(awsConfig);
// accessToken provided from API Gateway
new Promise((resolve, reject) => {
  cognito.getUser({ accessToken }, (errorCallback, response) => {
    if (errorCallback) {
      reject(errorCallback);
} else {
     resolve(response);
    }
});
});
The errorCallback and response do not matter. If you get an error, the token is invalid. If you don’t, it’s valid.

Categories

Resources