Google Sign-In idToken with createSessionCookie causing error - there is no user record corresponding to the provided identifier - node.js

Stack:
Google Sign-in (Vanilla JS - client side),
Firebase Functions (ExpressJS)
Client-Side:
My Firebase function express app uses vanilla javascript on the client side. To authenticate I am making use of Firebase's Google SignIn feature client-side javascript web apps, found here.
// Firebase setup
var firebaseConfig = {
apiKey: "AIza...",
authDomain: "....firebaseapp.com",
databaseURL: "https://...-default-rtdb.firebaseio.com",
...
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
function postIdTokenToSessionLogin(idToken, csrfToken) {
return axios({
url: "/user/sessionLogin", < ----- endpoint code portion found below
method: "POST",
data: {
idToken: idToken,
csrfToken: csrfToken,
},
});
}
// ...
// On sign-in click
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth()
.signInWithPopup(provider)
.then(async value => {
const idToken = value.credential.idToken;
const csrfToken = getCookie('_csrf');
return postIdTokenToSessionLogin(idToken, csrfToken);
}).then(value => {
window.location.assign("/user/dashboard")
}).catch((error) => {
alert(error.message);
});
Note I am using value.credential.idToken (most sources imply to use this, but haven't found an example saying use this specifically)
Directly after calling signInWithPopup, a new account is created in my Firebase Console Authentication matching the gmail account that was just signed in.
Server-side:
Once I authenticate, I create an axios request passing in the {user}.credential.idToken and following the server-side setup here (ignoring the CSRF - this just doesn't want to work).
In creating the session, I use the following code in my firebase functions express app, the endpoint which is router.post('/sessionLogin', (req, res) => (part of /user route prefix):
// Set session expiration to 5 days.
const expiresIn = 60 * 60 * 24 * 5 * 1000;
const idToken = req.body.idToken.toString(); // eyJhbGciOiJSUzI1NiIsImt...[936]
admin
.auth()
.createSessionCookie(idToken, {expiresIn}) < ----------- Problem line
.then((sessionCookie) => {
// Set cookie policy for session cookie.
const options = {maxAge: expiresIn, httpOnly: true, secure: true};
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({status: 'success'}));
}).catch((error) => {
console.error(error);
res.status(401).send('UNAUTHORIZED REQUEST!');
});
On the createSessionCookie call, I get the following error & stack trace:
Error: There is no user record corresponding to the provided identifier.
at FirebaseAuthError.FirebaseError [as constructor] (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:44:28)
at FirebaseAuthError.PrefixedFirebaseError [as constructor] (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:90:28)
at new FirebaseAuthError (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:149:16)
at Function.FirebaseAuthError.fromServerError (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:188:16)
at C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\auth\auth-api-request.js:1570:49
at processTicksAndRejections (internal/process/task_queues.js:93:5)
This is part of the sign-in flow with a existing Gmail account.
What is causing this?

After many hours of searching, Googling - I have seen the light.
For some additional context, this error featured heavily in my struggle "Firebase ID token has invalid signature." - I will get to that in a second.
Further, another issue I also faced was using a local auth emulator for web client-side (javascript), see this for setup.
TL;DR to solve the immediate problem
Client-side remained largely the same, however the documentation provided by Firebase was inaccurate/misleading - thanks to this post, I found the solution. Thus, it follows...
Which is the ID Token? (Client-side):
The examples from here (to allow signInWithPopup), the response (if successful) results in
...
.signInWithPopup(provider)
.then((result) => {
/** #type {firebase.auth.OAuthCredential} */
var credential = result.credential;
// This gives you a Google Access Token. You can use it to access the Google API.
var token = credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
})
Looking for an idToken, I found one using result.credential.idToken but no where on the internet on if this was infact the correct token to use.
I ran into this error using the provided idToken above:
Firebase ID token has incorrect "aud" (audience) claim. Expected
"[insert your **projectId**]" but got
"59895519979-2l78aklb7cdqlth0eob751mdm67kt301.apps.googleusercontent.com".
Make sure the ID token comes from the same Firebase project as the
service account used to authenticate this SDK.
Trying other tokens like result.credential.accessToken responded with various verification errors - what to do?
Mention earlier, this solution on Github suggested to use firebase.auth().currentUser.getIdToken() AFTER you have signed in. An example (building on my previous code) is to do the following:
...
.signInWithPopup(provider)
.then((result) => {
// current user is now valid and not null
firebase.auth().currentUser.getIdToken().then(idToken => {
// send this ID token to your server
const csrfToken = getCookie('_csrf');
return postIdTokenToSessionLogin(idToken, csrfToken);
})
})
At this point, you can verify your token and createSessionCookies to your heart's desire.
BUT, a secondary issue I unknowingly created for myself using the Authentication Emulator.
To setup for client-side use:
var auth = firebase.auth();
auth.useEmulator("http://localhost:9099");
To setup for hosting your firebase functions app (assuming you are using this with e.g. nodejs + express, see this for setup, ask in comments, can provide more details if needed)
Using Authentication Emulator caused the following errors AFTER using the above mentioned "fix". Thus, DO NOT RUN the local authentication emulator (with Google sign-in of a valid Google account) as you will consistently get.
Firebase ID token has invalid signature. See
https://firebase.google.com/docs/auth/admin/verify-id-tokens for
details on how to retrieve an ID token
You can use all your local emulators, but (so far in my experience) you will need to use an online authenticator.

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.

How do I call Google Analytics Admin API (for GA4) using an OAuth2 client in node.js?

I've noticed that all the node.js code samples for Google Analytics Admin and Google Analytics Data assume a service account and either a JSON file or a GOOGLE_APPLICATION_CREDENTIALS environment variable.
e.g.
const analyticsAdmin = require('#google-analytics/admin');
async function main() {
// Instantiates a client using default credentials.
// TODO(developer): uncomment and use the following line in order to
// manually set the path to the service account JSON file instead of
// using the value from the GOOGLE_APPLICATION_CREDENTIALS environment
// variable.
// const analyticsAdminClient = new analyticsAdmin.AnalyticsAdminServiceClient(
// {keyFilename: "your_key_json_file_path"});
const analyticsAdminClient = new analyticsAdmin.AnalyticsAdminServiceClient();
const [accounts] = await analyticsAdminClient.listAccounts();
console.log('Accounts:');
accounts.forEach(account => {
console.log(account);
});
}
I am building a service which allows users to use their own account to access their own data, so using a service account is not appropriate.
I initially thought I might be able to use the google-api-node-client -- Auth would be handled by building a URL to redirect and do the oauth dance...
Using google-api-nodejs-client:
const {google} = require('googleapis');
const oauth2Client = new google.auth.OAuth2(
YOUR_CLIENT_ID,
YOUR_CLIENT_SECRET,
YOUR_REDIRECT_URL
);
// generate a url that asks permissions for Google Analytics scopes
const scopes = [
"https://www.googleapis.com/auth/analytics", // View and manage your Google Analytics data
"https://www.googleapis.com/auth/analytics.readonly", // View your Google Analytics data
];
const url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes
});
// redirect to `url` in a popup for the oauth dance
After auth, Google redirects to GET /oauthcallback?code={authorizationCode}, so we collect the code and get the token to perform subsequent OAuth2 enabled calls:
// This will provide an object with the access_token and refresh_token.
// Save these somewhere safe so they can be used at a later time.
const {tokens} = await oauth2Client.getToken(code)
oauth2Client.setCredentials(tokens);
// of course we need to handle the refresh token too
This all works fine, but is it possible to plug the OAuth2 client from the google-api-node-client code into the google-analytics-admin code?
👉 It looks like I need to somehow call analyticsAdmin.AnalyticsAdminServiceClient() with the access token I've already retrieved - but how?
The simple answer here is don't bother with the Node.js libraries for Google Analytics Admin & Google Analytics Data.
Cut out the middleman and build a very simple wrapper yourself which queries the REST APIs directly. Then you will have visibility on the whole of the process, and any errors made will be your own.
Provided you handle the refresh token correctly, this is likely all you need:
const getResponse = async (url, accessToken, options = {}) => {
const response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response;
};
I use Python but the method could be similar. You should create a Credentials object based on the obtained token:
credentials = google.auth.credentials.Credentials(token=YOUR_TOKEN)
Then use it to create the client:
from google.analytics.admin import AnalyticsAdminServiceClient
client = AnalyticsAdminServiceClient(credentials=credentials)
client.list_account_summaries()

#azure/msal-node: Is there a way to log out / invalidate tokens?

I'm using the #azure/msal-node package in a node application to enable my users to log in using their AzureAD credentials. Logging in and acquiring session tokens works fine, but I cannot find a way to invalidate a session / log out a user - am I overlooking something obvious here?
Just for context, here's how I'm getting my tokens:
// msalConfig is my valid config object
const msalApp = new msal.ConfidentialClientApplication(msalConfig);
const authCodeUrlParameters = {
scopes: ['user.read'],
redirectUri: BASE_URL + '/msal-redirect'
};
try {
const authCodeResponse = await msalApp.getAuthCodeUrl(authCodeUrlParameters);
reply.redirect(authCodeResponse);
} catch (e) {
logError('auth code redirect error', e);
}
In the redirect handler, I'm doing this:
const tokenResponse = await msalApp.acquireTokenByCode({
code: request.query.code,
scopes: ['user.read'],
redirectUri: BASE_URL + '/msal-redirect'
});
and then I'm using that token to display the logged in user etc.
What I'm missing is something like msalApp.logout() - what am I not seeing here?
Unfortunately MSAL does not currently contain an msalApp.logout() API. Instead, you will have to manually implement the steps.
A logout operation will contain multiple steps:
Removing the account and the tokens from the msal application cache.
Redirecting to the AAD logout endpoint so the user logs out and AAD cookies are deleted.
If your webapp has a session, invalidating it.
For removing the account and tokens from the msal application cache, you can do something along the lines of:
const accounts = msalApp.getTokenCache().getAllAccounts();
// filter on the account that you want to delete from the cache.
// I take the first one here to keep the code sample short
const account = accounts[0];
msalApp.getTokenCache().removeAccount(account);
For logging out from AAD, you'll have to redirect the user to the Azure AD logout endpoint. The documentation here should explain how to craft this request.
In answer of sgonzalez, I can see the error in "accounts". This const is a Promisse:
const accounts = msalApp.getTokenCache().getAllAccounts(); // <- is a Promisse
// filter on the account that you want to delete from the cache.
// I take the first one here to keep the code sample short
const account = accounts[0];
msalApp.getTokenCache().removeAccount(account);
Correcting:
pca.getTokenCache().getAllAccounts().then((response) => {
const account = response[0];
pca.getTokenCache().removeAccount(account).then(() => {
res.sendStatus(200);
}).catch((error) => {
res.status(500).send({error});
});
}).catch((error) => {
res.status(500).send(error);
});
I don't know if this is the best way to implement it, but it worked for me.

Verify JWT from Google Chat POST request

I have a bot in NodeJS connected to Google Chat using HTTPs endpoints. I am using express to receive requests. I need to verify that all requests come from Google, and want to do this using the Bearer Token that Google Sends with requests.
My problem is that I am struggling to find a way to verify the tokens.
I have captured the token and tried a GET reuqes to https://oauth2.googleapis.com/tokeninfo?id_token=ey... (where ey... is the token start).
Which returns:
"error": "invalid_token",
"error_description": "Invalid Value"
}
I have tried what Google recommends:
var token = req.headers.authorization.split(/[ ]+/);
client.verifyIdToken({
idToken: token[1],
audience: JSON.parse(process.env.valid_client_ids)
}).then((ticket) => {
gchatHandler.handleGChat(req.body, res);
}).catch(console.error);
And get the following error:
Error: No pem found for envelope: {"alg":"RS256","kid":"d...1","typ":"JWT"}
Any idea where I should head from here?
Edit: https://www.googleapis.com/service_accounts/v1/metadata/x509/chat#system.gserviceaccount.com found this, investigating how to use it. The kid matches the one I get.
Worked it out, eventually.
You need to hit: https://www.googleapis.com/service_accounts/v1/metadata/x509/chat#system.gserviceaccount.com to get a JSON file containing the keys linked to their KIDs.
Then when a request arrives, use jsonwebtoken (NPM) to decode the token and extract the KID from the header.
Use the KID to find the matching public key in the response from the website above, then use the verify function to make sure the token matches the public key.
You also need to pass the audience and issuer options to verify, to validate that it is your particular service account hitting the bot.
The solution above maybe the correct for Google Chat, but in my experience Google services (e.g. Google Tasks) use OIDC tokens, which can be validated with verifyIdToken function.
Adding my solution here, since your question/answer was the closest thing I was able to find to my problem
So, In case if you need to sign a request from your own code
on client, send requests with OIDC token
import {URL} from 'url';
import {GoogleAuth} from 'google-auth-library';
// will use default auth or GOOGLE_APPLICATION_CREDENTIALS path to SA file
// you must validate email of this identity on the server!
const auth = new GoogleAuth({});
export const request = async ({url, ...options}) => {
const targetAudience = new URL(url as string).origin;
const client = await auth.getIdTokenClient(targetAudience);
return await client.request({...options, url});
};
await request({ url: 'https://my-domain.com/endpoint1', method: 'POST', data: {} })
on the server, validate OIDC (Id token)
const auth = new OAuth2Client();
const audience = 'https://my-domain.com';
// to validate
const token = req.headers.authorization.split(/[ ]+/)[1];
const ticket = await auth.verifyIdToken({idToken: token, audience });
if (ticket.getPayload().email !== SA_EMAIL) {
throw new Error('request was signed with different SA');
}
// all good
Read more about Google OpenID Connect Tokens

