Firebase customToken workflow (ICW Angular2 & Express) - firebase

I'm trying to wrap my head around the best way to handle the creation of a customToken with Firebase in a secure way.
This is what I came up with:
The user logs in on the client side with email and password. firebase.auth().signInWithEmailAndPassword(email, password)
Storing the current UID of the user in local storage. localStorage.setItem('uid', response.uid);
Get the JWT token of the current user. firebase.auth().currentUser.getToken(true) and store the token in localStorage localStorage.setItem('token', res)
Make a post call to the server and add the token to Authorization header and send the UID in the body. const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Authorization', localStorage.getItem('token'));
this.http.post('/api/login', localStorage.getItem('uid'), {
headers: headers
})
On the serverside verify the token const authorization = req.headers.authorization; admin.auth().verifyIdToken(authorization). If valid set the UID this.uid = decodedToken.uid;
Now generate the custom token. Add the additionalClaims const additionalClaims = {
premiumAccount: true }; and call the createCustomToken function. admin.auth().createCustomToken(this.uid, additionalClaims)
Send the custom token back to the client res.status(200).send({
token: customToken
})
On the client side login with the customToken. firebase.auth().signInWithCustomToken(data.token)
Is this summary a good practice or are there better ways to handle this?

Related

How i get new access token when current access token has expired, google firebase auth?

