Quick Background: I'm programming an API that is thought to be used "standalone" i.e. there is no frontend involved. API access should be possible directly from e.g. Postman or Curl with a Bearer token in the Authentication Header.
I was looking at Google Firebase and thought it is probably a really good fit because all of the authentication is already "builtin" and directly compatible with Google Cloud Functions.
However after a weekend of experimenting I can not seem to figure out how to implement an REST API (With Google Cloud Functions) where the User can (In an web-interface) request an API token to interact with the API.
I don't want to handle authentication myself. I really would love to use the Firebase authentication for the API.
Here is what the final process should look like:
User logs into an web-interface with the standard Firebase Authentication process.
User clicks on something like "Request API Key" and gets a key shown in the web-interface (e.g. abc...). that is generated by Firebase Authentication.
User can make requests with e.g. curl to the API Hosted in Google Cloud Functions and just has to set the Authorization Header (Bearer abc...) and the "validation" of that token is handled by Firebase Authentication.
Here is what I already tried to generate the token:
admin.auth().createCustomToken(uid)
.then(function(customToken) {
console.log(customToken);
})
.catch(function(error) {
console.log('Error creating custom token:', error);
})
And then set the Token logged to the console in Postman as Bearer Token, and then use the following function to verify the token:
const authenticate = async (req, res, next) => {
if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) {
res.status(403).send('Unauthorized');
return;
}
const idToken = req.headers.authorization.split('Bearer ')[1];
try {
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
req.user = decodedIdToken;
next();
return;
} catch(e) {
console.log(e);
res.status(403).send('Unauthorized');
return;
}
}
Then I get this error
message: 'verifyIdToken() expects an ID token, but was given a custom token. See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.'
I understand that if I would implement an web-interface I could grab the ID token from the devtools (?), but the token is then only valid 1 hour... What I need is a token that is valid "indefinitely" and can be generated and shown to the user.
I think I know that I have to use Custom Tokens somehow but can not figure out how to get them working... (https://firebase.google.com/docs/auth/admin/create-custom-tokens).
Thanks very much in advance everybody!
Best
Rick
You're trying to build an API management solution on top of Firebase and Cloud Functions. Custom tokens and ID tokens are not suitable for this purpose. Custom tokens are only meant to be used as a user authentication credential on end user devices, and ID tokens represent a successful auth response. Both types of tokens expire after an hour.
If you need long-lived, managed API keys, then you will have to implement them yourself. There's nothing built into Firebase that you can use out of the box. I once implemented such a solution as a prototype, where I generated a Firestore document each time a user signed in and requested an API key. Then I used the document ID as the API key, which I could validate in the Cloud Function.
const apiKey = req.headers.authorization.split('Bearer ')[1];
const doc = await admin.firestore().collection('apiKeys').doc(apiKey).get();
if (doc.exists) {
next();
}
I also had to implement some local API key caching to make this work efficiently.
You might be able to avoid some of this work by using a solution like Google Cloud Endpoints (https://cloud.google.com/endpoints), although I don't have any personal experience with that. Finally, also look at open source solutions like https://wso2.com/api-management/ that enable you to set up your own API key management and gateway.
Related
Working on a Firebase app that will help manage a users Google Calendar.
I am using the official Google Calendar Quickstart Guide code - https://developers.google.com/calendar/api/quickstart/js
Everything works great, I can sign in, authorize access and pull the calendar events.
Firebase allows you to log a user in by passing Firebase a Google ID token.
On this official Firebase guide, https://firebase.google.com/docs/auth/web/google-signin#expandable-2 it shows how to use the Sign In With Google library and then pass the resulting ID token to Firebase to sign in.
Everything works fine on the provided Firebase code until it get to this line.
const idToken = response.credential;
The token returned to the Google Sign In callback doesn't include a credential.
The object has these properties:
access_token, expires_in, scope, token_type
So when I try to access the .credential on the response it is undefined, so the resulting call to login to Firebase with that credential fails.
The new Sign In With Google flow separates the authentication and authorization. https://developers.google.com/identity/gsi/web/guides/overview#separated_authentication_and_authorization_moments and states
"To enforce this separation, the authentication API can only return ID
tokens which are used to sign in to your website, whereas the
authorization API can only return code or access tokens which are used
only for data access but not sign-in."
Something seems strange because it appears the token being returned is the Google Calendar data access token, when I thought it would be the Google Sign in token.
I've googled every combination, and read any related SO answer I can think of trying to fix this, seems like I am missing something simple.
Figure out it was the wrong token, because when I removed everything out and just tried to implement a basic Sign In With Google, that token works.
Used the Google provided button/popup that their library provides from their guide here:
<div id="g_id_onload"
data-client_id="CLIENT_ID_GOES_HERE"
data-callback="handleCredentialResponse">
</div>
In the handleCredentialResponse callback, the returned token did have a .credential
Passing that to Firebase worked to login.
const idToken = response.credential;
const provider = new firebase.auth.GoogleAuthProvider();
const credential = provider.credential(idToken);
auth.signInWithCredential(credential).catch((error) => {
// Handle Errors here.
});
So obviously I wasn't understanding what was happening in the Google Quick start example.
Now I assume I can use the Google Sign In Token to request Calendar OAuth permissions.
We have built a custom nodejs backend but the authentication is using firebase auth with idtoken, the idtokens expire after 1 hour and the user is automatically logged out. When using firestore this is handled automatically, we have seen solutions that suggest a service worker but that has not worked.
Can someone please suggest a stable solution for this may be a middleware on the backend API's that can regenerate the tokens?
Thanks
The user is not logged out and that is why Firestore keeps working. You can use getIdToken() method again to get user's ID Token and then pass it in API request.
firebase.auth().currentUser.getIdToken(/* forceRefresh */ true).then(function(idToken) {
// Send token to your backend via HTTPS
// ...
}).catch(function(error) {
// Handle error
});
The normal approach (which the Firebase services themselves use too) is to always the current token with each request to the backend service, so that the service has at least 5m to complete the request (which is a lot more than most services need).
If you need a token that can be used for longer, you can consider forcing a refresh of the token before you call the service as Dharmaraj pointed out in their answer.
Alternative, you can switch to using session cookies for the user, which can have an expiration of up to two weeks.
I'm trying to implement Google sign-in and API access for a web app with a Node.js back end. Google's docs provide two options using a combo of platform.js client-side and google-auth-library server-side:
Google Sign-In with back-end auth, via which users can log into my app using their Google account. (auth2.signIn() on the client and verifyIdToken() on the server.)
Google Sign-in for server-side apps, via which I can authorize the server to connect to Google directly on behalf of my users. (auth2.grantOfflineAccess() on the client, which returns a code I can pass to getToken() on the server.)
I need both: I want to authenticate users via Google sign-in; and, I want to set up server auth so it can also work on behalf of the user.
I can't figure out how to do this with a single authentication flow. The closest I can get is to do the two in sequence: authenticate the user first with signIn(), and then (as needed), do a second pass via grantOfflineAccess(). This is problematic:
The user now has to go through two authentications back to back, which is awkward and makes it look like there's something broken with my app.
In order to avoid running afoul of popup blockers, I can't give them those two flows on top of each other; I have to do the first authentication, then supply a button to start the second authentication. This is super-awkward because now I have to explain why the first one wasn't enough.
Ideally there's some variant of signIn() that adds the offline access into the initial authentication flow and returns the code along with the usual tokens, but I'm not seeing anything. Help?
(Edit: Some advice I received elsewhere is to implement only flow #2, then use a secure cookie store some sort of user identifier that I check against the user account with each request. I can see that this would work functionally, but it basically means I'm rolling my own login system, which would seem to increase the chance I introduce bugs in a critical system.)
To add an API to an existing Google Sign-In integration the best option is to implement incremental authorization. For this, you need to use both google-auth-library and googleapis, so that users can have this workflow:
Authenticate with Google Sign-In.
Authorize your application to use their information to integrate it with a Google API. For instance, Google Calendar.
For this, your client-side JavaScript for authentication might require some changes to request
offline access:
$('#signinButton').click(function() {
auth2.grantOfflineAccess().then(signInCallback);
});
In the response, you will have a JSON object with an authorization code:
{"code":"4/yU4cQZTMnnMtetyFcIWNItG32eKxxxgXXX-Z4yyJJJo.4qHskT-UtugceFc0ZRONyF4z7U4UmAI"}
After this, you can use the one-time code to exchange it for an access token and refresh token.
Here are some workflow details:
The code is your one-time code that your server can exchange for its own access token and refresh token. You can only obtain a refresh token after the user has been presented an authorization dialog requesting offline access. If you've specified the select-account prompt in the OfflineAccessOptions [...], you must store the refresh token that you retrieve for later use because subsequent exchanges will return null for the refresh token
Therefore, you should use google-auth-library to complete this workflow in the back-end. For this,
you'll use the authentication code to get a refresh token. However, as this is an offline workflow,
you also need to verify the integrity of the provided code as the documentation explains:
If you use Google Sign-In with an app or site that communicates with a backend server, you might need to identify the currently signed-in user on the server. To do so securely, after a user successfully signs in, send the user's ID token to your server using HTTPS. Then, on the server, verify the integrity of the ID token and use the user information contained in the token
The final function to get the refresh token that you should persist in your database might look like
this:
const { OAuth2Client } = require('google-auth-library');
/**
* Create a new OAuth2Client, and go through the OAuth2 content
* workflow. Return the refresh token.
*/
function getRefreshToken(code, scope) {
return new Promise((resolve, reject) => {
// Create an oAuth client to authorize the API call. Secrets should be
// downloaded from the Google Developers Console.
const oAuth2Client = new OAuth2Client(
YOUR_CLIENT_ID,
YOUR_CLIENT_SECRET,
YOUR_REDIRECT_URL
);
// Generate the url that will be used for the consent dialog.
await oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope,
});
// Verify the integrity of the idToken through the authentication
// code and use the user information contained in the token
const { tokens } = await client.getToken(code);
const ticket = await client.verifyIdToken({
idToken: tokens.id_token!,
audience: keys.web.client_secret,
});
idInfo = ticket.getPayload();
return tokens.refresh_token;
})
}
At this point, we've refactored the authentication workflow to support Google APIs. However, you haven't asked the user to authorize it yet. Since you also need to grant offline access, you should request additional permissions through your client-side application. Keep in mind that you already need an active session.
const googleOauth = gapi.auth2.getAuthInstance();
const newScope = "https://www.googleapis.com/auth/calendar"
googleOauth = auth2.currentUser.get();
googleOauth.grantOfflineAccess({ scope: newScope }).then(
function(success){
console.log(JSON.stringify({ message: "success", value: success }));
},
function(fail){
alert(JSON.stringify({message: "fail", value: fail}));
});
You're done with the front-end changes and you're only missing one step. To create a Google API's client in the back-end with the googleapis library, you need to use the refresh token from the previous step.
For a complete workflow with a Node.js back-end, you might find my gist helpful.
While authentication (sign in), you need to add "offline" access type (by default online) , so you will get a refresh token which you can use to get access token later without further user consent/authentication. You don't need to grant offline later, but only during signing in by adding the offline access_type. I don't know about platform.js but used "passport" npm module . I have also used "googleapis" npm module/library, this is official by Google.
https://developers.google.com/identity/protocols/oauth2/web-server
https://github.com/googleapis/google-api-nodejs-client
Check this:
https://github.com/googleapis/google-api-nodejs-client#generating-an-authentication-url
EDIT: You have a server side & you need to work on behalf of the user. You also want to use Google for signing in. You just need #2 Google Sign-in for server-side apps , why are you considering both #1 & #2 options.
I can think of #2 as the proper way based on your requirements. If you just want to signin, use basic scope such as email & profile (openid connect) to identify the user. And if you want user delegated permission (such as you want to automatically create an event in users calendar), just add the offline access_type during sign in. You can use only signing in for registered users & offline_access for new users.
Above is a single authentication flow.
I'm using AWS Lambda, Cognito, and API Gateway (orchestrated with Serverless) to build an API for my web-app.
A user authenticates using Cognito, and then makes an authenticated request to the API (pattern copied from the Serverless Stack tutorial, where I grab their Cognito ID:
event.requestContext.identity.cognitoIdentityId
Then I grab the user record associated with that cognitoIdentityId to perform role/permissions based logic and return the relevant data.
The trouble I've been running into is that when different people (other devs I'm working with, currently) log in using the same credentials, but from different computers (and, in some cases, countries), the cognitoIdentityId sent with their request is completely different -- for the same user userPool user record!
Note: I am not integrating with any "Federated Identities" (ie, Facebook, etc). This is plain old email sign-in. And everyone is using the same creds, but some people's requests come from different Cognito IDs.
This is highly problematic, because I don't see another way to uniquely identify the user record in my DB associated with the Cognito record.
QUESTIONS: Am I missing something? Is there a better way to do this? Is this the expected behavior?
The API is currently not actually plugged into a DB. Because our data structure is still in flux, and the app is far from live, I've built out an API that acts like it integrates with a database, and returns data, but that data is just stored in a JSON file. I'll reproduce some of the relevant code below, in case it's relevant.
An example lambda, for fetching the current user:
export function getSelf(event, context, callback) {
const { cognitoID } = parser(event);
const requester = cognitoID && users.find(u => u.cognitoID === cognitoID);
try {
if (requester) {
return callback(null, success(prep(requester, 0)));
} else {
return authError(callback, `No user found with ID: ${cognitoID}`);
}
} catch (error) {
return uncaughtError(callback, error);
}
}
That parser stuff up top is just a util to get the ID I want.
The associated user record might look like this:
{
cognitoID: 'us-west-2:605249a8-8fc1-40ed-bf89-23bc74ecc232',
id: 'some-slug',
email: 'email#whatever.com',
firstName: 'John',
lastName: 'Jacob Jingleheimer Schmidt',
headshot: 'http://fillmurray.com/g/300/300',
role: 'admin'
},
Cognito User Pools is used to authenticate users and provides you JWT tokens. When you want to access any AWS Services you need AWS Credentials (access key and secret key). This is where you should use Federated Identities. The tokens you get from Cognito User Pools should be exchanged with Federated Identities to get AWS credentials to access other AWS services. The serverless-stack also covers this in detail.
Now since you have not added the user pool as an authentication provider in your identity pool, my observation is that you are getting an unauthenticated identity from Federated Identities (you can confirm this from the Amazon Cognito console) which is why it is different for each of your team members. You should add the user pool as an authentication provider in the identity pool and follow the documentation to provide the information required in logins map.
I am using the Drive API and the Google Sheets API to list users private and public spreadsheets using Google Console app.
I want the user to log in to our app the first time to view their files (this process is working). Now I want this user can access his files list without login after first-time authorization.
According to the Google guide, for this, I need to generate the access token and refresh token. I am having both the access token and refresh token.
here:
{
"access_token":"1/fFAGRNJru1FTz70BzhT3Zg",
"expires_in":3920,
"token_type":"Bearer",
"refresh_token":"1/xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI"
}
Now how I can use the refresh token and access token for users to avoid login in again and again. Please give a step-by-step process.
{
"access_token":"1/fFAGRNJru1FTz70BzhT3Zg",
"expires_in":3920,
"token_type":"Bearer",
"refresh_token":"1/xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI"
}
With the access_token you can get data from your google APIs. The token is specific to the API you have got it from. Use it as Authorization: Bearer 1/fFAGRNJru1FTz70BzhT3Zg).
You can use the refresh token to renew the access token. Access tokens expire after some time (3920 secs in your case).
The general Google API docs just say:
Access tokens have limited lifetimes. If your application needs access to a Google API beyond the lifetime of a single access token, it can obtain a refresh token. A refresh token allows your application to obtain new access tokens.
See https://developers.google.com/identity/protocols/oauth2?hl=en
Here's a step by step instruction available for the playground:
https://github.com/ivanvermeyen/laravel-google-drive-demo/blob/master/README/2-getting-your-refresh-token.md
Please have a look at this SO question/answer. An answer in this URL provides the following code:
window.gapi.client.init({
apiKey: this.GOOGLE.API_KEY,
clientId: this.GOOGLE.CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(()=>{
const authInstance = window.gapi.auth2.getAuthInstance();
authInstance.grantOfflineAccess()
.then((res) => {
console.log(res);
this.data.refreshToken = res.code;
});
});