send Oauth 2.0 authentication request with Google Apps Script - javascript

I'm using apps script to create an interaction between a spreadsheet and a website using its API.
I must first authenticate with Oauth 2.0, here is the documentation:
Authentication - OAuth 2.0
Authentication is required before any other API call.
POST
/oauth/token
Body :
grant_type=client_credentials
Header :
Champ
Type
Description
Authorization
String
Autorization method "Basic" followed by your public_key and private_key in your settings > developer > API combined into a string "public_key:private_key" and encoded using Base64
Content-Type
String
Must be : application/x-www-form-urlencoded
Header (example) :
```
Authorization: Basic dGVzdGNsaWVudDp0ZXN0cGFzcw==
Content-Type: application/x-www-form-urlencoded
```
I'm completely new to API requests, and I don't understand how to format the request, I found this post:
Send POST request in Google Apps Script with Headers and Body
And as I understand, application/x-www-form-urlencoded is by default with UrlFetchApp, so I tried:
function authentication() {
const ENDPOINT = 'api url'
const CLIENT_ID = 'public key'
const CLIENT_SECRET = 'secret key'
const TOKEN_URL = ENDPOINT + '/oauth/token'
const HEADERS = {
'Authorization' : 'Basic ' + CLIENT_ID + ':' + CLIENT_SECRET
}
const BODY = 'grant_type=client_credentials'
const OPTIONS = {
'method' : 'post',
'headers' : HEADERS
}
let response = UrlFetchApp.fetch(TOKEN_URL + "?" + BODY,OPTIONS)
Logger.log(response.getContentText());
}
But I get a 404 error and know an unknown error.
I guess I'm doing something wrong at least with the body but I don't understand how to format properly the request.
Can someone help me?
Thanks

I would suggest that you refer to this documentation, Apps script has a library that allows you to use Oauth2 you can find it here.
Here is an example:
function accessProtectedResource(url, method_opt, headers_opt) {
var service = getOAuthService();
var maybeAuthorized = service.hasAccess();
if (maybeAuthorized) {
var accessToken = service.getAccessToken();
var method = method_opt || 'get';
var headers = headers_opt || {};
headers['Authorization'] =
Utilities.formatString('Bearer %s', accessToken);
var resp = UrlFetchApp.fetch(url, {
'headers': headers,
'method' : method,
'muteHttpExceptions': true, // Prevents thrown HTTP exceptions.
});
var code = resp.getResponseCode();
if (code >= 200 && code < 300) {
return resp.getContentText("utf-8"); // Success
} else if (code == 401 || code == 403) {
// Not fully authorized for this action.
maybeAuthorized = false;
} else {
// Handle other response codes by logging them and throwing an
// exception.
console.error("Backend server error (%s): %s", code.toString(),
resp.getContentText("utf-8"));
throw ("Backend server error: " + code);
}
}
if (!maybeAuthorized) {
// Invoke the authorization flow using the default authorization
// prompt card.
CardService.newAuthorizationException()
.setAuthorizationUrl(service.getAuthorizationUrl())
.setResourceDisplayName("Display name to show to the user")
.throwException();
}
}
function getOAuthService() {
return OAuth2.createService('SERVICE_NAME')
.setAuthorizationBaseUrl('SERVICE_AUTH_URL')
.setTokenUrl('SERVICE_AUTH_TOKEN_URL')
.setClientId('CLIENT_ID')
.setClientSecret('CLIENT_SECRET')
.setScope('SERVICE_SCOPE_REQUESTS')
.setCallbackFunction('authCallback')
.setCache(CacheService.getUserCache())
.setPropertyStore(PropertiesService.getUserProperties());
}
function authCallback(callbackRequest) {
var authorized = getOAuthService().handleCallback(callbackRequest);
if (authorized) {
return HtmlService.createHtmlOutput(
'Success! <script>setTimeout(function() { top.window.close() }, 1);</script>');
} else {
return HtmlService.createHtmlOutput('Denied');
}
}
/**
* Unauthorizes the non-Google service. This is useful for OAuth
* development/testing. Run this method (Run > resetOAuth in the script
* editor) to reset OAuth to re-prompt the user for OAuth.
*/
function resetOAuth() {
getOAuthService().reset();
}

