Limit access to Google App Engine Endpoints - google-app-engine

What I'd like to do is limit access to my service endpoints to only the web app deployed on Google App Engine, so, access from only some-app-id.appspot.com and the domain mapped to it, mymappeddomain.com. I also do not want a user to login in order for the APIs to work. So, basically I want the API to only be accessible to the JavaScript code hosted in the same Google App Engine app instance, without having users login to use the service. Ideally also my API is not viewable in the API Explorer.
I've researched this a bit, and found a few articles (sample code, walk through), but haven't had success applying them. Also, it seems that all the solutions seem to still require a user to login. I've created a web application client id using my Google Developer Console.
The endpoint is written in Go and deployed to Google App Engine. It's similar to this:
package my
import ( ... "github.com/GoogleCloudPlatform/go-endpoints/endpoints" ... )
func (ms *MyService) ListData(c endpoints.Context, r *MyRequest) (*MyResponse, error) {
_, err := endpoints.CurrentBearerTokenUser(c,
[]string{ endpoints.EmailScope },
[]string{ "some-long-id-matching-what-is-used-in-javascript.apps.googleusercontent.com" })
if err != nil {
log.Printf("auth failed")
return nil, err
}
...
My JavaScript looks like:
var app = angular.module('myApp', ['ngRoute', 'angular-google-gapi']);
app.run(['$window', 'GAuth', 'GApi', function ($window, GAuth, GApi) {
var CLIENT = 'some-long-id-matching-what-is-used-in-go.apps.googleusercontent.com';
var BASE;
if($window.location.hostname === 'localhost') {
BASE = '//localhost:8080/_ah/api';
} else {
BASE = 'https://my-app-id.appspot.com/_ah/api';
}
GApi.load('myservice', 'v1', BASE);
GAuth.setClient(CLIENT);
GAuth.setScopes('https://www.googleapis.com/auth/userinfo.email');
GAuth.checkAuth().then(function () {
console.log('authenticated');
}, function () {
console.log('authentication issue');
});
}]);
When I run this app from a browser, I see the following errors in the JavaScript console:
angular-google-gapi.min.js:7 myservice v1 api loaded
https://content.googleapis.com/oauth2/v2/userinfo Failed to load resource: the server responded with a status of 401 (OK)
app.js:24 authentication issue
I'd love suggestions on how to make this work. Thank you in advance.

GAuth.checkAuth() it's just a method to test if the user is already sign-in your application and autoconnect the user if it's possible. To login for the first time, you must use the method GAuth.login(). See example there : https://github.com/maximepvrt/angular-google-gapi/tree/gh-pages

I think what might work for you is to simply set an 'Access-Control-Allow-Origin' header in each of your endpoint handlers to block cross-origin requests [1]. This is the normal idiom for locking down some resource to Javascript clients by domain.
[1] http://enable-cors.org/server_appengine.html

Related

Service to service requests on App Engine with IAP

I'm using Google App Engine to host a couple of services (a NextJS SSR service and a backend API built on Express). I've setup my dispatch.yaml file to route /api/* requests to my API service and all other requests get routed to the default (NextJS) service.
dispatch:
- url: '*/api/*'
service: api
The problem: I've also turned on Identity-Aware Proxy for App Engine. When I try to make a GET request from my NextJS service to my API (server-side, via getServerSideProps) it triggers the IAP sign-in page again instead of hitting my API. I've tried out a few ideas to resolve this:
Forwarding all cookies in the API request
Setting the X-Requested-With header as mentioned here
Giving IAP-secured Web App User permissions to my App Engine default service account
But nothing seems to work. I've confirmed that turning off IAP for App Engine allows everything to function as expected. Any requests to the API from the frontend also work as expected. Is there a solution I'm missing or a workaround for this?
You need to perform a service to service call. That's no so simple and you have not really example for that. Anyway I tested (in Go) and it worked.
Firstly, based your development on the Cloud Run Service to Service documentation page.
You will have this piece of code in NodeJS sorry, I'm not a NodeJS developer and far least a NexJS developer, you will have to adapt
// Make sure to `npm install --save request-promise` or add the dependency to your package.json
const request = require('request-promise');
const receivingServiceURL = ...
// Set up metadata server request
// See https://cloud.google.com/compute/docs/instances/verifying-instance-identity#request_signature
const metadataServerTokenURL = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=';
const tokenRequestOptions = {
uri: metadataServerTokenURL + receivingServiceURL,
headers: {
'Metadata-Flavor': 'Google'
}
};
// Fetch the token, then provide the token in the request to the receiving service
request(tokenRequestOptions)
.then((token) => {
return request(receivingServiceURL).auth(null, null, true, token)
})
.then((response) => {
res.status(200).send(response);
})
.catch((error) => {
res.status(400).send(error);
});
This example won't work because you need the correct audience. Here, the variable is receivingServiceURL. It's correct for Cloud Run (and Cloud Functions) but not for App Engine behind IAP. You need to use the Client ID of the OAuth2 credential named IAP-App-Engine-app
Ok, hard to understand what I'm talking about. So, go to the console, API & Services -> Creentials. From there, you have a OAuth2 Client ID section. copy the Client ID column of the line IAP-App-Engine-app, like that
Final point, be sure that your App Engine default service account has the authorization to access to IAP. And add it as IAP-secured Web App User. The service account has this format <PROJECT_ID>#appspot.gserviceaccount.com
Not really clear also. So, go to the IAP page (Security -> Identity Aware Proxy), click on the check box in front of App Engine and go the right side of the page, in the permission panel
In the same time, I can explain how to deactivate IAP on a specific service (as proposed by NoCommandLine). Just a remark: deactivate security when you have trouble with it is never a good idea!!
Technically, you can't deactive IAP on a service. But you can grant allUsers as IAP-secured Web App User on a specific service (instead of clicking on the checkbox of App Engine, click on the checkbox of a specific service). And like that, even with IAP you authorized all users to access to your service. it's an activation without checks in fact.

Go client to access GAE login required apps

I wanted to authenticate myself (Google Account) using a golang client against protected apps on Google App Engine where login: required or login: admin is specified in app.yaml.
First I wrote a simple OAuth2 offline access client but it didn't work at all - the server just redirects clients to Google Account's sign in page. I've tried with various Google API scopes and currently no luck.
package main
import (
"context"
"fmt"
"io"
"log"
"os"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
const (
AppURL = "https://login-requried-app.appspot.com"
AuthClientID = "....."
AuthClientSecret = "....."
AuthRedirectURL = "urn:ietf:wg:oauth:2.0:oob"
AuthScope = "https://www.googleapis.com/auth/cloud-platform"
)
func main() {
ctx := context.Background()
conf := &oauth2.Config{
ClientID: AuthClientID,
ClientSecret: AuthClientSecret,
Endpoint: google.Endpoint,
RedirectURL: AuthRedirectURL,
Scopes: []string{AuthScope},
}
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
fmt.Printf("Visit the URL for the auth dialog: %v\n", url)
fmt.Printf("Enter authentication code: ")
var code string
if _, err := fmt.Scan(&code); err != nil {
log.Fatal(err)
}
tok, err := conf.Exchange(ctx, code)
if err != nil {
log.Fatal(err)
}
client := conf.Client(ctx, tok)
res, err := client.Get(AppURL)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
log.Println(res.Status)
io.Copy(os.Stdout, res.Body)
}
I'm looking for the details of GAE's user authentication modes used in such apps to write a non web browser client. I feel it's something different than standard OAuth2 authentication/authorization after reading App Engine Users API docs and code that is receiving user information via HTTP headers like X-AppEngine-User-Email.
Users Go API Overview
google.golang.org/appengine/user package document
user.Current() implementation (it's for flex env but almost the same in standard env)
UPDATE: After some research it looks like the GAE frontend uses SACSID cookie for tracking authenticated sessions, which isn't related to the OAuth2 standard. Indeed as stated in Users Go API document:
Note that using OAuth to identify your users is completely orthogonal to the standard user authentication modes. For example, pages marked with login: required or login: admin will refuse to load if the user is only authenticated via OAuth.
Is there any supported way for a CLI application to acquire SACSID properly authorized by user's consent?
Related questions:
Access an App Engine app from command line using OAuth2?
On Google App Engine, can I relate a Google OAuth 2 Token and a SACSID token I got using Android's AccountManager?
Given the situation you explain here, I suggest using a remote API. This way you can access App Engine services from your Go app.
First you have to configure your app.yaml file by adding the following:
- url: /_ah/remote_api
script: _go_app
You also have to add the following import to your .go source file:
import _ "google.golang.org/appengine/remote_api"
When this is done, deploy your updated app to App Engine:
gcloud app deploy app.yaml
The website I included here includes an example on how to use the remote API. You can try it and adapt your code if this works for you.

ADAL.JS with Mobile App

I'm trying to integrate some Oracle delivered Mobile Application Framework Apps (MAF) mobile apps with Azure AD authentication. I have tried the Java approach, which apparently doesn't work in my case.
So I decided to try using a Javascript login page option using ADAL.JS. Since MAF creates cross-platform compatible code by transpiling to HTML 5/Javascript/Cordova, I reckoned I could make the JS option work without resorting to having multiple SDK specific solutions like ADAL-Android or ADAL-IOS. Since I can wrap it all in an HTML page as I can use the OAUTH implicit flow option that ADAL.JS requires. I have the ADAL.JS part working from my PC using this example with a local Node/Webpack dev server for the redirect URI. (Note, just like that example, I'd prefer to use the strict adal.js option and avoid any angular-js stuff). However, I'm running into an issue when deployed on the Android mobile device. It appears to be due to the reply URI. After being prompted for Azure credentials and supplying those, the following error is produced.
AADSTS50011: Reply address 'file:///data/user/0/com.company.app/storage/assets/FARs/ViewController/public_html/SignOn/login.html' has an invalid scheme.
I found that when deploying to a mobile device the Azure registered app must be set to type "Native" instead of "Web/API" which I have done. And according to an MSFT example (which I cannot include since I don't have enough rep to include more than two links) the redirect URI must be set to "https://login.microsoftonline.com/common/oauth2/nativeclient". But I still get the same error.
UPDATE since #FeiXue Reply
I'm using the original endpoint not 2.0. When I set the redirectURI as such:
redirectURI=https://login.microsoftonline.com/common/oauth2/nativeclient
The browser returns this in the address bar and remains there on a blank screen and does not issue a token. It does this both on the PC browser and mobile browser.
http://login.microsoftonline.com/common/oauth2/nativeclient#id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImEzUU4wQlpTN3M0bk4tQmRyamJGMFlfTGRNTSIsImtpZCI6ImEzUU4wQlpTN3M0bk4tQmRyamJGMFlfTGRNTSJ9.(shortened for brevity)&state=e1ce94fb-6310-4dec-9e8b-053727ceb9b8&session_state=1beafa4d-af55-415b-85d5-83e8b4035594
However, for the exact same code, on the PC when I set the redirectURI as such it returns an access token:
redirectURI=https://localhost:8443 <-- port to my local node server
I've also tried it with a redirectURI of urn:ietf:wg:oauth:2.0:oob, but that does not work either.
Code
<!DOCTYPE html>
<html>
<head>
<title>Authenticate User with ADAL JS</title>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/adal.js"></script>
<script type="text/javascript">
$(document).ready(function() {
"use strict";
var variables = {
azureAD: "mytenant.onmicrosoft.com",
clientId: "cc8ed7e0-56e9-45c9-b01e-xxxxxxxxxx"
}
window.config = {
tenant: variables.azureAD,
clientId: variables.clientId,
postLogoutRedirectUri: window.location.origin,
redirectUri: "https://login.microsoftonline.com/common/oauth2/nativeclient",
endpoints: {
aisApiUri: "cc8ed7e0-56e9-45c9-b01e-xxxxxxxxxx"
}
//cacheLocation: "localStorage"
};
var authContext = new AuthenticationContext(config);
var isCallback = authContext.isCallback(window.location.hash);
authContext.handleWindowCallback();
if (isCallback && !authContext.getLoginError()) {
window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
}
var user = authContext.getCachedUser();
if (!user) {
authContext.login();
}
authContext.acquireToken(config.endpoints.aisApiUri, function (error, token) {
if (error || !token) {
console.log("ADAL error occurred in acquireToken: " + error);
return;
}
else {
var accessToken = "Authorization:" + " Bearer " + token;
console.log("SUCCESSFULLY FETCHED TOKEN: " + accessToken);
}
});
});
</script>
</head>
<body>
<h1>Test Login</h1>
</body>
</html>
Update
#FeiXue So I guess from what you're saying the id_token IS the access token? I think then the problem is this.
When the redirectURI="https://localhost:8443" it redirects back to my index.html after AAD login and the authContext.acquireToken() works and returns a valid token.
But when the redirectURI="https://login.microsoftonline.com/common/oauth2/nativeclient" it never redirects back from http://login.microsoftonline.com/common/oauth2/nativeclient#id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1Ni......
While it shows the id_token, it never redirects back to my index.html So I can't make a call to authContext.acquireToken() for passing it onto my web API.
From my research on this topic here is the gist on ADAL.JS and Native (Mobile) Device Support
As #fei-xue-msft mentioned, ADAL.JS is not intended for nor does it work with native/mobile devices. ADAL.JS was written with the “original” Azure endpoint in mind, not the v2.0 endpoint that provides more functionality for mobile/native devices (see more below on the two different endpoint options). There is however an experimental ADAL.JS branch you can try (uses the v2.0 endpoint), but it is not not being actively updated anymore so you are on your own. The new MSFT approach is to use the new MSAL library, which is written towards the v2.0 endpoints. However there is no MSAL-for-JS library yet but rumor is there will be one at some point. For more on the two different Azure endpoints (“original” versus “v2.0”) see the links below. The confusion over this was a source of frustration in my troubleshooting so I help this helps some others going down this track.
So if you are looking to get Azure Oauth authentication on mobile devices, first decide which Azure Endpoint you want to use (Supporting links on that below as v2.0 does have some restrictions that the original endpoint does not). You can determine what your specific endpoints for your tenant are by viewing the Metadata Doc links listed below, just substitute your tenant name or ID. You should be able to use either.
To register an application for a specific type of endpoint (original versus v2.0) use the appropriate App Registration Portal link cited below. Then, to decide what your options are for creating an Azure auth solution for native/mobile device, see the code samples for each endpoint version, and make sure the sample is for “native” else it probably won’t work on your mobile device. For example, you will not see an ADAL.JS sample for the original endpoint library options, but you will see one for Cordova (which is why #fei-xue-msft suggested that approach). For the v2.0 endpoint samples you will see the MSAL/Xamarin options, and for an Javascript option you can try something like the Hello.JS Sample.
Original Endpoint
https://login.microsoft.com/{tenant id}/oauth2/authorize
App Registration Portal: https://portal.azure.com
Code Samples: https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-code-samples#native-application-to-web-api
Native Auth Scenarios: https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-authentication-scenarios#native-application-to-web-api
OpenID Metadata Doc: https://login.microsoft.com/{tenant id}/.well-known/openid-configuration
V2.0 Endpoint
https://login.microsoftonline.com/{tenant id}/oauth2/v2.0/authorize
App Registration Portal: https://apps.dev.microsoft.com
V2.0 Endpoint Compare: https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-compare
Code Sample: https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-libraries
OpenID Metadata Doc: https://login.microsoft.com/{tenant id}/v2.0/.well-known/openid-configuration
Are you developing with Azure AD V2.0 endpoint?
If not, we are able to config the redirect URIs as we want on the portal for the native app. However as the error message indicates that the file protocol is not a a validate scheme.
In this scenario, we can use the http or https since you were developing with HTML.
And in the Azure AD V2.0 endpoint, we are not able to set the redirect_Uri for the native app at present. We can use urn:ietf:wg:oauth:2.0:oob or https://login.microsoftonline.com/common/oauth2/nativeclient for the redirect_Uri. The first one is used for the native app for the device and the second we can use for the client which host in browser(web-view).
At last, please ensure that the redirect_uri in the request is using the correct one you register for the portal. You can also test the request on the browser to narrow down whether this issue was cause the incorrect redirect_uri in the request. And for the authorization request, you can refer links below:
Authorize access to web applications using OAuth 2.0 and Azure Active Directory
v2.0 Protocols - OAuth 2.0 Authorization Code Flow
Update(there is no href property if open the HTML from disk which cause the popup page is not closed)
AuthenticationContext.prototype._loginPopup = function (urlNavigate) {
var popupWindow = this._openPopup(urlNavigate, "login", this.CONSTANTS.POPUP_WIDTH, this.CONSTANTS.POPUP_HEIGHT);
if (popupWindow == null) {
this.warn('Popup Window is null. This can happen if you are using IE');
this._saveItem(this.CONSTANTS.STORAGE.ERROR, 'Error opening popup');
this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, 'Popup Window is null. This can happen if you are using IE');
this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, 'Popup Window is null. This can happen if you are using IE');
if (this.callback)
this.callback(this._getItem(this.CONSTANTS.STORAGE.LOGIN_ERROR), null, this._getItem(this.CONSTANTS.STORAGE.ERROR));
return;
}
if (this.config.redirectUri.indexOf('#') != -1)
var registeredRedirectUri = this.config.redirectUri.split("#")[0];
else
var registeredRedirectUri = this.config.redirectUri;
var that = this;
var pollTimer = window.setInterval(function () {
if (!popupWindow || popupWindow.closed || popupWindow.closed === undefined) {
that._loginInProgress = false;
window.clearInterval(pollTimer);
}
try {
//there is no href property if open the HTML from disk
if (popupWindow.location.href.indexOf(registeredRedirectUri) != -1) {
if (that.isAngular) {
that._onPopUpHashChanged(popupWindow.location.hash);
}
else {
that.handleWindowCallback(popupWindow.location.hash);
}
window.clearInterval(pollTimer);
that._loginInProgress = false;
that.info("Closing popup window");
popupWindow.close();
}
} catch (e) {
}
}, 20);
};
This issue is caused that when we open the HTML page from device(disk), the parent HTML page(login page) is not able to get the location of the popup page. So the parent page is not able to close that page based on the location of popup page. To workaround this issue, I suggest that you developing with azure-activedirectory-library-for-cordova or host the login page on the back end of web API.

The resource 'projects/<my project>' was not found" error when trying to get list of running instances

My goal is to test out google's orchestrator and the compute engine api by first retrieving a list of active instances. The orchestrator project including the servlet file is stored in a jar.
I'm trying to test out the java google compute engine client api. I have a cron job which calls on the orchestrator servlet. The target for the cron is a backend. From which I try to get the list of instances:
...
AppIdentityCredential credential = getCredential(computeScope);
String appName = ConfigProperties.getInstance().getGceConfigProperties().get("projectId");
try {
httpTransport = GoogleNetHttpTransport.newTrustedTransport();
final Compute compute = new Compute.Builder(
httpTransport, JSON_FACTORY, credential).setApplicationName(appName)
.build();
logger.info("================== Listing Compute Engine Instances ==================");
Compute.Instances.List instances = compute.instances().list(projectId, zone);
InstanceList list = instances.execute();
if (list.getItems() == null) {
logger.info("No instances found. Sign in to the Google APIs Console and create "
+ "an instance at: code.google.com/apis/console");
} else {
for (Instance instance : list.getItems()) {
logger.info(instance.toPrettyString());
}
}
...
There error response I get is(I omitted my project name from the response, I confirmed that I'm using the correct project id in my code):
com.google.cloud.solutions.sampleapps.orchestration.orchestrator.server.GceClientApiUtils
getInstances: com.google.api.client.googleapis.json.GoogleJsonResponseException: 404 OK
{
"code" : 404,
"errors" : [ {
"domain" : "global",
"message" : "The resource 'projects/<project-name-here>' was not found",
"reason" : "notFound"
} ],
"message" : "The resource 'projects/<project-name_here>' was not found"
}
I've also attempted this by retrieving an access token and making a RESTful call to get the list of instances and i received the exact same response. I confirmed the Url constructed was correct by comparing it against a successful query of the instances using the api explorer.
EDIT: I determined the solution to the issue with help of another post:
I was finally able to find the solution in the post Compute Engine API call fails with http 404
I needed to add my app engine service account as a team member with edit capabilities, which it does not have by default. Once I did this, the code worked as expected. I had to do this through cloud.google.com/console, as if done through appengine.google.com, a pending status will be given to the service account and will not have access.
For me i had to make sure i had authorization. Try this in the terminal gcloud auth login
Make sure you are in the right project, you can run this command on your vm to see if you are in the right project:
gcloud config list
Take a look at this post in Google Groups
Do you have access to the developers console https://console.developers.google.com?
It seems that the user account #appspot.gserviceaccount.com has not access to compute engine. In my case I see #developer.gserviceaccount.com.
If you don't have one, visit https://developers.google.com/console/help/new/#generatingoauth2 to create a new Client ID

google app script consumer with google appEngine Provider (Oauth)

I could get an Oauth conexion between GAS and GAE.
I built a Google Gadget which needs some data from my datastore application, it has 3 end points to finally get an access token.
http//[myapp].appspot.com/_ah/OAuthGetRequestToken
http//[myapp].appspot.com/_ah/OAuthAuthorizeToken
http//[myapp].appspot.com/_ah/OAuthGetAccessToken
In GAS side i have the tipical function to get an access token.
function oauthTokenFetch(){
var oAuthConfig = UrlFetchApp.addOAuthService("myAppName");
oAuthConfig.setAccessTokenUrl("https://<myApp>appspot.com/_ah/OAuthGetAccessToken");
oAuthConfig.setRequestTokenUrl("https://<myApp>.appspot.com/_ah/OAuthGetRequestToken");
oAuthConfig.setAuthorizationUrl("https://<myApp>.appspot.com/_ah/OAuthAuthorizeToken");
oAuthConfig.setConsumerKey("<myApp>.appspot.com");
oAuthConfig.setConsumerSecret("myConsumerSecret");
var requestData = {
"method": "GET",
"oAuthServiceName": "myAppName",
"oAuthUseToken": "always"
};
try {
var response2=UrlFetchApp.fetch("http://<myApp>.appspot.com/test/oauth",requestData);
Logger.log(response2.getResponseCode());
}catch(exception){
Logger.log(exception);
}
If i revoked the access token on google accounts, it isn't able for getting another one, the popup which grants the authorization don't appears ... i have to copy the google gadget and do the authorization again.
Somebody have a Solution?
There currently isn't a method to allow you to revoke or remove an OAuth token in a script. It looks like you already filed a bug/feature request on the topic, and we'll follow up there.

Resources