Google Sheet API - Service account with Domain Wide Delegation - The request is missing a valid API key - google-sheets-api

I am using the GAPI library v75 on Node.
I am successfully making calls to the Google Classroom API with a service account and domain wide delegation, in order to make changes in the user's context.
I now need to access a Google Sheet so have activated the Sheets API and authorised the required scopes in the admin console. Here is the code:
const sheets = google.sheets({version: 'v4', jwtObject});
sheets.spreadsheets.get({spreadsheetId : thisSheetId}, function(err, returnValue) {
if (err) {
res.json(err)
} else {
res.json(returnValue.data)
}
});
JWT being generated via:
var jwtClient = new google.auth.JWT(
client_email,
null,
private_key,
scopes,
emailToImpersonate
);
jwtClient.authorize(function (err, tokens) {
if (err) {
console.error('Unable to authenticate with google:', err);
return
}
callback(err, jwtClient)
I generated the JWT exactly the same way that I generate for the classroom API and the Admin SDK and they both work fine. But for the sheets API, I receive:
"code":403,"errors":[{"message":"The request is missing a valid API key.","domain":"global","reason":"forbidden"}]
I do not understand why if I am passing a valid JWT why I need to provide an API key - I did try generating an API key, and then I received a permissions error, which makes sense as the sheet is not public.
Thank you for any help in pointing me in the right direction.

In your script, how about the following modification?
From:
const sheets = google.sheets({version: 'v4', jwtObject});
To:
const sheets = google.sheets({version: 'v4', auth: jwtObject});
Note:
When I tested const sheets = google.sheets({version: 'v4', jwtObject});, I confirmed the same issue with you. So, when I modified your script like the above, I confirmed that the issue was resolved and the values are retrieved from Spreadsheet.

Related

NextJS/Next-Auth Backend Authentication with OAuth

I am currently building a web app based on a turborepo (monorepo) in which I want to use Discord OAuth login with next-auth. Therefore I have two modules web and api, where api is my express backend with discord.js. The web app is basically a dashboard for a Discord bot.
I figured that next-auth only provides client side authentication. So my question is how can I validate the OAuth session from the client side in the best manner?
My middleware for express currently looks like this:
function throwUnauthorized(res: Response) {
res.status(401).json({ code: 401, message: 'Unauthorized' });
}
export async function isAuthorized(req: Request, res: Response, next: NextFunction) {
try {
const authorization = req.headers.authorization;
if (!authorization) {
return throwUnauthorized(res);
}
// validate token with Discord API
const { data } = await axios.get('https://discord.com/api/oauth2/#me', {
headers: { Authorization: authorization },
});
// protect against token reuse
if (!data || data.application.id !== process.env.TC_DISCORD_CLIENT_ID) {
return throwUnauthorized(res);
}
// map to database user
let user = await User.findOne({ id: data.user.id });
user ??= await User.create({ id: data.user.id });
data.user.permissions = user.permissions;
req.user = data.user;
next();
} catch (error) {
return throwUnauthorized(res);
}
}
In this approach the Discord OAuth Token would be send via the Authorization header and checked before each request that requires Authorization. Which leads to my problem: The token needs to be validated again causing multiple request to Discord API.
Is there a better way to handle this? Because I need to map Discord user profiles to database profiles. I read that you could try decode the jwt session token from next-auth, but this did not work when I tested it.
Maybe there is a whole different project structure suggested for my project. But I thought I should separate the api and web-app since I would have needed a custom express server because it includes the Discord bot and Prometheus logging functions. I am open for suggestions and your thoughts!

Twitter API OAuth 403 Error - Authentication with unknown is forbidden

I signed up for the Twitter API yesterday and I've been trying to get it working using node.js and the twitter-api-v2 npm package. I am pretty sure I've used the correct configuration (0Auth 1), I've looked through the twitter developer portal and my read-write permissions are correct. Checked my keys about a dozen times. Checked the twitter-api-v2 documentation too. Anybody know what I might be missing here? and writing something like this:
const {TwitterApi} = require('twitter-api-v2');
const config = {
appKey: 'XXXXXXXXX',appSecret: 'XXXXXXXXX', accessToken: 'XXXXXXXXX',accessTokenSecret: 'XXXXXXXXX', }
// OAuth 1.0a (User context)
const userClient = new TwitterApi( config );
const rwClient = userClient.readWrite;
const tweet = async () => {
try { await rwClient.currentUserV2(); await rwClient.tweet('Testing the Twitter API') } catch (err){ console.error(err) } }
tweet()
Unfortunately, each time I try run this I get the following error:
*
'Authenticating with Unknown is forbidden for this endpoint.
Supported authentication types are [OAuth 1.0a User Context, OAuth 2.0
User Context].'
I am pretty sure I've used the correct configuration (0Auth 1), I've looked through the twitter developer portal and my read-write permissions are correct. Checked my keys about a dozen times. Checked the twitter-api-v2 documentation too. Anybody know what I might be missing here?

Is there a way to call the google books api from firebase auth to get a logged in users bookshelves in "My Library"?

I have the firebase auth set up and can log in, get the consent screen to access google books account, get the token, and log out.
Google will be deprecating the gapi.auth2 module and using Google Identity Services so I'm trying to find a solution that doesn't involve using the gapi.auth2 module.
The following website has an example of how to use the Google Identity Services:
https://developers.google.com/identity/oauth2/web/guides/migration-to-gis#gis-only
In this example they use the Google Identity Services library to get the access_token to then pass it along in the The XMLHttpRequest object to request data from a logged in users account. Seeing as I can already get the access_token I'm trying to make the request without using the Google Identity Service or Google API Client Library for JavaScript and just use a XMLHttpRequest.
The below code returns a response of the index.html page and not the response from the google books API.
function getData(access_token){
if(access_token !== undefined){
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
// Typical action to be performed when the document is ready:
console.log(xhttp.responseText);
}
};
xhttp.open("GET", "books/v1/mylibrary/bookshelves/4/volumes?fields=totalItems, items(id)");
xhttp.setRequestHeader('Authorization', 'Bearer ' + access_token);
xhttp.send();
}
}
function Login(){
useEffect(()=>{
signInWithGoogle();
});
provider.addScope("https://www.googleapis.com/auth/books");
const signInWithGoogle = () =>{
signInWithPopup(auth, provider).then((result)=>{
// This gives you a Google Access Token. You can use it to access the Google API.
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
if(result.user){
getData(token);
}
}).catch((error)=>{
if(error.code === 'auth/popup-closed-by-user'){
}
});
}
return (
<p>Logging in...</p>
)
}
export default Login;
I know that "books/v1/mylibrary/bookshelves/4/volumes?fields=totalItems, items(id)" works because I've used it in a working version that doesn't use firebase as it's just a plain html version that uses the Google Services Identity library with the Google API Client Library for JavaScript. The network request look the same and have the same access_token with my adapted version and the plain html version that works.
Is there a way to call the google books API from firebase auth to get a logged in users bookshelves in "My Library"?

