Firebase refresh id tokens for server authentication - firebase

I have a project using firebase where I use firebase id token to verify user on back-end.
Actually, on client, when a user login I use getIdToken(true) and attach it to my requests header.
On server for each request I use verifyIdToken to check if the user is logged in and it's valid.
The problem is that after some time the token expire and the backend is not able to verify the user.
My question is: how to get this work?
I think about using a request interceptor from client side to get an id token for each request but i don't know if this is a good practice and if I have to invalidate the other tokens too with admin.auth().revokeRefreshTokens(userId).
Thanks in advance.
client
axios.interceptors.request.use(function (config) {
if (!firebase.auth().currentUser) {
return config
}
return firebase.auth().currentUser.getIdToken(true).then(token => {
config.headers.Authentication = token
return config
})
}, function (error) {
// Do something with request error
return Promise.reject(error)
})
server
let decodedToken = await admin.auth().verifyIdToken(token);
let userId = decodedToken.uid;

Related

how to handle expiring access token from partner api

I'm new to Next js. I'm sure this is a common issue but I don't know what to search for. Here's an outline:
One of my partners has an API with Bearer auth. The Bearer token comes from an endpoint I call (/auth) with my username and password. That endpoint returns the Bearer token that I use for all other endpoints, but it expires in one day.
How would I handle making API calls on Next.js API routes to this partner? I.e. where would I store this access token so each API route doesn't need to constantly fetch it. And, how do I update it when it expires?
Your clients (once authenticated) should be the ones "storing" these tokens. You would basically need to fetch it from the client's session, cookie, or however you are storing those.
As far as updating these tokens, your auth provider should also provide a "refresh token" that can be used to retrieve a new jwt once it has expired. The purpose here is that you'll be able to refresh the token for the user without requiring them to log in again.
Depending on your provider, this may be a new endpoint you'll need to call.
https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/
If you are making calls to your partner's api when you go to a specific route(like a protected one), then you should store your access token as cookie with http-only flag to avoid security issues like xss attacks that can steal the session data from your browser(the http-only flag should be set in the /auth route in your partner's api when he sends the response with the token), so make the calls within the getServerSideProps sending the cookie with the request, so your partner should take the token from the cookie and validate it to allow the request, one thing i need to point out is that you can't access an http-only cookie from client side, but as you are using nextJs you can still access it from getServerSideProps with a library called nookies, so you could do something like this:
export const getServerSideProps: GetServerSideProps = async ctx => {
const cookies = nookies.get(ctx)
const someApiData = await fetchApiData(cookies)
if (!someApiData) {
return {
redirect: {
// Redirect to home if not authorized
destination: '/',
permanent: false
}
}
}
return {
//return data fetched from the api
props: {
someApiData
}
}
}
and the function that makes the api call, could look like this(note that i'm using axios):
const fetchApiData = async (cookies: CookieData) => {
try {
const result = await axios.get<ApiData>('/some/api/route', {
// Pay attention to this line, here we are sending the cookie with the access token
headers: {
Cookie: `token=${cookies.token}; HttpOnly;`
}
})
return result.data
} catch (error) {
console.log(error)
}
}
Note that you should send the cookie whenever you make a request to a protected route and your partner should validate this token in each route that he wants to protect.
And to refresh the token without login again, your partner can implement a refresh token like #SLee mentioned. This is just an example but you got the idea.

Authenticate *from* Firebase Cloud Functions to custom Node Server

I'm having troubles to authenticate from Firebase Cloud Functions to a custom Node server.
Background:
I use a custom node + express server to receive calls from authenticated clients coming from our website. I can successfully verify their tokens using something like this:
const bearerHeader = req.headers['authorization'];
const bearerToken = bearerHeader.split(' ')[1];
await admin.auth().verifyIdToken( bearerToken );
What I need now:
I need to call one of this endpoints but from a Cloud Function (not from an authenticated web client).
So, on the cloud function I'm calling:
const admin = require('firebase-admin');
admin.initializeApp(); // regular initialization
const token = await admin.app().options.credential?.getAccessToken();
const config = { headers: { Authorization: `Bearer ${token.id_token}` } };
await axios.post(url, body, config);
The problem:
The token.id_token field is missing from getAccessToken():
// token:
{
access_token: 'ya29. ... ... ',
expires_in: 3599,
token_type: 'Bearer'
}
When locally run with other credentials (my user credentials when running firebase functions:shell, for example, or when setting GOOGLE_APPLICATION_CREDENTIALS to a service account credentials file) I do get an extra property id_token that I can use to authenticate. But when deployed and run on the Cloud Function, id_token property is empty.
Any ideas?
Thanks in advance.
PS: an extra problem... the local test with a service account do include id_token, but when authenticating to the server getAccessToken() fails with:
Firebase ID token has incorrect "aud" (audience) claim. Expected "<project-id>" but got "<some-hash>.apps.googleusercontent.com".
Maybe both problems solve the same way. But the server does work properly to verify user tokens coming from a website.
EDIT:
I had to exchange the access_token for an id_token, for which I followed something similar to this, with the projectId as audience.
Now the problem is about the issuer (iss) instead of the audience (aud):
Firebase ID token has incorrect "iss" (issuer) claim. Expected "https://securetoken.google.com/<project id>" but got "https://accounts.google.com".
I guess I could verify the token on the server using the same library, but then my web clients would have the same issuer problem

How to use my firebase authentication to work with external services?

Ok so I am using firebase as authentication for my iOS app. Now I plan on adding video calling to my app using an external service know as connectyCube. This service has their own authentication system and I cannot use their services unless a user is authenticated.
Option 1: I can use their own authentication which means my app would have two authentication systems - not very productive
Option 2: They say I can use an existing authentication to validate users
I understand that this is a common thing in the developers world and I see the word OAuth and JWT being thrown around but I am a rookie developer and I want to understand how I can use firebase and authenticate a user from an external service.
These are the questions they have asked when I opted for the "I have my own authentication" option:
What is your end point URL
Is it GET or POST
Request Headers
Request Params
Response Params
Where do I get all this information from firebase? Any help would be great
As an alternative to #Dharmaraj's answer, you could instead make use of a HTTP Event Cloud Function for this based on the code sample they've provided.
Using this method, you create the endpoint /verifyUserToken to be used by ConnectyCube.
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();
export const verifyUserToken = functions.https.onRequest((req, res) => {
const idToken = req.query.token;
verifyUser(idToken)
.then(
(userData) => {
res.status(200).json(userData)
},
(err) => {
console.log("Token verification failed.", err.code || err.message);
res.status(422).json({error: "User token is invalid"})
}
)
.catch((err) => console.error("Unexpected crash", err));
});
async function verifyUser(token) {
if (!token)
throw new Error("token missing");
// using `true` here to force token to be checked against the Firebase
// Auth API rather than trusting its contents as-is
const { uid, email } = await admin.auth().verifyIdToken(token, true);
// pull the user's username from their user data
// at /users/{userId}/username
const username = (await admin.database().ref("users/" + uid + "/username")).val();
// use user's actual email if available, otherwise fallback
// to a userID based email
const uEmail = email || uid + "#users.noreply.yourapp.com";
// use user's username if available, otherwise fallback to
// the email address above.
const uLogin = username !== null ? username : uEmail;
return {
uid,
login: uLogin,
email: uEmail,
user: {id: uid, login: uLogin, email: uEmail}, // <- this part in particular is used by ConnectyCube
users: [{uid, login: uLogin, email: uEmail}]
};
}
Once deployed, you would use the following settings:
Setting
Value
API URL:
https://us-central1-PROJECT-ID.cloudfunctions.net/verifyUserToken
GET/POST
GET
Request params:
{"token": "#{login}"}
Response params:
{"uid": "#{user.id}", "email": #{user.email}, "login": "#{user.login}"}
It looks like ConnectyCube uses some sort of Session Tokens as mentioned in their documentation with their own username and password.
The most easiest way would be creating a ConnectyCube account whenever a new user signs up in your Firebase app using Firebase Auth Triggers for Cloud functions. Then you can generate username and password on behalf of your user and store them in a Database.
So whenever you need to create a ConnectyCube session, check for the currently logged in user and fetch their ConnectyCube credentials.
async function createCCSession() {
const userId = firebase.auth().currentUser.uid
const ccCrednetials = (await firebase.database().ref(`ccCreds/${userId}`).once('value')).val()
ConnectyCube.createSession(ccCredentials)
.then((session) => {
console.log(session)
return session
}).catch((error) => console.log(error));
}
You can protect the database using security rules so a user can access their credentials only.
{
"rules": {
"ccCreds": {
"$uid": {
".read": "$uid === auth.uid"
}
}
}
}
While I don't normally double-answer a question, in the course of exploring some other authentication related problems, I've managed to eliminate the Cloud Function from my other answer entirely and instead call the Authentication API directly.
Setting
Value
API URL:
https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo?key=FIREBASE_CONFIG_API_KEY
GET/POST
POST
Request params:
{"idToken": "#{login}"}
Response params:
{"uid": "#{users.0.localId}", "email": #{users.0.email}, "full_name": "#{users.0.displayName}"}
On your client, you just call the ConnectyCube Login API with the following data:
POST https://api.connectycube.com/login
login=<Firebase-ID-token>
password=<any-random-value-to-pass-the-validation>

firebase auth with MIcrosoft Graph (accessToken)

I am super hopeful someone can help me - I'm kind of stuck.
I'm happily using firebase auth with Microsoft AD. My AuthProvider is firebase.auth.OAuthProvider('microsoft.com').
When I call firebase.auth().signInWithPopup() with that provider, everything works GREAT. I can pick out the accessToken from the resulting UserCredential and access Microsoft Graph api's no problem (yay!).
Firebase persists and renews the authentication and my app gets the expected callback via onAuthStateChanged with the new firebase.User when the user returns to my SPA later (also yay!).
The bad news (where I'm stuck) is: how do I get the Microsoft Graph accessToken in this flow (e.g. when the user returns to my app later)? I don't want them to have to re-authenticate with another popup (yech).
Basically, how do I go from a valid firebase.User to a MS Graph accessToken when the user returns?
Thanks so so much for any help!
Firebase Auth only focuses on authentication only. They will return the OAuth access token on sign in success via UserCredential but will discard the Microsoft OAuth refresh token and not store any OAuth credential associated with the provider. So you have no way to get a new access token afterwards. If you have a good reason for Firebase Auth to manage OAuth access tokens, please file an official feature request.
UPDATE/answer: so it turns out to be simpler than I thought:
The basic idea is to authenticate (re-authenticate) using firebase and use the same clientID for silent microsoft authentication. However, you must supply a loginHint
parameter to the microsoft auth, even if you were previously authorized. loginHint can
be the email address for the firebase user...
In that scenario, the authentication is shared and you won't need to popup a second sign-in for the "microsoft half" of the process - the firebase auth works fine.
I ended up using microsoft's MSAL library (https://github.com/AzureAD/microsoft-authentication-library-for-js)... something like this:
const graphDebug = false;
const msalLogger = new Logger(msalLogCallback, { level: LogLevel.Error });
export async function graphClient(loginHint: string) {
const msal = new UserAgentApplication({
// gotcha: MUST set the redirectUri, otherwise get weird errors when msal
// tries to refresh an expired token.
auth: { clientId: CLIENT_ID, redirectUri: window.location.origin },
system: { logger: msalLogger },
// TODO: should we set cache location to session/cookie?
});
/**
* Create an authprovider for use in subsequent graph calls. Note that we use
* the `aquireTokenSilent` mechanism which works because firebase has already
* authenticated this user once, so we can share the single sign-on.
*
* In order for that to work, we must pass a `loginHint` with the user's
* email. Failure to do that is fatal.
*/
const authProvider: AuthProvider = callback => {
msal
.acquireTokenSilent({ scopes: SCOPES, loginHint })
.then(result => {
callback(null, result.accessToken);
})
.catch(err => callback(err, null));
};
const client = Client.init({
authProvider,
debugLogging: graphDebug,
});
return client;
}
When you are using signInWithPopup, the result object contains the credentials you are looking for.
firebase.auth().signInWithPopup(provider)
.then(function(result) {
// User is signed in.
// IdP data available in result.additionalUserInfo.profile.
// OAuth access token can also be retrieved:
// result.credential.accessToken
// OAuth ID token can also be retrieved:
// result.credential.idToken
})
.catch(function(error) {
// Handle error.
});
Hope this helps.
If you look deep enough you should find msal access token in firebase response under (firebaseAuth.currentUser as zzx).zzj()

Meteor retrieve Twitter credentials returning undefined

I'm trying to use the Twitter package to retrieve credentials for a user. The client receives the temporary token just fine, but when I use it to retrieve the real credentials, I get undefined. My code is very simple.
// Client
Twitter.requestCredential(function (tokenOrError) {
Meteor.call('register', tokenOrError)
}
// Server
Meteor.methods({
register: function(token) {
var result = Twitter.retrieveCredential(token);
console.log(result); // undefined
}
})
When I look in my mongo database, the pending credentials have correctly been stored in meteor_oauth_pendingCredentials and the key that was returned to the client is correct. When I do the same query in the database it returns the result just fine, but calling retrieveCredential from the server does not.
Any thoughts?
Links to the OAuth code:
https://github.com/meteor/meteor/blob/832e6fe44f3635cae060415d6150c0105f2bf0f6/packages/oauth/pending_credentials.js
https://github.com/meteor/meteor/blob/832e6fe44f3635cae060415d6150c0105f2bf0f6/packages/oauth/oauth_server.js
https://github.com/meteor/meteor/blob/devel/packages/twitter/twitter_server.js
My problem is similar to others that have already been asked (Meteor retrieve Twitter credentials) but the posted solution didn't work for me.
Turns out the API has changed and requires you to retrieve the credential secret on the client and send both the credential secret and token to the server.
// Client
Twitter.requestCredential(function (tokenOrError) {
var secret = Package.oauth.OAuth._retrieveCredentialSecret(tokenOrError)
Meteor.call('register', tokenOrError, secret)
}
// Server
Meteor.methods({
register: function(token, secret) {
var result = Twitter.retrieveCredential(token, secret);
console.log(result);
}
})
How can I get the credential secret in Meteor, when I do Facebook.requestCredential on the client?

Resources