Gmail Add-On: Oauth not being triggered - javascript

In the code below, 'Test' button triggers a function which calls an external endpoint to load data. However, nothing happens when the button is clicked and I get a 400 error in the console area saying Invalid Argument.
Code.gs
function buildAddOn(e) {
// Create a section for that contains all user Labels.
var section = CardService.newCardSection()
var action = CardService.newAction()
.setFunctionName("testCall");
var button = CardService.newTextButton().setText('Test').setOnClickAction(action);
section.addWidget(CardService.newButtonSet().addButton(button));
// section.addWidget(CardService.newTextParagraph()
// .setText("This is a text paragraph widget. Multiple lines are allowed if needed.");)
// Build the main card after adding the section.
var card = CardService.newCardBuilder()
.setHeader(CardService.newCardHeader()
.setTitle('Authentication Card')
.setImageUrl('https://www.gstatic.com/images/icons/material/system/1x/label_googblue_48dp.png'))
.addSection(section)
.build();
return [card];
}
function testCall(){
console.log("test");
var data = accessProtectedResource('https://api.ssdf.io/v1.0/asd/4/174203','get');
return CardService.newActionResponseBuilder()
.setNotification(CardService.newNotification()
.setType(CardService.NotificationType.INFO)
.setText(data))
.build();
}
authService.gs
/**
* Attempts to access a non-Google API using a constructed service
* object.
*
* If your add-on needs access to non-Google APIs that require OAuth,
* you need to implement this method. You can use the OAuth1 and
* OAuth2 Apps Script libraries to help implement it.
*
* #param {String} url The URL to access.
* #param {String} method_opt The HTTP method. Defaults to GET.
* #param {Object} headers_opt The HTTP headers. Defaults to an empty
* object. The Authorization field is added
* to the headers in this method.
* #return {HttpResponse} the result from the UrlFetchApp.fetch() call.
*/
function accessProtectedResource(url, method_opt, headers_opt) {
var service = getOAuthService();
var maybeAuthorized = service.hasAccess();
if (maybeAuthorized) {
// A token is present, but it may be expired or invalid. Make a
// request and check the response code to be sure.
// Make the UrlFetch request and return the result.
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("Login to ....")
.throwException();
}
}
/**
* Create a new OAuth service to facilitate accessing an API.
* This example assumes there is a single service that the add-on needs to
* access. Its name is used when persisting the authorized token, so ensure
* it is unique within the scope of the property store. You must set the
* client secret and client ID, which are obtained when registering your
* add-on with the API.
*
* See the Apps Script OAuth2 Library documentation for more
* information:
* https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service
*
* #return A configured OAuth2 service object.
*/
function getOAuthService() {
return OAuth2.createService('auth')
.setAuthorizationBaseUrl('https://app.ss.io/oauth/authorize')
.setTokenUrl('https://api.ss.io/oauth/token')
.setClientId('2361c9fbc5ba4b88813a3ef')
.setClientSecret('f5d3a04f4asda30a52830e230e43727')
.setScope('1')
.setCallbackFunction('authCallback')
.setCache(CacheService.getUserCache())
.setPropertyStore(PropertiesService.getUserProperties());
}
/**
* Boilerplate code to determine if a request is authorized and returns
* a corresponding HTML message. When the user completes the OAuth2 flow
* on the service provider's website, this function is invoked from the
* service. In order for authorization to succeed you must make sure that
* the service knows how to call this function by setting the correct
* redirect URL.
*
* The redirect URL to enter is:
* https://script.google.com/macros/d/<Apps Script ID>/usercallback
*
* See the Apps Script OAuth2 Library documentation for more
* information:
* https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service
*
* #param {Object} callbackRequest The request data received from the
* callback function. Pass it to the service's
* handleCallback() method to complete the
* authorization process.
* #return {HtmlOutput} a success or denied HTML message to display to
* the user. Also sets a timer to close the window
* automatically.
*/
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();
}