DocuSign Node SDK not returning loginInfo in Production

I've built out an integration using DocuSign's Node SDK. While testing using a DocuSign sandbox account, the authentication flow works just fine using the example in the docs.
I'm now trying to do the same within a live DocuSign production account using the Integrator Key that was promoted from the sandbox account. authApi.login() seems to work just fine, I get no error and the status code of the response is 200. However, the value of loginInfo comes back as exports {} with no account info included.
I've made sure to change the base path from https://demo.docusign.net/restapi to www.docusign.net/restapi and as far as I can tell from the docs, there doesn't seem to be anything else I need to make the switch to production. Here is the code I am using:
apiClient.setBasePath('www.docusign.net/restapi');
apiClient.addDefaultHeader('Authorization', 'Bearer ' + token);
docusign.Configuration.default.setDefaultApiClient(apiClient);
const authApi = new docusign.AuthenticationApi();
const loginOps = {
apiPassword: true,
includeAccountIdGuid: true
};
authApi.login(loginOps, function (err, loginInfo, response) {
if (err) {
console.log(err);
}
if (loginInfo) {
// loginInfo returns 'exports {}' so the variables below cannot be set.
const loginAccounts = loginInfo.loginAccounts;
const loginAccount = loginAccounts[0];
const baseUrl = loginAccount.baseUrl;
const accountDomain = baseUrl.split('/v2');
const accountId = loginAccount.accountId;
apiClient.setBasePath(accountDomain[0]);
docusign.Configuration.default.setDefaultApiClient(apiClient);
www.docusign.net endpoint will only work if your PROD account is in NA1, if your PROD Account is in NA2, then you need to use na2.docusign.net and if it is in NA3 then na3.docusign.net. This is the main reason you should use /oauth/userinfo call with OAUTH2 Access Token to know your base URL, and then call all APIs with this baseURL. You can find more details at https://docs.docusign.com/esign/guide/authentication/userinfo.html

Resources