How to pass Firebase Auth Token from client to server?

The website that I'm working on uses Firebase authentication and different users that login have different permissions as to which pages they can visit.
The way signing in is setup is similar to this post:
User Logins in with two parameters - "id" and "email"
Server uses these to create a custom "uid", then uses the Firebase Admin SDK to create a custom token that is sent back to the client.
The client logs in with the Javascript Firebase SDK - firebase.auth().signInWithCustomToken()
Now that the user is logged in, they can click different pages - i.e. '/foo', '/bar'
The issue I'm running into is that when they visit new pages, I'm trying to pass the token from the client back to the server (almost identical to how its done in this Firebase Doc ), verify the token & check if it has permission to view the webpage.
I'm trying to figure out the best (& most secure) way to do this. I've considered the following option:
Construct a URL with the token, but I've heard this isn't good practice because the token is getting exposed and session hijacking becomes a lot easier.
I've been trying to pass the token in the request header, but from my understanding you can't add headers when the user clicks on a link to a different page (or if its redirected in javascript). The same issue applies to using POST.
What can I do to securely pass this information to the server and check permissions when a user clicks on a link to a different page?
You can get the accessToken (idToken) on client side by:
var accessToken = null;
firebase.auth().currentUser
.getIdToken()
.then(function (token) {
accessToken = token;
});
and pass it in your request headers:
request.headers['Authorization'] = 'Bearer ' + accessToken;
and on your server side get the token with your prefered method and authenticate the request with Firebase Admin SDK, like (Node.js):
firebaseAdmin.auth()
.verifyIdToken(accessToken)
.then(decodedIdToken => {
return firebaseAdmin.auth().getUser(decodedIdToken.uid);
})
.then(user => {
// Do whatever you want with the user.
});
Nowadays, it looks like we're meant to use httpsCallable() client-side to get an object pre-authorized to talk to your endpoint.
eg:
// # ./functions/index.js
exports.yourFunc = functions.https.onCall((data, context) => {
// Checking that the user is authenticated.
if (!context.auth) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
'while authenticated.');
}
// ... rest of your method
});
// ./src/models/addMessage.js
const firebase = require("firebase");
require("firebase/functions");
firebase.initializeApp({
apiKey: '### FIREBASE API KEY ###',
authDomain: '### FIREBASE AUTH DOMAIN ###',
projectId: '### CLOUD FUNCTIONS PROJECT ID ###'
databaseURL: 'https://### YOUR DATABASE NAME ###.firebaseio.com',
});
var functions = firebase.functions();
// This is the new code:
var yourFunc = firebase.functions().httpsCallable('yourFunc');
yourFunc({foo: bar}).then(function(result) {
// ...
});
From firebase documentation

