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

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.

Related

Azure AD When to use API Permissions vs Expose an API

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)

Firebase HTTPS callable function context.auth is always null when used with custom token auth

HTTPS callable function is called directly from our app after signing in using custom token (custom auth), but context.auth is null in function eventually.
I am wondering if this is something expected? I am not providing any specific example (our client is using Firebase SDK with Kotlin, everything is implemented accordingly to the example in docs), just want to know if maybe someone had similar issue or maybe we need to double check our client's code (custom token authentication is actually working there, since we use firestore with security rules that require it).
I was trying to find some information about certain restrictions, but there's none: Firebase FAQ https://firebase.google.com/support/troubleshooter/functions/auth/callable (nothing about custom token), this answer here Do I need to use verifyIdToken on the context.auth object in firebase cloud functions?
Been asked to add an example of the cloud function, nothing specific, is reproducible with simple one like the following (auth will be always null in log record):
exports.getData = functions.https.onCall((data, context) => {
functions.logger.info('Auth info', { auth: context.auth });
return {
success: true,
data: null,
};
});
Seems like a potential race condition, Ensure that Auth has created the user object before requesting the callable function if you are calling it directly after a sign-in method.
This can be done using a callback from an onAuthStateChanged.
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
Source: https://firebase.google.com/docs/auth/web/manage-users#get_the_currently_signed-in_user

How to validate JWT token from Google pub/sub push (No pem found for envelope)

Context
I'm following Google's RTDNs guide on enabling Real-Time Developer Notifications. I've successfully created the topic and subscription and have received the push notifications sent to the API that I have created. I would now like to authenticate and validate these messages. For that, I'm following this guide on Authentication and Authorization. Their developer documentation here and here has a seemingly useful example.
The Issue
After following the resources outlined above, I get the following error:
Error: No pem found for envelope: {"typ":"JWT","alg":"HS256"}
Relevant Code
const authClient = new OAuth2Client();
// ...
app.post('/pubsub/authenticated-push', jsonBodyParser, async (req, res) => {
// Verify that the push request originates from Cloud Pub/Sub.
try {
// Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.
const bearer = req.header('Authorization');
const [, token] = bearer.match(/Bearer (.*)/);
// Verify and decode the JWT.
// Note: For high volume push requests, it would save some network
// overhead if you verify the tokens offline by decoding them using
// Google's Public Cert; caching already seen tokens works best when
// a large volume of messages have prompted a single push server to
// handle them, in which case they would all share the same token for
// a limited time window.
// verifyIdToken is failing here with the `No pem found for envelope` error
const ticket = await authClient.verifyIdToken({
idToken: token,
audience: 'example.com',
});
// ...
} catch (e) {
res.status(400).send('Invalid token');
return;
}
res.status(200).send();
});
The Questions
From this, I'm assuming I need to have some public key.
Where do I get said public key?
Where do I put said public key so that the google client is initialized with it?
How can I generate an example JWT to test my endpoint?
Edits
I was able to find the source of this error in their code here:
if (!Object.prototype.hasOwnProperty.call(certs, envelope.kid)) {
// If this is not present, then there's no reason to attempt verification
throw new Error('No pem found for envelope: ' + JSON.stringify(envelope));
}
However, I've verified that the kid attribute does indeed exist in the decoded object:
{"alg":"RS256","kid":"7d680d8c70d44e947133cbd499ebc1a61c3d5abc","typ":"JWT"}
Turns out the kid was invalid and therefore threw the No pem found for envelope error. Once a valid kid was supplied, the error no longer persisted.

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.

AWS Cognito - Developer Authenticated Identities in JavaScript(Browser)