Related

Translating Axios + crypto request to postman + cryptojs

I would like to make a request to an API using Postman. There is an example in the API documentation for Javascript using axios and crypto.
The documentation says the following
Signing a request
The value of the X-App-Access-Sig is generated by a sha256 HMAC algorithm using a secret key (provided upon App Token generation) on the bytes obtained by concatenating the following information:
A timestamp (value of the X-App-Access-Ts header) taken as a string
An HTTP method name in upper-case, e.g. GET or POST
URI of the request without a host name, starting with a slash and including all query parameters, e.g. /resources/applicants/123?fields=info
Request body, taken exactly as it will be sent. If there is no request body, e.g., for GET requests, don't include it.
Example:
const axios = require('axios');
const crypto = require('crypto');
var config = {};
config.baseURL= BASE_URL;
axios.interceptors.request.use(createSignature, function (error) {
return Promise.reject(error);
})
function createSignature(config) {
console.log('Creating a signature for the request...');
var ts = Math.floor(Date.now() / 1000);
const signature = crypto.createHmac('sha256', SECRET_KEY);
signature.update(ts + config.method.toUpperCase() + config.url);
if (config.data instanceof FormData) {
signature.update (config.data.getBuffer());
} else if (config.data) {
signature.update (config.data);
}
config.headers['X-App-Access-Ts'] = ts;
config.headers['X-App-Access-Sig'] = signature.digest('hex');
return config;
}
My attempt to implement this as a postman Pre-request Script:
var CryptoJs = require("crypto-js")
console.log('Creating a signature for the request...');
const endpoint = pm.request.url.toString().split("{{BASE_URL}}").pop()
var ts = Math.floor(Date.now() / 1000);
const signature = CryptoJs.HmacSHA256(ts + pm.request.method.toUpperCase() + endpoint + pm.request.body, "{{SECRET_KEY}}")
pm.request.headers.add({
key: 'X-App-Access-Ts',
value: ts
})
pm.request.headers.add({
key: 'X-App-Access-Sig',
value: signature.toString(CryptoJs.enc.Hex)
})
console.log(pm.request)
But I get the error:
{
"description": "Request signature mismatch",
"code": 401,
"correlationId": "req-53db2067-b0ec-4ce8-b333-387b0b20a955",
"errorCode": 4003,
"errorName": "app-token-signature mismatch"
}

Different headers used in Axios patch