All URLs in the function getOAuthService() have to be the original google-URLs of the example:
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
and
// Set the scopes to request (space-separated for Google services).
.setScope('https://www.googleapis.com/auth/drive')
It might be possible to change the latter one, but only the path, not the domain, and you've to look in the API if and how it is adjustable. Also this scope-parameter differs in your example, but I don't know if '1' could be accepted.
Your own application get's a feedback if access is granted but is not involved in validating the authentication. Therefore you also need to get the access-token, I see it in your code: var accessToken = service.getAccessToken();, in the example it looks a bit different:
function makeRequest() {
var driveService = getDriveService();
var response = UrlFetchApp.fetch('https://www.googleapis.com/drive/v2/files?maxResults=10', {
headers: {
Authorization: 'Bearer ' + driveService.getAccessToken()
}
});
// ...
}
see at the line with Authorization: 'Bearer ' + driveService.getAccessToken().
Your own server isn't (and shouldn't be) configured to handle authentication requests and therefore throws the 400 error. The API is built for use by Javascript on client-side, therefore I advise not to use it to authenticate on an own server. Nevertheless below I listed APIs for usage on your own server.
Using your own server for authentication
If you dismiss my advise to use the google-servers for authentication, then the scope of the tagged issues is getting larger, as server-configuration (apache, nginx, ...) and server-side programming-languages (PHP, Python, ...) might be involved.
You've to debug the headers then, what is sent exactly to the server and why the server can't handle it. You can debug the request and response in the browser's developer-tool (network panel) and check the error-files of the server.
The server has to acquire the google-service then by itself and do what would be done in frontend only by javascript if you'd follow the example you linked in your code.
In the example are three server-request made:
- one to authenticate
- one to get the authentication-token
- one to get the protected service
You've to keep in mind that all three steps have to be done then by your own server and that your server has to be able to answer on all three request-types.
An important question might be which of the three requests is producing the error?
You should extend your question then with the detailed problems you discover concerning communication with your server.
APIs and Examples for server-side OAuth-athentication
PHP: https://developers.google.com/api-client-library/php/auth/web-app
Python: https://developers.google.com/api-client-library/python/auth/web-app
Ruby: https://developers.google.com/api-client-library/ruby/auth/web-app
NodeJs: https://developers.google.com/apps-script/api/quickstart/nodejs
Java: https://developers.google.com/api-client-library/java/google-oauth-java-client/
Go: https://github.com/googleapis/google-api-go-client
.NET: https://developers.google.com/api-client-library/dotnet/apis/oauth2/v2

The 400 error code seems to refer to a bad link, I think you need to check your different URLs.
And particularly in the getOAuthService, you have written: "https://apP.ss.io/oauth/authorize" in the authorization base URL and "https://apI.ss.io/oauth/token" in the token URL.

Related

Asana NodeJS API - Cannot authenticate request error when using client.useOauth

I’m currently setting up and integration with Asana and want to have my users connected through the oAuth side of the library. I have successfully got the auth flow working in the sense of having the user navigated to Asana, authorize the app, then storing the access token and refresh token for use down the line. I’m aware that the access tokens expire after 1 hour and thus when making calls to the api it’s best to use the ‘client.useOauth’ side of things to pass in the access & refresh tokens allows the library to refresh it as needs be without having to do it manually which is great. But with my code implementation below I am getting an error:
/**
* Create a new asana client
*/
const client = Asana.Client.create({
clientId: asanaCredentials.id,
clientSecret: asanaCredentials.secret,
redirectUri: asanaCredentials.redirect
});
/*
* Get the access token and refresh token from the parameters passed in
*/
const { access_token, refresh_token } = data;
/*
* Pass the client access & refresh tokens
*/
client.useOauth({
access_token, refresh_token
});
/*
* Get the users workspaces
*/
const workspaces = client.workspaces.getWorkspaces().then((result) => { return result.data; });
/*
* Return the results back to the app
*/
return workspaces;
When this runs, I am getting the following error printed out:
Unhandled error Error: Cannot authenticate a request without first obtaining credentials
at
OauthAuthenticator.authenticateRequest (/workspace/node_modules/asana/lib/auth/oauth_authenticator.js:42)
at
doRequest (/workspace/node_modules/asana/lib/dispatcher.js:247)
at
(/workspace/node_modules/asana/lib/dispatcher.js:295)
at
Promise._execute (/workspace/node_modules/bluebird/js/release/debuggability.js:300)
at
Promise._resolveFromExecutor (/workspace/node_modules/bluebird/js/release/promise.js:481)
at
Promise (/workspace/node_modules/bluebird/js/release/promise.js:77)
at
Dispatcher.dispatch (/workspace/node_modules/asana/lib/dispatcher.js:244)
at
Dispatcher.get (/workspace/node_modules/asana/lib/dispatcher.js:321)
at
Function.Resource.getCollection (resource.js:36)
at
Workspaces.Resource.dispatchGetCollection (resource.js:77)
at
Workspaces.getWorkspaces (/workspace/node_modules/asana/lib/resources/gen/workspaces.js:73)
at
(/workspace/lib/src/integrations.js:132)
at
Generator.next ()
at
(/workspace/lib/src/integrations.js:8)
at
Promise ()
at
__awaiter (/workspace/lib/src/integrations.js:4)
Any help on what could be causing this would be greatly appreciated, thanks!
Fixed: My problem was the way in which I was passing in the two tokens, the client.useOauth needed to look like this:
client.useOauth({
credentials: { access_token, refresh_token }
});