How to use Firebase's email & password authentication method to connect with AWS to make Fine Uploader S3 work?

I decided to use Fine Uploader for my current AngularJS project (which is connected to hosted on Firebase) because it has many core features that I will need in an uploader already built in but, I am having trouble understanding how to use Firebase's email & password authentication method to communicate with AWS (Amazon Web Services) to allow my users to use Fine Uploader S3 to upload content. Based on Fine Uploader blog post Uploads without any server code, the workflow goes like:
Authenticate your users with the help of an identity provider, such as Google
Use the temporary token from your ID provider to grab temporary access keys from AWS
Pass the keys on to Fine Uploader S3
Your users can now upload to your S3 bucket
The problem is that I won't be using OAuth 2.0 (which is used by Google, Facebook or Amazon to provide user identities) to allow my user's to sign into my app and upload content. Instead I will be using Firebase's email & password authentication.
So how can I make Firebase's email & password authentication method create a temporary token to grab temporary access keys from AWS and pass those keys on to Fine Uploader S3 to allow my users to upload content to S3?
To connect AWS with an outside application, Cognito is going to be a good solution. It will let you generate an OpenID token using the AWS Node SDK and your secret keys in your backend, that you can then use with the AWS JavaScript SDK and WebIdentityCredentials in your client.
Note that I'm unfamiliar with your specific plugin/tool, but this much will at least get you the OpenID and in my work it does let me connect using WebIdentityCredentials, which I imagine is what they are using.
Configure Cognito on AWS
Setup on Cognito is fairly easy - it is more or less a walkthrough. It does involve configuring IAM rules on AWS, though. How to set this up is pretty project specific, so I think I need to point you to the official resources. They recently made some nice updates, but I am admittedly not up to speed on all the changes.
Through the configuration, you will want to setup a 'developer authenticated identity', take note of the 'identity pool id', and the IAM role ARN setup by Cognito.
Setup a Node Server that can handle incoming routes
There are a lot of materials out there on how to accomplish this, but you want to be sure to include and configure the AWS SDK. I also recommend using body-parser as it will make reading in your POST requests easier.
var app = express();
var bodyParser = require('body-parser');
var AWS = require('aws-sdk');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
Create POST Function to talk with Cognito
Once you have your server setup, you then reach out to Cognito using getOpenIdTokenForDeveloperIdentity. In my setup, I use authenticated users because I expect them to come back and want to be able to continue the associations, so that is why I send in a UserID in req.body.UserIDFromAngularApp.
This is my function using express.router().
.post(function(req, res) {
if(req.body.UserIDFromAngularApp) {
var cognitoidentity = new AWS.CognitoIdentity();
var params = {
IdentityPoolId: 'your_cognito_identity_pool_id',
Logins: {
'your_developer_authenticated_identity_name': req.body.UserIDFromAngularApp
}
};
cognitoidentity.getOpenIdTokenForDeveloperIdentity(params, function(err, data) {
if (err) { console.log(err, err.stack); res.json({failure: 'Connection failure'}); }
else {
console.log(data); // so you can see your result server side
res.json(data); // send it back
}
});
}
else { res.json({failure: 'Connection failure'}); }
});
If all goes well, that will return an OpenID Token back to you. You can then return that back to your Angular application.
POST from Angular, Collect from Promise
At the very least you need to post to your new node server and then collect the OpenID token out of the promise. Using this pattern, that will be found in data.Token.
It sounds like from there you may just need to pass that token on to your plugin/tool.
In case you need to handle authentication further, I have included code to handle the WebIdentityCredentials.
angular.module('yourApp').factory('AWSmaker', ['$http', function($http) {
return {
reachCognito: function(authData) {
$http.post('http://localhost:8888/simpleapi/aws', {
'UserIDFromAngularApp': authData.uid,
})
.success(function(data, status, headers, config) {
if(!data.failure) {
var params = {
RoleArn: your_role_arn_setup_by_cognito,
WebIdentityToken: data.Token
};
AWS.config.credentials = new AWS.WebIdentityCredentials(params, function(err) {
console.log(err, err.stack);
});
}
});
}
}]);
This should get you on your way. Let me know if I can help further.
Each OAuth provider has a slightly unique way of handling things, and so the attributes available in your Firebase authenticated token vary slightly based on provider. For example, when utilizing Facebook, the Facebook auth token is stored at facebook.accessToken in the returned user object:
var ref = new Firebase(URL);
ref.authWithOAuthPopup("facebook", function(error, authData) {
if (authData) {
// the access token for Facebook
console.log(authData.facebook.accessToken);
}
}, {
scope: "email" // the permissions requested
});
All of this is covered in the User Authentication section of the Web Guide.