I spent an hour looking in the Chrome console and I cannot see where this bug comes from.
I am finishing an update of OAuth implementation in my Vue app.
The story begins when socialLink.js finds out that a new user must be created. Vue component Vue-authentication depends on the presence of access_token in a response so I return some dummy text:
return api.sendResponse(res, { email, name, socialId, access_token: 'abcd' });
The library stores this value in localStorage:
After a redirect, the SignUp.vue is rendered and I complete the form. The first communication with the server is a Vuex call to create a new user:
response = await this.$store.dispatch('CREATE_USER_PROFILE', payload);
Which returns a real short lived JWT token:
const token = auth.createToken(userId, nickname, new Date(), null, false, '1m');
return api.sendCreated(res, api.createResponse(token));
Which I store in the Vue page afterwards:
const { data } = response;
const token = data.data;
if (token === undefined) {
this.error = this.$t('sign-up.something-went-wrong');
return false;
}
I checked that the token contains what the server returned:
Request URL: https://beta.mezinamiridici.cz/api/v1/users
Request Method: POST
Status Code: 201 Created
{"success":true,"data":"eyJhbGciOiJIUzI1NiIs...Tl8JFw2HZ3VMXJk"}
Then I call another Vuex method and pass the current JWT token:
await this.$store.dispatch('UPDATE_USER_PROFILE', {
I checked in the Vuex devtools that there really is the correct JWT token. I then pass it further to api.js.
Here I create an Axios configuration holding an Authorization header:
function getAuthHeader(context, jwt = undefined, upload) {
const config = { headers: { } };
if (jwt || (context && context.rootState.users.userToken)) {
config.headers.Authorization = `bearer ${jwt || context.rootState.users.userToken}`;
}
Again, I checked that the correct JWT token is used there.
Finally, I pass all data to Axios:
function patch(endpoint, url, body, context, jwt) {
const headers = getAuthHeader(context, jwt);
console.log(headers);
if (endpoint === 'BFF') {
return axios.patch(`${VUE_APP_BFF_ENDPOINT}${url}`, body, headers);
} else {
return axios.patch(`${VUE_APP_API_ENDPOINT}${url}`, body, headers);
}
}
Which I log and can confirm the correct JWT is still there:
bearer eyJhbGciOiJIUzI1N....8JFw2HZ3VMXJk
There is nothing that could change the header now to abcd, but, the 'Network' tab shows it:
And the server fails with a parse error.
Has anybody got an idea why Axios uses the Authorization header with a different value than I pass it?
Ok, mystery solved. vue-authenticate is the reason, because, it creates Axios interceptors and handles the Authorization header itself.
vue-authenticate.common.js:
var defaultOptions = {
bindRequestInterceptor: function ($auth) {
var tokenHeader = $auth.options.tokenHeader;
$auth.$http.interceptors.request.use(function (config) {
if ($auth.isAuthenticated()) {
config.headers[tokenHeader] = [
$auth.options.tokenType, $auth.getToken()
].join(' ');
} else {
delete config.headers[tokenHeader];
}
return config
});
},
My code is more complex and it supports internal accounts with email/password so this code is breaking mine. The interceptor must be present and be a function, so the solution was:
Vue.use(VueAuthenticate, {
tokenName: 'jwt',
baseUrl: process.env.VUE_APP_API_ENDPOINT,
storageType: 'localStorage',
bindRequestInterceptor() {},
bindResponseInterceptor() {},
providers: {
facebook: {
clientId: process.env.VUE_APP_FACEBOOK_CLIENT_ID,
redirectUri: process.env.VUE_APP_FACEBOOK_REDIRECT_URI,
},

Bad Request when trying to get an access token

I'm trying to use the Nest Device API, however I am unable to get an Access Token as it throws
{
"error": "invalid_grant",
"error_description": "Bad Request"
}
I've downloaded my credentials.json from GCP and I open a new tab with the AUTH_URL below:
const credentials = require('../../../credentials.json');
const PROJECT_ID = <NEST DEVICE API PROJECT ID>;
const REDIRECT_URL = 'http://localhost:3000/connect/callback';
const AUTH_URL =
`https://nestservices.google.com/partnerconnections/${PROJECT_ID}/auth?` +
`redirect_uri=${REDIRECT_URL}&access_type=offline&prompt=consent&client_id=${credentials.web.client_id}&` +
`response_type=code&scope=https://www.googleapis.com/auth/sdm.service`;
From that, I have my callback page that gets the authcode.
const credentials = require('../../../credentials.json');
const { code } = router.query; // Auth Code
try {
const url =
`https://www.googleapis.com/oauth2/v4/token?client_id=${credentials.web.client_id}` +
`&client_secret=${credentials.web.client_secret}&code=${code}&grant_type=authorization_code&` +
`redirect_uri=${REDIRECT_URL}`;
const response = await fetch(url, {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
});
console.log(response);
} catch (e) {
console.error(e);
}
This is where the API returns the above error. I have also tried taking this URL and doing curl -L -X POST <url> however I get exactly the same results.
Any ideas?

Returning response header OnAuthenticationFailed

I am trying to return an altered header if the token a user sends up is expired so that I can resend up my refresh token if it is expired.
I am using .NET Core 2.2 with "In-Process" hosting incase that matters.
Here is my ConfigureServices method from my Startup.cs.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "bearer";
options.DefaultChallengeScheme = "bearer";
}).AddJwtBearer("bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Configuration["serverSigningPassword"])),
ValidateLifetime = true,
ClockSkew = System.TimeSpan.Zero //the default for this setting is 5 minutes
};
options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return System.Threading.Tasks.Task.CompletedTask;
}
};
});
And then when I try to fetch on an "authorize" endpoint from javascript with the following.
async function fetchWithCredentials(url, options) {
options.headers['Authorization'] = 'Bearer ' + jwtToken;
var response = await fetch(url, options);
if (response.ok) { //all is good, return the response
return response;
}
console.log(response.headers) //nothing in this array
// it will never do this "if" statement because there are no headers
if (response.status === 401 && response.headers.has('Token-Expired')) {
// refresh the token
return await fetchWithCredentials(url, options); //repeat the original request
} else { //status is not 401 and/or there's no Token-Expired header
return response;
}
}
This image is from hovering over the header. It certainly hits my breakpoint (for the context.Response.Headers.Add() and I can see the count = 1 (which is the "Token-Expired" when I examine it).
Finally, here is a screenshot from Postman after a failed request so the response is sending, but not being received in my JS.
Any ideas as to why my header is not sticking to my response in the javascript?
There is a restriction to access response headers when you are using Fetch API over CORS. Due to this restriction, you can access only following standard headers:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
Reference : https://stackoverflow.com/a/44816592/5751404
So one way to access your custom header in client is to add the header access-control-expose-headers to response, with the comma-separated headers:
services.AddCors(o => o.AddPolicy("MyPolicy", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.WithExposedHeaders("Token-Expired"); ;
}));
In Configure:
app.UseCors("MyPolicy");
So that you can access the custom header from client using Fetch API over CORS .
Here I describe for both -
1. Token expire and get refresh token
2. Only for unauthorized request.
async function fetchWithCredentials(url, options) {
options.headers['Authorization'] = 'Bearer ' + jwtToken;
var response = await fetch(url, options);//this is a function for get a response. I didn't explain it here. Hope you understand.
if (response.ok) {
return response;
}
let flag:boolean=false; //set flag for executing one if statement at a time.
if (response.status == 401 && response.headers.has('Token-Expired')) {
// refresh the token
flag=true; //set flag true.
//write something as per your requirement.
}
if (response.status == 401 && flag==false) {
**// Only for unauthorized request. You can use this for your problem.**
//write something as per your requirement.
}
}
And most important thing is, You have to use below code in startup.cs.
services.AddCors(context => context.AddPolicy("CustomPolicy", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.WithExposedHeaders("Token-Expired"); ;
}));
In Configure:
app.UseCors("CustomPolicy");
and use below code as it is.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "bearer";
options.DefaultChallengeScheme = "bearer";
}).AddJwtBearer("bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Configuration["serverSigningPassword"])),
ValidateLifetime = true,
ClockSkew = System.TimeSpan.Zero //the default for this setting is 5 minutes
};
options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return System.Threading.Tasks.Task.CompletedTask;
}
};
});
Now, you'll get response on client side.
Hope you'll find your solution. Please let me know for any doubt.