Angular 5 InMemoryWebAPI - How to simulate CRUD with HTTP Verbs?

In working with the Angular docs / tutorial on the in-memory web api, I want to return some JSON values that indicate the success or failure of a request. i.e.:
{success:true, error:""}
or
{success:false, error:"Database error"}
But, the code in the example for the in-memory-data.service.ts file only has the one method: createDb().
How do update that service code to respond to a PUT/POST/DELETE request differently than a GET?
Note: In real-life / production, the backend will be PHP, and we can return these values any way we want (with the correct status codes). This question is specifically directed at making the In Memory Web API mock those responses.
Example:
Executing:
return = this.http.post(url,someJsonData,httpHeaders);
I would want return to be:
{success:'true',id:1234} with an HTTP Status code of 200.
Later, to delete that record that was just created:
url = `/foo/` + id + '/'; // url = '/foo/1234/';
this.http.delete(url);
This wouldn't really need a JSON meta data response. An HTTP Status code of 200 is sufficient.
How do update that service code to respond to a PUT/POST/DELETE
request differently than a GET?
The server will always respond to the requests that you have made. So if you fire an Ajax request it may be one of the known HTTP Methods (like POST, GET, PUT...). Afterwards you'll wait for the answer.
/** POST: add a new hero to the database */
addHero (hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions)
.pipe(
catchError(this.handleError('addHero', hero))
);
}
/** GET: get a new hero from the database */
getHero (heroId: number): Observable<Hero> {
return this.http.get(this.heroesUrl + '/' + heroId)
}
I found the answer I was looking for: It's HttpInterceptors.
This blog post and the corresponding github repo demonstrate exactly how to simulate CRUD operations in Angular 2/5 without having to setup a testing server.

How can I get the raw request body in a Google Cloud Function?

