Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth samples for Endpoints. #431

Merged
merged 3 commits into from
Aug 2, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 73 additions & 3 deletions appengine/flexible/endpoints/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ With the API key, you can use the echo client to access the API:

$ python clients/echo-client.py https://YOUR-PROJECT-ID.appspot.com YOUR-API-KEY

### Using the JWT client.
### Using the JWT client (with key file)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't with key file implied? Is there any way to do it without a key file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the three auth samples added here are to show how to authenticate with endpoints without a key file.


The JWT client demonstrates how to use service accounts to authenticate to endpoints. To use the client, you'll need both an API key (as described in the echo client section) and a service account. To create a service account:
The JWT client demonstrates how to use a service account to authenticate to endpoints with the service account's private key file. To use the client, you'll need both an API key (as described in the echo client section) and a service account. To create a service account:

1. Open the Credentials page of the API Manager in the [Cloud Console](https://console.cloud.google.com/apis/credentials).
2. Click 'Create credentials'.
Expand All @@ -76,7 +76,7 @@ Now you can use the JWT client to make requests to the API:

$ python clients/google-jwt-client.py https://YOUR-PROJECT-ID.appspot.com YOUR-API-KEY /path/to/service-account.json

### Using the ID Token client.
### Using the ID Token client (with key file)

The ID Token client demonstrates how to use user credentials to authenticate to endpoints. To use the client, you'll need both an API key (as described in the echo client section) and a OAuth2 client ID. To create a client ID:

Expand All @@ -93,3 +93,73 @@ To use the client ID for authentication:
Now you can use the client ID to make requests to the API:

$ python clients/google-id-token-client.py https://YOUR-PROJECT-ID.appspot.com YOUR-API-KEY /path/to/client-id.json

### Using the App Engine default service account client (no key file needed)

The App Engine default service account client demonstrates how to use the Google App Engine default service account to authenticate to endpoints.
We refer to the project that serves API requests as the server project. You also need to create a client project in the [Cloud Console](https://console.cloud.google.com).

To use the App Engine default service account for authentication:

1. Update the `gae_default_service_account`'s `x-issuer` and `x-jwks_uri` in `swagger.yaml` with your client project ID.
2. Redeploy your server application.
3. Update clients/service_to_service_gae_default/main.py, replace 'YOUR-CLIENT-PROJECT-ID' and 'YOUR-SERVER-PROJECT-ID' with your client project ID and your server project ID.
4. Upload your application to Google App Engine by invoking the following command under clients/service_to_service_gae_default directory.
This opens a browser window for you to sign in using your Google account. You'll be providing the project ID as the argument for -A. Use
the -V argument to specify a version name. Additional information on how to deploy an app to Google Cloud App Engine can be found [here](https://cloud.google.com/appengine/docs/python/quickstart).

appcfg.py -A <YOUR-CLIENT-PROJECT-ID> -V v1 update .
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gcloud app is GA, please use that instead.


Your client app is now deployed at https://<YOUR-CLIENT-PROJECT-ID>.appspot.com. When you access https://<YOUR-CLIENT-PROJECT-ID>.appspot.com, your client calls your server project API using
the client's service account.

### Using the service account client (no key file needed)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a common use case? Seems rather convoluted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is a common use case. Every project has a a App Engine default service account, This samples shows how to make authenticated API calls from a cloud project. In the associated bug, Mingliang mentioned that our customers have complained having to manage keys that are shipped with their applications, so we added a few samples here to show how to make authenticated calls without a key file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah but in this case you're actually using the app engine app to create a new keyfile and use it on-the-fly. This seems like a pretty convoluted and advanced use case. Have we had users ask explicitly for this use case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Mingliang, the main complain from customers is that they need to manage key rotation. In this case, the key is not managed by the users but the cloud itself, which solves the issue.


The service account client demonstrates how to use a non-default service account to authenticate to endpoints.
We refer to the project that serves API requests as the server project. You also need to create a client project in the [Cloud Console](https://console.cloud.google.com).

In the example, we use Google Cloud Identity and Access Management (IAM) API to create a JSON Web Token (JWT) for a service account, and use it to call an Endpoints API.

To use the client, you will need to enable "Service Account Actor" role for App Engine default service account:
1. Go to [IAM page] of your client project (https://console.cloud.google.com/iam-admin/iam).
2. For App Engine default service account, from “Role(s)” drop-down menu, select “Project”-“Service Account Actor”, and Save.

You also need to install Google API python library because the client code (main.py) uses googleapiclient,
which is a python library that needs to be uploaded to App Engine with your application code. After you run "pip install -t lib -r requirements",
Google API python client library should have already been installed under 'lib' directory. Additional information can be found
[here](https://cloud.google.com/appengine/docs/python/tools/using-libraries-python-27#requesting_a_library).

To use the client for authentication:
1. Update the `google_service_account`'s `x-issuer` and `x-jwks_uri` in `swagger.yaml` with your service account email.
2. Redeploy your server application.
3. Update clients/service_to_service_non_default/main.py, replace 'YOUR-SERVICE-ACCOUNT-EMAIL', 'YOUR-SERVER-PROJECT-ID' and 'YOUR-CLIENT-PROJECT-ID'
with your service account email, your server project ID, and your client project ID.
4. Upload your application to Google App Engine by invoking the following command under clients/service_to_service_non_default directory.
This opens a browser window for you to sign in using your Google account. You'll be providing the project ID as the argument for -A. Use
the -V argument to specify a version name. Additional information on how to deploy an app to Google Cloud App Engine can be found [here](https://cloud.google.com/appengine/docs/python/quickstart).

appcfg.py -A <YOUR-CLIENT-PROJECT-ID> -V v1 update .

Your client app is now deployed at https://<YOUR-CLIENT-PROJECT-ID>.appspot.com. When you access https://<YOUR-CLIENT-PROJECT-ID>.appspot.com, your client calls your server project API using
the client's service account.

### Using the ID token client (no key file needed)

This example demonstrates how to authenticate to endpoints from Google App Engine default service account using Google ID token.
In the example, we first create a JSON Web Token (JWT) using the App Engine default service account. We then request a Google
ID token using the JWT, and call an Endpoints API using the Google ID token.

We refer to the project that serves API requests as the server project. You also need to create a client project in the [Cloud Console](https://console.cloud.google.com).

To use the client for authentication:
1. Update the `google_id_token`'s audiences, replace `YOUR-SERVER-PROJECT-ID` with your server project ID.
2. Redeploy your server application.
3. Update clients/service_to_service_google_id_token/main.py, replace 'YOUR-CLIENT-PROJECT-ID' and 'YOUR-SERVER-PROJECT-ID' with your client project ID and your server project ID.
4. Upload your application to Google App Engine by invoking the following command under clients/service_to_service_google_id_token directory.
This opens a browser window for you to sign in using your Google account. You'll be providing the project ID as the argument for -A. Use
the -V argument to specify a version name. Additional information on how to deploy an app to Google Cloud App Engine can be found [here](https://cloud.google.com/appengine/docs/python/quickstart).

appcfg.py -A <YOUR-CLIENT-PROJECT-ID> -V v1 update .

Your client app is now deployed at https://<YOUR-CLIENT-PROJECT-ID>.appspot.com. When you access https://<YOUR-CLIENT-PROJECT-ID>.appspot.com, your client calls your server project API from
the client's service account using Google ID token.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /.*
script: main.app
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0(the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http: // www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Example of calling a Google Cloud Endpoint API with a JWT signed by
Google App Engine Default Service Account."""

import base64
import httplib
import json
import time

from google.appengine.api import app_identity
import webapp2

DEFAUTL_SERVICE_ACCOUNT = 'YOUR-CLIENT-PROJECT-ID@appspot.gserviceaccount.com'
HOST = "YOUR-SERVER-PROJECT-ID.appspot.com"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two newlines between constants and module-level functions.


def generate_jwt():
"""Generates a signed JSON Web Token using the Google App Engine default
service account."""
now = int(time.time())

header_json = json.dumps({
"typ": "JWT",
"alg": "RS256"})

payload_json = json.dumps({
'iat': now,
# expires after one hour.
"exp": now + 3600,
# iss is the Google App Engine default service account email.
'iss': DEFAUTL_SERVICE_ACCOUNT,
'sub': DEFAUTL_SERVICE_ACCOUNT,
# aud must match 'audience' in the security configuration in your
# swagger spec.It can be any string.
'aud': 'echo.endpoints.sample.google.com',
"email": DEFAUTL_SERVICE_ACCOUNT
})

headerAndPayload = '{}.{}'.format(base64.urlsafe_b64encode(header_json),
base64.urlsafe_b64encode(payload_json))
(key_name, signature) = app_identity.sign_blob(headerAndPayload)
signed_jwt = '{}.{}'.format(headerAndPayload,
base64.urlsafe_b64encode(signature))

return signed_jwt


def make_request(signed_jwt):
"""Makes a request to the auth info endpoint for Google JWTs."""
headers = {'Authorization': 'Bearer {}'.format(signed_jwt)}
conn = httplib.HTTPSConnection(HOST)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't forget to close the connection.

conn.request("GET", '/auth/info/googlejwt', None, headers)
res = conn.getresponse()
conn.close()
return res.read()


class MainPage(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'text/plain'
signed_jwt = generate_jwt()
res = make_request(signed_jwt)
self.response.write(res)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two newlines here, please.


app = webapp2.WSGIApplication([
('/', MainPage),
], debug=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /.*
script: main.app
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0(the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http: // www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Example of calling a Google Cloud Endpoint API from Google App Engine
Default Service Account using Google ID token."""

import base64
import httplib
import json
import time
import urllib

from google.appengine.api import app_identity
import webapp2

DEFAUTL_SERVICE_ACCOUNT = "YOUR-CLIENT-PROJECT-ID@appspot.gserviceaccount.com"
HOST = "YOUR-SERVER-PROJECT-ID.appspot.com"
TARGET_AUD = "YOUR-SERVER-PROJECT-ID@appspot.gserviceaccount.com"


def generate_jwt():
"""Generates a signed JSON Web Token using the Google App Engine default
service account."""
now = int(time.time())

header_json = json.dumps({
"typ": "JWT",
"alg": "RS256"})

payload_json = json.dumps({
"iat": now,
# expires after one hour.
"exp": now + 3600,
# iss is the Google App Engine default service account email.
"iss": DEFAUTL_SERVICE_ACCOUNT,
# scope must match 'audience' for google_id_token in the security
# configuration in your swagger spec.
"scope": TARGET_AUD,
# aud must be Google token endpoints URL.
"aud": "https://www.googleapis.com/oauth2/v4/token"
})

headerAndPayload = '{}.{}'.format(base64.urlsafe_b64encode(header_json),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: break after the opening ( so that this doesn't hang, e.g.:

headerAndPayload = '{}.{}'.format(
    base64.urlsafe_b64encode(header_json),
    base64.urlsafe_b64encode(payload_json))

base64.urlsafe_b64encode(payload_json))
(key_name, signature) = app_identity.sign_blob(headerAndPayload)
signed_jwt = '{}.{}'.format(headerAndPayload,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here (and anywhere else this occurred)

base64.urlsafe_b64encode(signature))

return signed_jwt


def get_id_token():
"""Request a Google ID token using a JWT."""
params = urllib.urlencode({
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion': generate_jwt()})
headers = {"Content-Type": "application/x-www-form-urlencoded"}
conn = httplib.HTTPSConnection("www.googleapis.com")
conn.request("POST", "/oauth2/v4/token", params, headers)
res = json.loads(conn.getresponse().read())
conn.close()
return res['id_token']


def make_request(token):
"""Makes a request to the auth info endpoint for Google ID token."""
headers = {'Authorization': 'Bearer {}'.format(token)}
conn = httplib.HTTPSConnection(HOST)
conn.request("GET", '/auth/info/googleidtoken', None, headers)
res = conn.getresponse()
conn.close()
return res.read()


class MainPage(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'text/plain'
token = get_id_token()
res = make_request(token)
self.response.write(res)


app = webapp2.WSGIApplication([
('/', MainPage),
], debug=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /.*
script: main.app
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from google.appengine.ext import vendor

vendor.add('lib')
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0(the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http: // www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Example of calling a Google Cloud Endpoint API with a JWT signed by a
Service Account."""

import base64
import httplib
import json
import time

from googleapiclient.discovery import build
import httplib2
from oauth2client.contrib.appengine import AppAssertionCredentials
import webapp2

SERVICE_ACCOUNT_EMAIL = "YOUR-SERVICE-ACCOUNT-EMAIL"
HOST = "YOUR-SERVER-PROJECT-ID.appspot.com"
SERVICE_ACCOUNT = \
"projects/YOUR-CLIENT-PROJECT-ID/serviceAccounts/YOUR-SERVICE-ACCOUNT-EMAIL"


def generate_jwt():
"""Generates a signed JSON Web Token using a service account."""
credentials = AppAssertionCredentials(
'https://www.googleapis.com/auth/iam')
http_auth = credentials.authorize(httplib2.Http())
service = build(serviceName='iam', version='v1', http=http_auth)

now = int(time.time())

header_json = json.dumps({
"typ": "JWT",
"alg": "RS256"})

payload_json = json.dumps({
'iat': now,
# expires after one hour.
"exp": now + 3600,
# iss is the service account email.
'iss': SERVICE_ACCOUNT_EMAIL,
'sub': SERVICE_ACCOUNT_EMAIL,
# aud must match 'audience' in the security configuration in your
# swagger spec.It can be any string.
'aud': 'echo.endpoints.sample.google.com',
"email": SERVICE_ACCOUNT_EMAIL
})

headerAndPayload = '{}.{}'.format(base64.urlsafe_b64encode(header_json),
base64.urlsafe_b64encode(payload_json))
slist = service.projects().serviceAccounts().signBlob(
name=SERVICE_ACCOUNT,
body={'bytesToSign': base64.b64encode(headerAndPayload)})
res = slist.execute()
signature = base64.urlsafe_b64encode(
base64.decodestring(res['signature']))
signed_jwt = '{}.{}'.format(headerAndPayload, signature)

return signed_jwt


def make_request(signed_jwt):
"""Makes a request to the auth info endpoint for Google JWTs."""
headers = {'Authorization': 'Bearer {}'.format(signed_jwt)}
conn = httplib.HTTPSConnection(HOST)
conn.request("GET", '/auth/info/googlejwt', None, headers)
res = conn.getresponse()
conn.close()
return res.read()


class MainPage(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'text/plain'
signed_jwt = generate_jwt()
res = make_request(signed_jwt)
self.response.write(res)


app = webapp2.WSGIApplication([
('/', MainPage),
], debug=True)
Loading