How to run the triggers in Google app script without reauth each time

Hello I have a script which is not a web app but it is an API based one it fetch data from json and sent them to blogger as post , and I am running this function with time based trigger , but after some run I need to open the auth url and get verification .. How can I avoid that and run the triggers continues
This is the auth code
var service = getBloggerService_();
if (service.hasAccess()) {
var api ='https://www.googleapis.com/blogger/v3/blogs/' + blogId + '/posts/';
var headers = {
'Authorization': 'Bearer ' + getBloggerService_().getAccessToken()
};
var options = {
'headers': headers,
'method' : 'post',
'contentType': 'application/json',
'payload': body,
'muteHttpExceptions': false
};
try {
var response = UrlFetchApp.fetch(api, options);
var responseCode = response.getResponseCode();
Logger.log(responseCode);
var json = JSON.parse(response.getContentText());
Logger.log(json);
}
catch(err) {
Logger.log(err); // error with url fetch call
}
}
else {
var authorizationUrll=service.getAuthorizationUrl();
Logger.log('Open the following URL and
re-run the script: %s',
authorizationUrl);
}
}
Solution:
Add the Blogger scope in appsscript.json manually.
{
...
"oauthScopes": [
"https://www.googleapis.com/auth/blogger",
"https://www.googleapis.com/auth/script.external_request"
],
...
}
You can then use:
var headers = {
'Authorization': 'Bearer ' + ScriptApp.getOAuthToken(); //contains Blogger scope always
};
You don't need to use service or any other library.
References:
Setting Explicit scopes
ScriptApp#getOAuthToken

Categories

Resources