I need the raw request body to be able to SHA-1 digest it to validate the Facebook webhook X-Hub-Signature header that's passed along with the request to my Firebase Function (running on Google Cloud Functions).
The problem is that in cases like this (with a Content-Type: application/json header) GCF automatically parses the body using bodyParser.json() which consumes the data from the stream (meaning it cannot be consumed again down the Express middleware chain) and only provides the parsed javascript object as req.body. The raw request buffer is discarded.
I have tried to provide an Express app to functions.https.onRequest(), but that seems to be run as a child app or something with the request body already being parsed, just like when you pass a plain request-response callback to onRequest().
Is there any way to disable GCF from parsing the body for me? Or could I somehow specify my own verify callback to bodyParser.json()? Or is there some other way?
PS: I first contacted Firebase support about this a week ago, but for lack of a response there I'm trying it here now.
Now you can get the raw body from req.rawBody. It returns Buffer. See documentation for more details.
Thanks to Nobuhito Kurose for posting this in comments.
Unfortunately the default middleware currently provides no way to get the raw request body. See: Access to unparsed JSON body in HTTP Functions (#36252545).
const escapeHtml = require('escape-html');
/**
* Responds to an HTTP request using data from the request body parsed according
* to the "content-type" header.
*
* #param {Object} req Cloud Function request context.
* #param {Object} res Cloud Function response context.
*/
exports.helloContent = (req, res) => {
let name;
switch (req.get('content-type')) {
// '{"name":"John"}'
case 'application/json':
({name} = req.body);
break;
// 'John', stored in a Buffer
case 'application/octet-stream':
name = req.body.toString(); // Convert buffer to a string
break;
// 'John'
case 'text/plain':
name = req.body;
break;
// 'name=John' in the body of a POST request (not the URL)
case 'application/x-www-form-urlencoded':
({name} = req.body);
break;
}
res.status(200).send(`Hello ${escapeHtml(name || 'World')}!`);
};

Javascript using native browser authentication

I am currently developing a web-application using Angular2 with TypeScript (if that matters).
This application has to communicate with a webserver, which is asking for a digest authentication.
Until now i was using the native login prompt of the browsers, which is automatically showing, when the server returns a "401 unauthorized". The browser only asks for authentication once and automatically uses this username and password for future requests. So I don't have to take care about the authentication, the browser does everything for me.
Unfortunately now i have to create a custom login screen, as i have to implement some default actions, such as "register" or "reset passwort", which are ususally accessible from that screen.
As digest authentication is quite complex and the browser would allready do all the complex things for me I would like to continue using the browsers functionality, but without using it's login prompt.
So is it possible to use the browsers authentication functionality?
If it is possible, how can I set the username and the password it should use?
EDIT:
As someone wanted to close this question as "to broad", i'll try to add some more detail.
The web-application gets data from a restful webservice. This webservice requires digest authentication and responds with a 401, if you are using a wrong username or password.
As mentioned above, the browser automatically shows a login prompt, if he gets a 401 error. If you enter a valid login, the browser caches those values somewhere and automatically sets them for every future request.
Now i basicly want to replace the login prompt and programatically set the values the browser should use for the login.
I hope this helps to make the question clear.
Basically you have to write a HTTP decorator to intercept response code 401. Afterwards you add the Authentication header and replay the request.
import { Injectable } from '#angular/core';
import { Http, ConnectionBackend, RequestOptions, RequestOptionsArgs, Response, Headers } from '#angular/http';
import { Observable } from 'rxjs/Rx';
#Injectable()
export class CustomHttp extends Http {
/**
* intercept request
* #param {Observable<Response>} observable to use
* #param {string} url to request
* #returns {Observable<Response>} return value
* #private
*/
private _intercept(observable: Observable<Response>, url: string): Observable<Response> {
return observable.catch(error => {
if (error.status === 401) {
// add custom header
let headers = new Headers();
headers.append('Authentication', '<HEADER>');
// replay request with modified header
return this.get(url, new RequestOptions({
headers: headers
});
} else {
return Observable.throw(error);
}
});
};
/**
* constructor
* #param {ConnectionBackend} backend to use
* #param {RequestOptions} defaultOptions to use
* #returns {void} nothing
*/
constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) {
super(backend, defaultOptions);
};
/**
* get request
* #param {string} url to request
* #param {RequestOptionsArgs} options to use
* #returns {Observable<Response>} return value
*/
get(url: string, options?: RequestOptionsArgs): Observable<Response> {
return this._intercept(super.get(url, options), url);
};
}
What didn’t worked for me yet is determining correct responses as there was a bug filed against Angular2 (https://github.com/angular/angular/pull/9355), which was merged just a few days before.
You have to increment the request counter for each valid request following the first successful one.
Maybe somebody else can show up with a working solution.

Laravel: Redirect to login after session has expired on ajax request

If I log in to my web app, wait for the session to expire, then make an ajax request with a form in my web app I get the following error show up in the console:
Failed to load resource: the server responded with a status of 500 (Internal Server Error)
Ideally what would happen is a redirect to the login page or an error message shows up under the form that fired the ajax request (i.e. something meaningful to the user). It may be worth noting I already have the client side code to throw up errors to show an error message to the user if they make a validation error on the form.
I believe I have an idea how to check if the session is expired and return something useful to the user telling them to login but I'm unsure how I'd implement this globally. So I'm wondering is it possible to handle this issue globally from the back end in Laravel and (or) do I need to write some logic for each ajax request to catch the issue to show an error message client side?
I'm using Laravel and Javascript/JQuery. Thanks for any help!
Here's the quick solution for your case:
Controller (e.g. AuthController.php):
/**
* Check user session.
*
* #return Response
*/
public function checkSession()
{
return Response::json(['guest' => Auth::guest()]);
}
Also, probably it will be needed to add this method name to ignore by guest middleware:
$this->middleware('guest', ['except' => ['logout', 'checkSession']]);
Route:
Route::get('check-session', 'Auth\AuthController#checkSession');
Layout (JS part), only for signed in users:
#if (Auth::user())
<script>
$(function() {
setInterval(function checkSession() {
$.get('/check-session', function(data) {
// if session was expired
if (data.guest) {
// redirect to login page
// location.assign('/auth/login');
// or, may be better, just reload page
location.reload();
}
});
}, 60000); // every minute
});
</script>
#endif
use Middleware
Middleware:
<?php namespace App\Http\Middleware;
class OldMiddleware {
/**
* Run the request filter.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if(!**condition to check login**)
{
// if session is expired
return response()->json(['message' => 'Forbidden!'],403);
}
return $next($request);
}
}
Route:
Route::group(['middleware' => '\App\Http\Middleware\OldMiddleware'], function(){
//put the routes which needs authentication to complete
});
View:
$.ajax({
type: 'post',
url: {{route('someroute')}}
//etc etc
}).done(function(data){
//do if request succeeds
}).fail(function(x,y,z){
//show error message or redirect since it is failed.
});

Categories

Resources