I have trouble getting credentials in a browser script.
The authentication server returns cognito_identityId and cognito_token.
Then I set a Cookie:
$.cookie('cognito_identityId')
$.cookie('cognito_token')
I tried to get credentials in 4 ways on the browser, and all Failed:
CognitoIdentityCredentials
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'us-east-1:xxxxxxxxxxxx'
IdentityId: $.cookie('cognito_identityId'),
Logins: {
'myauth': $.cookie('cognito_token')
}
});
// => Error: Missing required key 'IdentityId' in params
assumeRoleWithWebIdentity
var params = {
RoleArn: 'arn:aws:iam::xxxxxxxxxxxx:role/Cognito_xxxxxxxAuth_Role',
RoleSessionName: 'xxxxxxxxxxx',
WebIdentityToken: $.cookie('cognito_token'),
DurationSeconds: 900,
ProviderId: 'myauth'
};
var sts = new AWS.STS({apiVersion: '2011-06-15'});
sts.assumeRoleWithWebIdentity(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
// => AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity
PolicyDocument
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"cognito-identity.amazonaws.com:aud": "us-east-1:xxxxxxxxxxxxx"
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authenticated"
}
}
}
]
}
GetCredentialsForIdentity
var params = {
IdentityId: $.cookie('cognito_identityId'),
Logins: {
"myauth": $.cookie('oauth.io_token')
}
};
var cognitoidentity = new AWS.CognitoIdentity({apiVersion: '2014-06-30'});
cognitoidentity.getCredentialsForIdentity(params, function(err, data) {
if (err) {
console.log(err, err.stack); // an error occurred
}
else {
console.log(data); // successful response
}
});
// => InvalidParameterException: Please provide a valid public provider
WebIdentityCredentials
AWS.config.credentials = new AWS.WebIdentityCredentials({
RoleArn: 'arn:aws:iam::xxxxxxxx:role/Cognito_xxxxxxxxxxAuth_Role',
WebIdentityToken: $.cookie('cognito_token')
});
// => Error: There were 2 validation errors:
// * MissingRequiredParameter: Missing required key 'IdentityPoolId' in params
// * MissingRequiredParameter: Missing required key 'IdentityId' in params
Questions:
What am I doing wrong?
What is the correct way to use this?
Thank you.
Thank you for your kindness.
I tyied your advice, but did not change.
Error messages.
POST https://cognito-identity.us-east-1.amazonaws.com/ 400 (Bad Request)
POST https://cognito-identity.us-east-1.amazonaws.com/ 400 (Bad Request)
Error: Missing required key 'IdentityId' in params
at fail (chrome-extension://hmjdjbikinkmjbilihjibcihbkbjdgjf/bower_components/aws-sdk-js/dist/aws-sdk.js:2163:37)
at validateStructure (chrome-extension://hmjdjbikinkmjbilihjibcihbkbjdgjf/bower_components/aws-sdk-js/dist/aws-sdk.js:2084:14)
at validateMember (chrome-extension://hmjdjbikinkmjbilihjibcihbkbjdgjf/bower_components/aws-sdk-js/dist/aws-sdk.js:2110:21)
at validate (chrome-extension://hmjdjbikinkmjbilihjibcihbkbjdgjf/bower_components/aws-sdk-js/dist/aws-sdk.js:2059:10)
at Request.VALIDATE_PARAMETERS (chrome-extension://hmjdjbikinkmjbilihjibcihbkbjdgjf/bower_components/aws-sdk-js/dist/aws-sdk.js:800:32)
at Request.callListeners (chrome-extension://hmjdjbikinkmjbilihjibcihbkbjdgjf/bower_components/aws-sdk-js/dist/aws-sdk.js:3913:20)
at callNextListener (chrome-extension://hmjdjbikinkmjbilihjibcihbkbjdgjf/bower_components/aws-sdk-js/dist/aws-sdk.js:3903:12)
at chrome-extension://hmjdjbikinkmjbilihjibcihbkbjdgjf/bower_components/aws-sdk-js/dist/aws-sdk.js:787:9
at finish (chrome-extension://hmjdjbikinkmjbilihjibcihbkbjdgjf/bower_components/aws-sdk-js/dist/aws-sdk.js:126:7)
at chrome-extension://hmjdjbikinkmjbilihjibcihbkbjdgjf/bower_components/aws-sdk-js/dist/aws-sdk.js:142:9
There are source code below link.
https://github.com/bisque33/my-custom-dictionary
and server side is a AWS Lambda Function.
var aws = require('aws-sdk');
aws.config.region = 'us-east-1';
var cognitoidentity = new aws.CognitoIdentity();
var identityPoolId = 'us-east-1:0dccff0d-5fd7-4d14-b38f-d27204feaecc';
console.log('Loading function');
exports.handler = function(event, context) {
console.log('token: %s', event.token);
var params = {
IdentityPoolId: identityPoolId,
Logins: {
'oauth.io': event.token
}
};
cognitoidentity.getOpenIdTokenForDeveloperIdentity(params,function(err,data){
if(err){
console.log(err);
context.fail('Something went wrong');
}else{
context.succeed(data);
}
});
};
This program is Google-Chrome-Extension.
AWS Lambda Function returns token by getOpenIdTokenForDeveloperIdentity.
app/scripts/popup.js calls Lambda Function and set cookies.
app/scripts/background.js calls AWS.config.credentials.get, and returns error.
Am I using it wrong?
Update for Additional Information
Thank you for the additional information.
Error appears on 104 line on background.js
AWS.config.credentials.get(function(){
and 115 line on background.js
dataset.synchronize(
And, My explaination was not enough. Facebook authentication needs the domain(ex. http :// example.com). However, Google-Chrome-Ext does not have domain. It has a domain 'chrome-extension://xxxxxxxxxxxxxxxxxxxx'. Then, I use https://oauth.io. It proxies any authentication and accepts chrome-extension domain.
Popup.js does Facebook authentication through oauth.io sdk. It gets a facebook token, and gives to getOpenIdTokenForDeveloperIdentity. I think facebook token.substr(0,14) is unique. But, If it is wrong, I use another unique identifier(ex. email-address.)
Sorry, I was wrong. AWS.config.credentials.get gives an Error:
Error: Invalid login token.
And, dataset.synchronize shows this Error:
Error: Missing required key 'IdentityId' in params
The first approach you have, using CognitoIdentityCredentials, is most likely the best approach for you to take. I can't spot exactly what's causing the error for you but lets try a couple things:
When using Developer Authenticated Identities, you do need to specify the IdentityId when initializing CognitoIdentityCredentials. You need to get the IdentityId value from the call to GetOpenIdTokenForDeveloperIdentity. However, you shouldn't need to preserve the IdentityId value in a cookie as CognitoIdentityCredentials will cache the id by default in the browser's local storage.
As for your Logins map: It looks like you're trying to use Developer Authenticated Identities. With the JavaScript SDK, use the key 'cognito-identity.amazonaws.com' and make sure the value is the token returned from your backend's call to getOpenIdTokenForDeveloperIdentity.
If you continue to have problem using the CognitoIdentityCredentials approach, please reply here with some more info such as the exact method/code you're calling when you receive the error message, and the traced output (i.e. with console.log('%o',..)) of the params input just before your call to the CognitoIdentityCredentials constructor.
Update Based on Additional Information Provided
I still need to know exactly which line of code you receive the error on, but based on the information provided I think I can still help...
Based on what I see in background.js, it looks like you're trying to initialize CognitoIdentityCredentials using a Developer Authenticated Identities provider. This is where I'm guessing that you're receiving the error.
However, in Popup.js, it looks like you're trying to authenticate the user with Facebook. If you're authenticating your users with Facebook, you should just pass the facebook access token into your Logins map when using Cognito. Just use graph.facebook.com as the key in the Logins map and the access token from Facebook. More detail on how to do this is in the Facebook Integration topic of the Amazon Cognito developer guide.
Facebook vs Developer Authenticated Identities
We can get Developer Authenticated Identities to work for you, but in this case, it doesn't look like the right solution for you since you're not actually doing any additional authentication on the identity in your Lambda function and the unique user identifier that you're passing into the getOpenIdTokenForDeveloperIdentity operation appears to be the facebook token, which is not good by the way since the token itself will change between user sessions even for the same user. Usually a good unique identifier is an email address or a user id used by an internal system.
Facebook Login & Redirects
Since you're ultimately trying to use Facebook for login and Amazon Cognito has built-in integration for Facebook, the best thing for you to do is get an access token from Facebook and pass in the Facebook token to Cognito's login map directly. I'm not sure if this will work with Auth.io or not (I'm just not familiar with it), but as long as Auth.io gives your JavaScript code a bonefide facebook token and you add the same Facebook App ID to both Auth.io and Amazon Cognito's Console, it should work. However, you mentioned you want to use Auth.io to avoid Facebook doing a redirect to a landing page. I could be mistaken, but I'm pretty sure if you're using Facebook's JavaScript SDK you won't need a redirect page. You should only need the redirect page if you're doing Facebook's Manually Build a Login Flow.

Categories

Resources