How to persist Cognito identity across pages in browser - javascript

I am authenticating through Cognito on client side browser using a developer authenticated identity. When my page loads (or is refreshed) I would like my application to remember the Identity for as long as the object is not expired (I think it lasts about an hour). However, I don't know how to retrieve the identity from Cognito without having to go through the developer authentication again.
Here is what the code does on page load:
var cognitoCredentials
$(document).ready(function() {
"use strict";
cognitoParams = {
IdentityPoolId: 'us-east-1:xxxxxxx'
};
cognitoCredentials = new AWS.CognitoIdentityCredentials(cognitoParams);
AWS.config.credentials = cognitoCredentials;
});
And after logging in through the developer authentication:
cognitoCredentials.params.IdentityId = output.identityId;
cognitoCredentials.params.Logins = {
'cognito-identity.amazonaws.com': output.token
};
cognitoCredentials.expired = true;
If I have already logged in, and then refresh the page, and try to log in again I get an error that I am trying to get an identity when I already have one
Error: Missing credentials in config(…) NotAuthorizedException: Missing credentials in config
"Access to Identity 'us-east-1:xxxxxxx' is forbidden."
However, I don't know how to access it. How do I retrieve the credentials so that when the page is refreshed, I can detect the previous identity given by Cognito?

Save at least accessKeyId, secretAccessKey, sessionToken in sessionStorage between pages. You can load these into AWS.config.credentials (after the AWS SDK has been loaded of course). It is much faster than waiting for Cognito to respond. Keep in mind, you'll have to manually refresh them with a token from one of the providers and this is only good until the temporary token expires (~1 hour).
var credKeys = [
'accessKeyId',
'secretAccessKey',
'sessionToken'
];
// After Cognito login
credKeys.forEach(function(key) {
sessionStorage.setItem(key, AWS.config.credentials[key]);
});
// After AWS SDK load
AWS.config.region = 'us-east-1'; // pick your region
credKeys.forEach(function(key) {
AWS.config.credentials[key] = sessionStorage.getItem(key);
});
// Now make your AWS calls to S3, DynamoDB, etc

The only way to get back to the same identity on page refresh would be to use the same token used to initialize that identity. You may want to refer to this question as the problems are similar (replacing the Facebook token with the OpenId Connect token from the developer authenticated identities flow).
To reiterate what that question says: the credentials in the SDK will not be persisted across pages, so you should cache the token to be reused.