I use firebase at client and use firebase-admin at server.
At client i use signInWithPopUp to sign in to my web app with google account, then receive accessToken, expirationTime, refreshToken at client.
stsTokenManager:{
accessToken: "eyJhbGciOiJzGcaUzI1JKKIsIzxcXzStpZC... ,
expirationTime: 1648809531166 ,
refreshToken: "AIwxAjDfh8_SkckvoSsASxXllMkIX8GBNx...
And i use verify function in firebase-admin lib for verify token was send by client, but until token has expired(1h), i can't use this token.
How i get new accesstoken when current access token has expired?
You don't have to necessarily have to read user's idToken, store it anywhere or refresh it yourself. Instead you just need to use getIdToken() to get user's ID Token. If the token has expired, Firebase SDK will refresh and return a new token or return existing one. You can use getIdToken() before every request to your API.
const userToken = await firebase.auth().currentUser.getIdToken();
// pass userToken to in the API request
// API request here
You can use the following to obtain the oauth access token:
const provider = new GoogleAuthProvider()
provider.addScope(...)
...
const result = await signInWithPopup(auth, provider)
const credential = GoogleAuthProvider.credentialFromResult(result)
console.log(credential.accessToken)
I don't know how to refresh that token.

Discourse SSO with Firebase (Discourse Connect)

I have a website that uses firebase authentication and I want to use these same login details for discourse, whats the best way to achieve this?
1. Using Redirects
This is the workflow I have found the most elegant. It allows sign in from discourse with a single click as long as you are signed in on the main site, because firebase auth will auto-sign-in on visit.
The flow of data is like this:
forum -> site
send Discourse SSO Request
site -> server
send user token and Discourse SSO Request
server -> site -> forum
send redirect to forum with Discourse Login
Here is the code used to create this:
forum - Discourse Connect Url
https://MY-SITE.com/forum-sign-in
site - page visit handler
This part should go on your website when people visit /forum-sign-in. It will automatically trigger the sso flow once they are signed in, which will happen automatically if they have previously signed in.
auth.onAuthStateChanged(async user => {
if (!user)
//this means user was not already signed in.
//allow the user to sign in like normal
//this callback will be called again once they do
return
//generate firebase auth token
const idToken = await user.getIdToken(true)
//get the search params that discourse sent
//if you're in react you should probably do useSearchParams or something
const params = new URLSearchParams(document.location.search)
const discoursePayload = params.get('sso')
const discourseSignature = params.get('sig')
const response = await fetch(`https://MY-BACKEND.com/discourseAuth`, {
method: 'POST', //sending a json payload
mode: 'cors', //work with another domain
cache: 'no-cache', //send every time
headers: { 'Content-Type': 'application/json' }, //sending a json payload
body: JSON.stringify({
discoursePayload,
discourseSignature,
idToken
})
})
const json = await response.json()
if (json?.redirectUrl)
//lets go back to forum with credentials in the redirectUrl
//we're doing a custom redirect instead of a redirect-follow which is easier for security
window.location.replace(json.redirectUrl)
else
//something went wrong
throw new Error(`Redirect URL not found in response - ${response.status} - ${json}`)
})
server - auth handler
I'm using firebase functions here, but as long as you have access to firebase-admin you can use whatever backend you like. Also this example includes some firestore stuff for usernames etc, which is not required.
import DiscourseSSO from 'discourse-sso'
import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'
const auth = admin.auth()
const firestore = admin.firestore()
const discourse = new DiscourseSSO(ssoSecret)
export const discourseAuth = functions.https.onRequest((req, res) => {
if (!handleCors(req, res))
return
//1. validate discourse payload
const { idToken, discoursePayload, discourseSignature } = req.body
if (!discourse.validate(discoursePayload, discourseSignature))
return res.status(401).send(`Bad Discourse Payload: ${origin}`)
//2. validate user
const decodedClaims = await auth.verifyIdToken(idToken).catch()
if (!decodedClaims)
return res.status(401).send(`Bad Id Token: ${idToken}`)
const { uid } = decodedClaims
const user = await auth.getUser(uid).catch()
if (!user)
return res.status(401).send(`User Not Found: ${uid}`)
//3. get user firestore (optional)
const firestoreDoc = await firestore.collection('users').doc(uid).get()
const userData = firestoreDoc.data()
//4. build discourse auth body
const q = discourse.buildLoginString({
nonce: discourse.getNonce(discoursePayload),
external_id: uid,
email: user.email,
//optional
name: userData.displayName,
username: userData.username,
avatar_url:userData.avatar
})
const redirectUrl = `$https://forum.MY-SITE.com/session/sso_login?${q}`
res.status(200).send(JSON.stringify({ redirectUrl }))
const handleCors = (req, res) => {
const origin = req.headers.origin || req.header('origin')
if(origin != 'https://MY-SITE.com'){
res.status(401).send(`Bad Origin: ${origin}`)
return false
}
res.set('Access-Control-Allow-Origin', origin)
res.set('Access-Control-Allow-Credentials', 'true')
res.set('Access-Control-Allow-Headers', ['Content-Type'])
if (req.method === 'OPTIONS'){
res.status(200).end()
return false
}
return true
}
2. Using Cookies
The second method is one I had trouble with because my server is on a different domain from the client and forum. This means that Safari will treat the cookies as third party and give you a hard time when trying to pass them around. If this isn't the case for you, check out this discussion and this GitHub gist. The credentials are all the same, the only difference is how they are passed around. It is something like this:
site -> server
send Firebase user token
server -> site
set session cookie
forum -> server
send cookie and discourse SSO request
server -> forum
send Discourse Login

What are access token and refresh token from firebase auth result

I set up my APP and can get an auth result by using firebase.auth.OAuthProvider('google.com'). Inside the returned result, there is an User object that contains refreshToken. Is this the refresh token generated by google.com so I can use it later to refresh an access token which will be used to access gmail? I am talking about this field [google reference].(https://firebase.google.com/docs/reference/js/firebase.User#refreshtoken)
My second question is where is the access token from google.com? Is it the credential.idToken?
const auth = firebase.auth();
var provider = new firebase.auth.GoogleAuthProvider();
const provider = new firebase.auth.OAuthProvider("google.com");
const currentUser = auth.currentUser;
auth.signInWithPopup(provider).then((result) => {
console.log(result.user) // contains `refreshToken`
console.log(result.credential.idToken) // is `idToken` access token?
}).catch((reason) => {
reject(reason);
});

Sync data between Google Firestore and Google Sheets using Cloud Functions/Admin SDK

While using Cloud Firestore as data backend, I need to share some data collections with non-tech site managers (editors, sales teams, etc.). Also, I wish to give these people access to edit the data stored in Cloud Firestore.
Google Sheets is a very familiar tool with site managers which can save me time in developing a CRUD admin panel like the interface from scratch for data updating and viewing.
This Stack Overflow answer shows how to send data using cloud function and levels deep, and this Github library can get data from Firestore using Google Apps Script (I wish to do it using Cloud Functions or Firebase Admin SDK), but I am still trying to figure out how to make an end-to-end Sheets based interface.
Please guide if there are any better alternatives to achieve the same objective. I'm facing some difficulties switching from SQL databases and Django auto-created admin interfaces to the Firebase-Firestore NoSQL world.
I understand that you want to be able to call a Cloud Function from a Google Sheet in order to build an "end-to-end Sheets based interface" for Firestore.
You can use the UrlFetchApp Class to make a request to fetch the URL of an HTTP Cloud Function.
You Apps Script code would be like:
function callSimpleHTTPCloudFunction() {
const url = "https://xxxxxxxx.cloudfunctions.net/simpleHttp";
response = UrlFetchApp.fetch(url, {
method: 'get'
})
respObj = JSON.parse(response.getContentText());
Logger.log(respObj);
}
While your Cloud Function would be like:
exports.simpleHttp = functions.https.onRequest((req, res) => {
res.send({ msg: 'simpleHttp' });
});
This is a very simple example of Cloud Function, but you can adapt this Cloud Function to read and write data from/to Firestore. Have a look at this official video for a starting point: https://www.youtube.com/watch?v=7IkUgCLr5oA&t=1s&list=PLl-K7zZEsYLkPZHe41m4jfAxUi0JjLgSM&index=3
Now, if you want to authenticate your users in such a way you can control who can access your data through the Cloud Function, it is going to be a bit more complex.
There is an official Cloud Function Sample which shows "how to restrict an HTTPS Function to only the Firebase users of your app": https://github.com/firebase/functions-samples/tree/master/authorized-https-endpoint
As explained in the code comments: "The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this: Authorization: Bearer <Firebase ID Token>. When decoded successfully, the ID Token content will be added as req.user."
So you need, in your Apps Script code, to generate a Firebase ID Token for the Firebase user. For that we will use the Firebase Auth REST API. In this example we will use the email of the user authenticated in the Google Sheet (Session.getActiveUser().getEmail()) as the Firebase User Name.
As explained in the doc, to call the Firebase Auth REST API, you need to obtain a Web API Key for your Firebase project, through the project settings page in your Firebase admin console.
The following Apps Script function will do the job:
function getToken() { {
const userName = Session.getActiveUser().getEmail();
const pwd = 'xyz' //For example get the password via a prompt.
//This is NOT the password of the account authenticated with Google Sheet, but the password of the Firebase user. In this example, the emails are the same but they are different accounts.
const verifyPasswordUrl = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=[API_KEY]" //Replace with your Web API Key
const payload = JSON.stringify({"email":userName,"password": pwd,"returnSecureToken": true});
const verifyPasswordResponse = UrlFetchApp.fetch(verifyPasswordUrl, {
method: 'post',
contentType: 'application/json',
muteHttpExceptions: true,
payload : payload
});
const token = JSON.parse(verifyPasswordResponse.getContentText()).idToken;
return token;
}
Then, still in Apps Script, you use the token in the call to the Cloud Function, as follows:
function callSecuredHTTPCloudFunction() {
const authHeader = {"Authorization": "Bearer " + getToken()};
const url = "https://us-central1-<yourproject>.cloudfunctions.net/securedHttp/";
const response = UrlFetchApp.fetch(url, {
method: 'get',
headers: authHeader,
muteHttpExceptions: true,
});
Logger.log(response);
//Here do what you want with the response from the Cloud Function, e.g. populate the Sheet
}
The Cloud Function code would be as follows, adapted from the official example.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const cors = require('cors')({
origin: true
});
const express = require('express');
const cookieParser = require('cookie-parser')();
const app = express();
// Express middleware that validates Firebase ID Tokens passed in the Authorization HTTP header.
// The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
// `Authorization: Bearer <Firebase ID Token>`.
// when decoded successfully, the ID Token content will be added as `req.user`.
const validateFirebaseIdToken = (req, res, next) => {
console.log('Check if request is authorized with Firebase ID token');
if (
!req.headers.authorization ||
!req.headers.authorization.startsWith('Bearer ')
) {
console.error(
'No Firebase ID token was passed as a Bearer token in the Authorization header.',
'Make sure you authorize your request by providing the following HTTP header:',
'Authorization: Bearer <Firebase ID Token>'
);
res.status(403).send('Unauthorized');
return;
}
let idToken;
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer ')
) {
console.log('Found "Authorization" header');
// Read the ID Token from the Authorization header.
idToken = req.headers.authorization.split('Bearer ')[1];
console.log(idToken);
} else {
// No cookie
res.status(403).send('Unauthorized');
return;
}
admin
.auth()
.verifyIdToken(idToken)
.then(decodedIdToken => {
console.log('ID Token correctly decoded', decodedIdToken);
req.user = decodedIdToken;
return next();
})
.catch(error => {
console.error('Error while verifying Firebase ID token:', error);
res.status(403).send('Unauthorized');
});
};
app.use(cors);
app.use(cookieParser);
app.use(validateFirebaseIdToken);
app.get('/', (req, res) => {
res.send(`Your email is ${req.user.email}`);
});
// This HTTPS endpoint can only be accessed by your Firebase Users.
// Requests need to be authorized by providing an `Authorization` HTTP header
// with value `Bearer <Firebase ID Token>`.
exports.securedHttp = functions.https.onRequest(app);
You can very well write a similar function with a POST and a payload in order to send data from the Google Sheet to the Cloud Function and then write to Firestore.
Finally, note that you could implement the same approach for calling, from the Google Sheet, the Firestore REST API instead of calling Cloud Functions.

How to use the Firebase refreshToken to reauthenticate?

I use the JS library call firebase.auth().signInWithEmailAndPassword(email, password) and get back a User object. The User object contains a refreshToken.
I use curl 'https://docs-examples.firebaseio.com/rest/saving-data/auth-example.json?auth=TOKEN' to make calls to Firebase.
The token will eventually expire. In order to make it look like the application (iOS and macOS) has persistent login, I want to refresh the token, how do I do that with using either the REST or JS library? I can't find any calls in the documentation that allow me to use the refreshToken to get a new token.
When you make call from a browser .getIdToken(true) will automatically refresh your token. Make call like this:
firebase.auth().currentUser.getIdToken(/ forceRefresh / true)
.then(function(idToken) {
}).catch(function(error) {
});
More info here https://firebase.google.com/docs/reference/js/firebase.User#getIdToken
** UPDATE ** this is also now documented in Firebase REST docs under Exchange a refresh token for an ID token section:
https://firebase.google.com/docs/reference/rest/auth/#section-refresh-token
Currently the only way I found to do this is here:
https://developers.google.com/identity/toolkit/reference/securetoken/rest/v1/token
You must make an HTTP request:
POST https://securetoken.googleapis.com/v1/token?key=YOUR_KEY
Where YOUR_KEY can be found in the Google developers console > API Manager > Credentials. It's under the API Keys section.
Make sure request body is structured in the following format:
grant_type=refresh_token&refresh_token=REFRESH_TOKEN
Where REFRESH_TOKEN is the refresh token from Firebase user object when they signed in.
You must set the header Content-Type: application/json or you will get errors (e.g. "MISSING_GRANT_TYPE").
The POST call will return a new idToken (used to be called access_token)
I guess most people here are looking for a way to persist their authentication not in a browser but e.g. on a node backend. Turns out there actually is a way to do this:
Trade the refresh-token for an access-token (using google's public api)
Trade the access-token for a custom-token (using a firebase-function, see below)
Login with custom-token
Here's the essence of the code:
const requestP = require('request-promise');
const fsP = require('fs').promises;
const refreshToken = await fsP.readFile('./refresh_token.txt');
const res = await requestP.post({
headers: {'content-type': 'application/x-www-form-urlencoded'},
url: 'https://securetoken.googleapis.com/v1/token?key=' + firebaseConf.apiKey,
body: 'grant_type=refresh_token&refresh_token=' + refreshToken,
json: true
});
const customToken = await requestP.post({
headers: {'content-type': 'text/plain'},
url: 'https://<yourFirebaseApp>.cloudfunctions.net/createCustomToken',
body: {token: res.access_token},
json: true
});
await firebaseApp.auth().signInWithCustomToken(customToken);
And the firebase function:
export const createCustomToken = functions.https.onRequest(async (request, response) => {
response.set('Access-Control-Allow-Origin', '*');
try {
const token = JSON.parse(request.body).token;
const decodedToken = await admin.auth().verifyIdToken(token);
const customToken = await admin.auth().createCustomToken(decodedToken.uid);
response.send(customToken);
} catch(e) {
console.log(e);
response.sendStatus(500);
}
});
// Create a callback which logs the current auth state
function authDataCallback(authData) {
if (authData) {
console.log("User " + authData['uid'] + " is logged with token" + authData['ie']);
} else {
console.log("User is logged out");
}
}
// Register the callback to be fired every time auth state changes
var ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com");
ref.onAuth(authDataCallback);
Event onAuth will be called on page refresh, if user was logged out authData will be null, else not. You can find token in authdata['ie']. In the screenshot bellow I have printed the token after auth and authdata object, how you can see authData['ie'] and token are similar.

Resources