I take a slightly different approach, that allows the SDK to refresh the credentials.
In short, I serialize the AssumeRoleWithWebIdentityRequest JSON object to session storage.
Here is an example using Angular, but concept applies in any JS app:
const AWS = require('aws-sdk/global');
import { STS } from 'aws-sdk';
import { environment } from '../../environments/environment';
const WEB_IDENT_CREDS_SS_KEY = 'ic.tmpAwsCreds';
// Handle tmp aws creds across page refreshes
const tmpCreds = sessionStorage.getItem(WEB_IDENT_CREDS_SS_KEY);
if (!!tmpCreds) {
AWS.config.credentials = new AWS.WebIdentityCredentials(JSON.parse(tmpCreds));
}
#Injectable({
providedIn: 'root'
})
export class AuthService {
...
async assumeAwsRoleFromWebIdent(fbUser: firebase.User) {
const token = await fbUser.getIdToken(false);
let p: STS.Types.AssumeRoleWithWebIdentityRequest = {
...environment.stsAssumeWebIdentConfig,
//environment.stsAssumeWebIdentConfig contains:
//DurationSeconds: 3600,
//RoleArn: 'arn:aws:iam::xxx:role/investmentclub-fbase-trust',
RoleSessionName: fbUser.uid + '#' + (+new Date()),
WebIdentityToken: token
};
// Store creds across page refresh, duno WTF `new AWS.WebIdentityCredentials(p)` don't have an option for this
AWS.config.credentials = new AWS.WebIdentityCredentials(p);
sessionStorage.setItem(WEB_IDENT_CREDS_SS_KEY, JSON.stringify(p));
}
removeAwsTempCreds() {
AWS.config.credentials = {};
sessionStorage.removeItem(WEB_IDENT_CREDS_SS_KEY);
}
...
Few things to note:
Upon login, I store the WebIdentityCredentials parameters as a JSON string in session cache.
You'll notice I check the browser session cache in global scope, to
handle page refreshes (sets creds before they can be used).
A tutorial with complete example can be found on my blog

Related

Firebase multiple real time databases, custom auth issues

I'm attempting to use multiple real time databases for my single project in Firebase. For my setup, I'm generating tokens and sending to users from a server that is not part of Google's servers.
Everything works so far for the default database, but issues arise when attempting access the second real time database. I have read that it might be because you have to generate a token for each app instance, but to avoid becoming a difficult process, is it possible to generate a single token that can access all databases?
On my backend server we do this to generate a token:
initializeApp({
credential: admin.credential.cert(FirebaseCredentials),
});
usually the next line is a databaseURL. What is passed the service account JSON file.
Is there an easy way to allow a user to use one token to access all databases if we had 10 of them or is the solution to initalizeApp and specify a different database each time with 10 different tokens if the data is spread across 10 different real time databases.
Client side I used the generated token to sign up. The generated token does not specify a databaseURL and appears to use the default database. When I attempt to use the second database, I tried to follow the multi database instructions.
const app2 = initializeApp({ databaseURL: '...'}, 'app2');
const db = getDatabase(app2);
set(ref(db, 'users'), {
user: 'me',
});
When I use that second one, I get hit with permissions denied. The rules are identical between rtdbs. I'm guessing its because my original app config token is for the default database and not for the new one? Is it possible to somehow use a single generated token for all databases?
Firebase Rules for both databases.
{
"rules": {
".read": "auth.token.chatEnabled === true",
".write": "auth.token.chatEnabled === true"
}
}
Here is how I use the token on client side. I simply pass the token back from the server. Client uses the Javascript SDK to initialize app with the firebase config generated from initial startup.
Then I do:
import { initializeApp } from "firebase/app";
import { getDatabase, ref, set } from 'firebase/database';
const firebaseConfig = {
... the config
};
const app = initializeApp(firebaseConfig);
const auth = getAuth();
await signInWithCustomToken(auth, token);
const app2 = initializeApp({
databaseURL: 'secondatabaseurl'
}, 'app2');
const db = getDatabase(app2);
set(ref(db, 'users'), {
user: 'me',
});
I believe my issue arises because when I go to call the second app, the token generated appears to be only usable for the first app or default config.
If all of my databases are on a single project, do I have to create a separate app for each new database? If so, would that mean I need a separate token for each app instance or to sign into each app instance with the same token?
Thank you for your time.
Not the answer, but I can initialize 2 different apps.
This works ->
const secondaryApp = initializeApp({ appconfig2 }, 'secondaryApp');
const auth = getAuth(secondaryApp);
const db = getDatabase(secondaryApp);
await signInWithCustomToken(auth, token);
set(ref(db, 'users'), {
user: 'me',
});
This does not work ->
const secondaryApp = initializeApp({ appconfig2 }, 'secondaryApp');
const auth = getAuth(secondaryApp);
await signInWithCustomToken(auth, token);
const db = getDatabase(secondaryApp);
set(ref(db, 'users'), {
user: 'me',
});
// now try a second database
const db2 = getDatabase();
set(ref(db2, 'users'), {
user: 'me',
});
From my understanding, getDatabase() pulls from the default initalizedApp. It appears that once a user has authenticated using one of the initializedApps (in this case the user initialized with secondaryApp), those authenticated details are not shared with other apps at least I'm not exactly sure how to share them with all apps declared. Even if the app configs have identical api keys, auth domains, etc but differ on the DatabaseURL, they do not somehow just connect.
Ideally once a user has authenticated, it should be that they have access to all databases in the single project/app, but this does not seem to be the case here.
The last response here might be the answer ->
Got permission denied when writing to the second instance of Firebase realtime database
So in order to access another database, a user must authenticate individually?
Okay so that link at the end was the answer, I just didn't expect to have to do this.
If you specify auth for each database, since the databases are declared in different apps, you have to authenticate the user with each one before being able to read/write.
In other words, to make it work I had to signInWithCustomToken per each database instance.
const app1 = initializeApp(config1);
const app2 = initializeApp(config2);
const auth1 = getAuth(app1);
const auth2 = getAuth(app2);
await signInWithCustomToken(auth1, token);
await signInWithCustomToken(auth2, token);
then you can use each app instance to getDatabase(app1/app2).
Maybe the reason for this is that people can use completely different apps. Maybe there is some way in future to make it so that one app with multiple sharded real time databases do not require authentication into each app database. Maybe it would be instead of declaring a new app for each sharded db, it should be some other helper function.

How to impersonate a Google Workspace account through ADC on Google Cloud Functions?

What I'm trying to do is to:
Create a service account
Give that service account Domain Wide Delegations
Use the Application Default Credentials to impersonate a Google Workspace user and access it's email with gmail API
My code works fine in local development, by using the credential key json file generated for the Service Account mentioned in step 1, through Application Credentials Default mechanism (I have set up the environment variable GOOGLE_APPLICATION_CREDENTIALS to the path of the credentials.json)
import {google} from 'googleapis'
const scopes = [
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.modify',
];
const auth = new google.auth.GoogleAuth({
clientOptions: {
subject: 'user#email.com',
},
scopes,
});
const gmail = google.gmail({
version: 'v1',
auth,
});
const list = await gmail.users.messages.list({
userId: 'me',
maxResults: 10,
});
The problem is AFAIK, in local environment (aka not GCE) GoogleAuth uses JWT and in GCE it uses Compute Auth method, where a subject can't be configured or if configured is ignored.
So that's why when deploying the Cloud Function it throws an error about Precondition Check Failed and nothing else more specific.
In my limited knowledge and research I think the solution would be to somehow convert the Compute Auth -> JWT with a subject defined.
The current solution I have implemented and works, consists in saving the credentials.json into the Google Secret Manager:
// Acquire credentials from secret manager:
const secret = await getApiKeyFromSecretManager('SECRET_NAME');
const jsonCreds = JSON.parse(Buffer.from(secret).toString());
// Create the JWT
const auth = google.auth.fromJSON(jsonCreds);
auth.subject = 'user#email.com';
auth.scopes = scopes;
But I'm not really comfortable having to access or save the credentials in the Secret Manager, as I think the solution is not as elegant as it could be.

Silent authentication for own website inside tab of custom Teams app

After two months of experimenting with Teams Authentication via adal.js and msal.js and failure, I’m close to giving up. So I really need your help.
Basically I need to “silently” authenticate the logged in Teams User for my own website (tab) inside my app that I created with App Studio. The reason for that is, so that I can use the data of the authentication token for the login of my own website.
So far I was only able to get this working with msal.js and a popup, which according to Teams developer I’ve asked is not the way to go. Understandable, since I cannot use the popup method on the Teams Client because it gets blocked.
I’ve tried this silent login method (https://github.com/OfficeDev/microsoft-teams-sample-complete-node/blob/master/src/views/tab-auth/silent.hbs) that was recommend to me.
Sadly it didn’t work. All I get is a “Renewal failed: Token renewal operation failed due to timeout” error.
Since the msal.js popup variant (Node.js Azure Quick Start Example) I used before worked in a web browser, I don’t think that the configuration of Azure App is wrong.
This is my code so far:
// onLoad="prepareForm()"
<!--- Import package for authentication information in Teams/Azure--->
<script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.15/js/adal.min.js" integrity="sha384-lIk8T3uMxKqXQVVfFbiw0K/Nq+kt1P3NtGt/pNexiDby2rKU6xnDY8p16gIwKqgI" crossorigin="anonymous"></script>
<script src="https://statics.teams.microsoft.com/sdk/v1.4.2/js/MicrosoftTeams.min.js" crossorigin="anonymous"></script>
<script language="JavaScript">
let config = {
clientId: "1402f497-d6e8-6740-9412-e12def41c451", // I've changed it for this stackoverflow post
redirectUri: "https://myredirect.com", // I've changed it for this stackoverflow post
cacheLocation: "localStorage",
navigateToLoginRequestUrl: false,
};
microsoftTeams.initialize()
/// START Functions for Teams
function getTeamsContext() {
microsoftTeams.getContext(function(context) {
startAuthentication(context);
});
}
function startAuthentication(teamsContext) {
if (teamsContext.loginHint) {
config.extraQueryParameters = "scope=openid+profile&login_hint=" + encodeURIComponent(teamsContext.loginHint);
} else {
config.extraQueryParameters = "scope=openid+profile";
}
let authContext = new AuthenticationContext(config);
user = authContext.getCachedUser();
if (user) {
if (user.profile.oid !== teamsContext.userObjectId) {
authContext.clearCache();
}
}
let token = authContext.getCachedToken(config.clientId);
if (token) {
console.log(token)
// Get content of token
} else {
// No token, or token is expired
authContext._renewIdToken(function (err, idToken) {
if (err) {
console.log("Renewal failed: " + err);
// Some way of logging in via Popup or similiar
} else {
console.log(idToken)
// Get content of token
}
});
}
}
/// END Functions for Teams
// initialized on page load!
function prepareForm() {
getTeamsContext();
document.InputForm.password.focus()
}
<script/>
Those are my questions:
What causes this error?
How do I authenticate the token on manipulation and is it Teams or Azure? (Does adal.js any functions for this?)
How do I login if the silent authentication fails and popups are blocked? Is there a website for authentication provided by Teams that returns a token?
Are there any working examples of the silent authentication that are not from the official Microsoft website? (I don't understand them.)

How can I check or verify that a user is signed in with AWS Cognito Javascript?

I am creating a React web app where the user sign in/up and other authentication related processes are being handled by AWS Cognito and the accompanying Javascript SDK.
My app has some 'public' routes/pages that everybody, signed in or not, can view, such as /documentation/ and /sign-in/. There also exist various private routes which you can only see when you are logged in, such as /my-documents/.
At the moment, I have a working sign in page, where a user is signed in with code very similar to use case #4 (Cognito Docs).
My question now is: as soon as a user goes to /my-documents/, how do I check whether the user is signed in and actually has the rights to see this page?
I am not using AWS Amplify for the authentication in my app. I only use the NPM package 'amazon-cognito-identity-js'.
This is the code I currently use to check if the session is valid, in other words if the user is successfully signed in. This however, seems like a cumbersome way to check such a simple status.
const isAuthenticated = () => {
const cognitoUser = userPool.getCurrentUser();
let isSessionValid = false;
if (cognitoUser) {
cognitoUser.getSession((err: Error, result: CognitoUserSession) => {
if (!err) {
isSessionValid = result.isValid();
}
});
}
return isSessionValid;
};
isSessionValid is returned before the callback in getSession is executed.

Add Google authentication to Firebase Real Time Database

I'm using the Firebase Real Time Database and I need to add user authentication to it. Users can only login with Google as a provider.
Current database mode:
{
"rules": {
".read": true,
".write": true
}
}
New mode should be like this:
// These rules grant access to a node matching the authenticated
// user's ID from the Firebase auth token
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
What should I use to authenticate in my case? userID, Google providerID or a token like described here?
This is the function without authentication to store data:
createMeetup ({commit, getters}, payload) {
console.log('index.js -> createMeetup')
const meetup = {
title: payload.title,
}
let imageUrl
let key
firebase.database().ref('meetups').push(meetup)
.then((data) => {
key = data.key
return key
})
.then(() => {
commit('createMeetup', {
...meetup,
imageUrl: imageUrl,
id: key
})
})
.catch((error) => {
console.log(error)
})
},
For your use case it seems like you need to sort out a few steps. I'm guessing your application can already connect/use Firebase, but these are essentially it:
Step 1 - Connecting
Connect to Firebase using your API key/config as per usual, should look something like the following.
firebase.initializeApp(config)
See also: https://firebase.google.com/docs/web/setup
You probably already have this somewhere. This does not change, but if you would apply the rules as described your users would not be able to use Firebase after just connecting.
Step 2 - Authenticating
This is basically telling Firebase who is connected. This must be done with a token/method Firebase can verify. Using a Google ID is the most common method.
With an existing Google ID / user login
// Initialize a generate OAuth provider with a `google.com` providerId.
var provider = new firebase.auth.OAuthProvider('google.com');
var credential = provider.credential(googleUser.getAuthResponse().id_token);
firebase.auth().signInWithCredential(credential)
See also: https://firebase.google.com/docs/reference/js/firebase.auth.OAuthProvider#credential
Or make Firebase SDK do the login flow
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider).then(function(result) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
})
See also: https://firebase.google.com/docs/auth/web/google-signin
This last option is preferred / suggested by the documentation you referenced.
If, as you described, users can already login with Google to your app for other functionalities then you should already have a login flow somewhere. Depending on your situation it might be advisable to let the Firebase SDK / library take over this process for simplicity in your application.
Step 3 - Using the database
Lastly, after authenticating users and applying the rules you suggested you will need to also make sure the paths you write to are within those accessible by the current user. You can put this in a simple function to make it easier.
const getUserRef = (ref) => {
const user = firebase.auth().currentUser;
return firebase.database().ref(`/users/${user.uid}/${ref}/`);
}
You should of course not be retrieving the current user every time you want to get a database reference, but I think this clearly illustrates the steps that need to be taken.
You can allow users to login/auth using multiple methods. Then you can merge them together to a single account as described here:
https://firebase.google.com/docs/auth/web/account-linking
So really it boils down to two options:
Allow users to login with multiple methods such as Facebook, Google, Github, basic username/password, etc.
Or allow only a single login method such as Google only.
Whichever options you pick will help you decide which ID to use.
The auth rules in your question are only stating that the users can read/write their own (presumably) user data.
I assume that you are rather looking for a solution to authorize the user to create meetup data and you should crerate rules similar to this:
These rules allow any user that is logged in to create meetups
{
"rules": {
"meetups": {
"$meetupId": {
".read": "auth.uid != null",
".write": "auth.uid != null"
}
}
}
}
Your code-snippet that pushes new meetup data to the database will automatically try and succeed or fail depending on whether the user was logged in or not. You don't need to specifically tell Firebase in which way the user was logged in. Firebase SDK will take care of the authentication for you.
But if you do want to provide different mechanisms depending on which login type that the user is authenticated with, you can check it in the rules. For example if you want to make sure that the user is not just "anonymously" logged in.
See the documentation: https://firebase.google.com/docs/database/security/user-security#section-variable
the documentation has you covered there: Authenticate Using Google Sign-In with JavaScript.
You can let your users authenticate with Firebase using their Google Accounts by integrating Google Sign-In into your app. You can integrate Google Sign-In either by using the Firebase SDK to carry out the sign-in flow, or by carrying out the Google Sign-In flow manually and passing the resulting ID token to Firebase.
Before you begin:
Add Firebase to your JavaScript project.
Enable Google Sign-In in the Firebase console:
In the Firebase console, open the Auth section.
On the Sign in method tab, enable the Google sign-in method and click Save.
Handle the sign-in flow with the Firebase SDK
If you are building a web app, the easiest way to authenticate your users with
Firebase using their Google Accounts is to handle the sign-in flow with the
Firebase JavaScript SDK. (If you want to authenticate a user in Node.js or
other non-browser environment, you must handle the sign-in flow manually.)
To handle the sign-in flow with the Firebase JavaScript SDK, follow these steps:
Create an instance of the Google provider object:
var provider = new firebase.auth.GoogleAuthProvider();
Optional: Specify additional OAuth 2.0 scopes that you want to request from the authentication provider. To add a scope, call addScope().
For example:
provider.addScope('https://www.googleapis.com/auth/contacts.readonly');
See the authentication provider documentation.
Optional: To localize the provider's OAuth flow to the user's preferred language without explicitly passing the relevant custom OAuth parameters, update the language code on the Auth instance before starting the OAuth flow.
For example:
firebase.auth().languageCode = 'pt';
// To apply the default browser preference instead of explicitly setting it.
// firebase.auth().useDeviceLanguage();
Optional: Specify additional custom OAuth provider parameters that you want to send with the OAuth request. To add a custom parameter, call setCustomParameters on the initialized provider with an object containing the key as specified by the OAuth provider documentation and the corresponding value.
For example:
provider.setCustomParameters({
'login_hint': 'user#example.com'
});
Reserved required OAuth parameters are not allowed and will be ignored. See the authentication provider reference for more details.
Authenticate with Firebase using the Google provider object. You can prompt your users to sign in with their Google Accounts either by opening a pop-up window or by redirecting to the sign-in page. The redirect method is preferred on mobile devices.
To sign in with a pop-up window, call signInWithPopup:
firebase.auth().signInWithPopup(provider).then(function(result) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
}).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});
Also notice that you can retrieve the Google provider's OAuth token which can be used to fetch additional data using the Google APIs.
This is also where you can catch and handle errors. For a list of error codes have a look at the Auth Reference Docs.
To sign in by redirecting to the sign-in page, call signInWithRedirect:
firebase.auth().signInWithRedirect(provider);
Then, you can also retrieve the Google provider's OAuth token by calling getRedirectResult() when your page loads:
firebase.auth().getRedirectResult().then(function(result) {
if (result.credential) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// ...
}
// The signed-in user info.
var user = result.user;
}).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});

Categories