-
Notifications
You must be signed in to change notification settings - Fork 6.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ee148dc
commit aca4aa2
Showing
15 changed files
with
895 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Firenotes: Firebase Authentication on Google App Engine | ||
|
||
A simple note-taking application that stores users' notes in their own personal | ||
notebooks separated by a unique user ID generated by Firebase. Uses Firebase | ||
Authentication, Google App Engine, and Google Cloud Datastore. | ||
|
||
You'll need to have [Python 2.7](https://www.python.org/), the | ||
[App Engine SDK](https://cloud.google.com/appengine/downloads#Google_App_Engine_SDK_for_Python), | ||
and the [Google Cloud SDK](https://cloud.google.com/sdk/?hl=en) | ||
installed and initialized to an App Engine project before running the code in | ||
this sample. | ||
|
||
## Setup | ||
|
||
1. Clone this repo: | ||
|
||
git clone https://github.com/GoogleCloudPlatform/python-docs-samples/tree/master/appengine/standard/firebase/auth/firenotes | ||
|
||
1. Within a virtualenv, install the dependencies to the backend service: | ||
|
||
pip install -r requirements.txt -t lib | ||
pip install pycrypto | ||
|
||
Although the pycrypto library is built in to the App Engine standard | ||
environment, it will not be bundled until deployment since it is | ||
platform-dependent. Thus, the app.yaml file includes the bundled version of | ||
pycrypto at runtime, but you still need to install it manually to run the | ||
application on the App Engine local development server. | ||
|
||
1. [Add Firebase to your app.](https://firebase.google.com/docs/web/setup#add_firebase_to_your_app) | ||
1. Add your Firebase Project ID to the backend’s `app.yaml` file as an | ||
environment variable. | ||
1. Select which providers you want to enable. Delete the providers from | ||
`main.js` that you do no want to offer. Enable the providers you chose to keep | ||
in the Firebase console under **Auth** > **SIGN-IN METHOD** > | ||
**Sign-in providers**. | ||
1. In the Firebase console, under **OAuth redirect domains**, click | ||
**ADD DOMAIN** and enter the domain of your app on App Engine: | ||
[PROJECT_ID].appspot.com. Do not include "http://" before the domain name. | ||
|
||
## Run Locally | ||
1. Add the backend host URL to `main.js`: http://localhost:8081. | ||
1. Navigate to the root directory of the application and start the development | ||
server with the following command: | ||
|
||
dev_appserver.py frontend/app.yaml backend/app.yaml | ||
|
||
1. Visit [http://locahost:8080/](http://locahost:8080/) in a web browser. | ||
|
||
## Deploy | ||
1. Change the backend host URL in `main.js` to | ||
https://backend-dot-[PROJECT_ID].appspot.com. | ||
1. Deploy the application using the Cloud SDK command-line interface: | ||
|
||
gcloud app deploy backend/index.yaml frontend/app.yaml backend/app.yaml | ||
|
||
The Cloud Datastore indexes can take a while to update, so the application | ||
might not be fully functional immediately after deployment. | ||
|
||
1. View the application live at https://[PROJECT_ID].appspot.com. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
lib |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
runtime: python27 | ||
api_version: 1 | ||
threadsafe: true | ||
service: backend | ||
|
||
handlers: | ||
- url: /.* | ||
script: main.app | ||
|
||
libraries: | ||
- name: ssl | ||
version: 2.7.11 | ||
- name: pycrypto | ||
version: 2.6 | ||
|
||
env_variables: | ||
# Replace with your Firebase project ID. | ||
FIREBASE_PROJECT_ID: '<PROJECT_ID>' |
18 changes: 18 additions & 0 deletions
18
appengine/standard/firebase/firenotes/backend/appengine_config.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Copyright 2016 Google Inc. | ||
# | ||
# 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. | ||
|
||
from google.appengine.ext import vendor | ||
|
||
# Add any libraries installed in the "lib" folder. | ||
vendor.add('lib') |
121 changes: 121 additions & 0 deletions
121
appengine/standard/firebase/firenotes/backend/firebase_helper.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
# Copyright 2016 Google Inc. | ||
# | ||
# 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. | ||
|
||
import json | ||
import logging | ||
import os | ||
import ssl | ||
|
||
from Crypto.Util import asn1 | ||
from google.appengine.api import urlfetch | ||
from google.appengine.api import urlfetch_errors | ||
import jwt | ||
from jwt.contrib.algorithms.pycrypto import RSAAlgorithm | ||
import jwt.exceptions | ||
|
||
|
||
# For App Engine, pyjwt needs to use PyCrypto instead of Cryptography. | ||
jwt.register_algorithm('RS256', RSAAlgorithm(RSAAlgorithm.SHA256)) | ||
|
||
# [START fetch_certificates] | ||
# This URL contains a list of active certificates used to sign Firebase | ||
# auth tokens. | ||
FIREBASE_CERTIFICATES_URL = ( | ||
'https://www.googleapis.com/robot/v1/metadata/x509/' | ||
'securetoken@system.gserviceaccount.com') | ||
|
||
|
||
# [START get_firebase_certificates] | ||
def get_firebase_certificates(): | ||
"""Fetches the current Firebase certificates. | ||
Note: in a production application, you should cache this for at least | ||
an hour. | ||
""" | ||
try: | ||
result = urlfetch.Fetch( | ||
FIREBASE_CERTIFICATES_URL, | ||
validate_certificate=True) | ||
data = result.content | ||
except urlfetch_errors.Error: | ||
logging.error('Error while fetching Firebase certificates.') | ||
raise | ||
|
||
certificates = json.loads(data) | ||
|
||
return certificates | ||
# [END get_firebase_certificates] | ||
# [END fetch_certificates] | ||
|
||
|
||
# [START extract_public_key_from_certificate] | ||
def extract_public_key_from_certificate(x509_certificate): | ||
"""Extracts the PEM public key from an x509 certificate.""" | ||
der_certificate_string = ssl.PEM_cert_to_DER_cert(x509_certificate) | ||
|
||
# Extract subjectPublicKeyInfo field from X.509 certificate (see RFC3280) | ||
der_certificate = asn1.DerSequence() | ||
der_certificate.decode(der_certificate_string) | ||
tbs_certification = asn1.DerSequence() # To Be Signed certificate | ||
tbs_certification.decode(der_certificate[0]) | ||
|
||
subject_public_key_info = tbs_certification[6] | ||
|
||
return subject_public_key_info | ||
# [EMD extract_public_key_from_certificate] | ||
|
||
|
||
# [START verify_auth_token] | ||
def verify_auth_token(request): | ||
"""Verifies the JWT auth token in the request. | ||
If no token is found or if the token is invalid, returns None. | ||
Otherwise, it returns a dictionary containing the JWT claims. | ||
""" | ||
if 'Authorization' not in request.headers: | ||
return None | ||
|
||
# Auth header is in format 'Bearer {jwt}'. | ||
request_jwt = request.headers['Authorization'].split(' ').pop() | ||
|
||
# Determine which certificate was used to sign the JWT. | ||
header = jwt.get_unverified_header(request_jwt) | ||
kid = header['kid'] | ||
|
||
certificates = get_firebase_certificates() | ||
|
||
try: | ||
certificate = certificates[kid] | ||
except KeyError: | ||
logging.warning('JWT signed with unkown kid {}'.format(header['kid'])) | ||
return None | ||
|
||
# Get the public key from the certificate. This is used to verify the | ||
# JWT signature. | ||
public_key = extract_public_key_from_certificate(certificate) | ||
|
||
# [START decrypt_token] | ||
try: | ||
claims = jwt.decode( | ||
request_jwt, | ||
public_key, | ||
algorithms=['RS256'], | ||
audience=os.environ['FIREBASE_PROJECT_ID']) | ||
except jwt.exceptions.InvalidTokenError as e: | ||
logging.warning('JWT verification failed: {}'.format(e)) | ||
return None | ||
# [END decrypt_token] | ||
|
||
return claims | ||
# [END verify_auth_token] |
176 changes: 176 additions & 0 deletions
176
appengine/standard/firebase/firenotes/backend/firebase_helper_test.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
# 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. | ||
|
||
import datetime | ||
import os | ||
import time | ||
|
||
# Remove any existing pyjwt handlers, as firebase_helper will register | ||
# its own. | ||
try: | ||
import jwt | ||
jwt.unregister_algorithm('RS256') | ||
except KeyError: | ||
pass | ||
|
||
import mock | ||
import pytest | ||
|
||
import firebase_helper | ||
|
||
|
||
def test_get_firebase_certificates(testbed): | ||
certs = firebase_helper.get_firebase_certificates() | ||
assert certs | ||
assert len(certs.keys()) | ||
|
||
|
||
@pytest.fixture | ||
def test_certificate(): | ||
from cryptography import utils | ||
from cryptography import x509 | ||
from cryptography.hazmat.backends import default_backend | ||
from cryptography.hazmat.primitives import hashes | ||
from cryptography.hazmat.primitives.asymmetric import rsa | ||
from cryptography.hazmat.primitives import serialization | ||
from cryptography.x509.oid import NameOID | ||
|
||
one_day = datetime.timedelta(1, 0, 0) | ||
private_key = rsa.generate_private_key( | ||
public_exponent=65537, | ||
key_size=2048, | ||
backend=default_backend()) | ||
public_key = private_key.public_key() | ||
builder = x509.CertificateBuilder() | ||
|
||
builder = builder.subject_name(x509.Name([ | ||
x509.NameAttribute(NameOID.COMMON_NAME, u'example.com'), | ||
])) | ||
builder = builder.issuer_name(x509.Name([ | ||
x509.NameAttribute(NameOID.COMMON_NAME, u'example.com'), | ||
])) | ||
builder = builder.not_valid_before(datetime.datetime.today() - one_day) | ||
builder = builder.not_valid_after(datetime.datetime.today() + one_day) | ||
builder = builder.serial_number( | ||
utils.int_from_bytes(os.urandom(20), "big") >> 1) | ||
builder = builder.public_key(public_key) | ||
|
||
builder = builder.add_extension( | ||
x509.BasicConstraints(ca=False, path_length=None), critical=True) | ||
|
||
certificate = builder.sign( | ||
private_key=private_key, algorithm=hashes.SHA256(), | ||
backend=default_backend()) | ||
|
||
certificate_pem = certificate.public_bytes(serialization.Encoding.PEM) | ||
public_key_bytes = certificate.public_key().public_bytes( | ||
serialization.Encoding.DER, | ||
serialization.PublicFormat.SubjectPublicKeyInfo) | ||
private_key_bytes = private_key.private_bytes( | ||
encoding=serialization.Encoding.PEM, | ||
format=serialization.PrivateFormat.TraditionalOpenSSL, | ||
encryption_algorithm=serialization.NoEncryption()) | ||
|
||
yield certificate, certificate_pem, public_key_bytes, private_key_bytes | ||
|
||
|
||
def test_extract_public_key_from_certificate(test_certificate): | ||
_, certificate_pem, public_key_bytes, _ = test_certificate | ||
public_key = firebase_helper.extract_public_key_from_certificate( | ||
certificate_pem) | ||
assert public_key == public_key_bytes | ||
|
||
|
||
def make_jwt(private_key_bytes, claims=None, headers=None): | ||
jwt_claims = { | ||
'iss': 'http://example.com', | ||
'aud': 'test_audience', | ||
'user_id': '123', | ||
'sub': '123', | ||
'iat': int(time.time()), | ||
'exp': int(time.time()) + 60, | ||
'email': 'user@example.com' | ||
} | ||
|
||
jwt_claims.update(claims if claims else {}) | ||
if not headers: | ||
headers = {} | ||
|
||
return jwt.encode( | ||
jwt_claims, private_key_bytes, algorithm='RS256', | ||
headers=headers) | ||
|
||
|
||
def test_verify_auth_token(test_certificate, monkeypatch): | ||
_, certificate_pem, _, private_key_bytes = test_certificate | ||
|
||
# The Firebase project ID is used as the JWT audience. | ||
monkeypatch.setenv('FIREBASE_PROJECT_ID', 'test_audience') | ||
|
||
# Generate a jwt to include in the request. | ||
jwt = make_jwt(private_key_bytes, headers={'kid': '1'}) | ||
|
||
# Make a mock request | ||
request = mock.Mock() | ||
request.headers = {'Authorization': 'Bearer {}'.format(jwt)} | ||
|
||
get_cert_patch = mock.patch('firebase_helper.get_firebase_certificates') | ||
with get_cert_patch as get_cert_mock: | ||
# Make get_firebase_certificates return our test certificate. | ||
get_cert_mock.return_value = {'1': certificate_pem} | ||
claims = firebase_helper.verify_auth_token(request) | ||
|
||
assert claims['user_id'] == '123' | ||
|
||
|
||
def test_verify_auth_token_no_auth_header(): | ||
request = mock.Mock() | ||
request.headers = {} | ||
assert firebase_helper.verify_auth_token(request) is None | ||
|
||
|
||
def test_verify_auth_token_invalid_key_id(test_certificate): | ||
_, _, _, private_key_bytes = test_certificate | ||
jwt = make_jwt(private_key_bytes, headers={'kid': 'invalid'}) | ||
request = mock.Mock() | ||
request.headers = {'Authorization': 'Bearer {}'.format(jwt)} | ||
|
||
get_cert_patch = mock.patch('firebase_helper.get_firebase_certificates') | ||
with get_cert_patch as get_cert_mock: | ||
# Make get_firebase_certificates return no certificates | ||
get_cert_mock.return_value = {} | ||
assert firebase_helper.verify_auth_token(request) is None | ||
|
||
|
||
def test_verify_auth_token_expired(test_certificate, monkeypatch): | ||
_, certificate_pem, _, private_key_bytes = test_certificate | ||
|
||
# The Firebase project ID is used as the JWT audience. | ||
monkeypatch.setenv('FIREBASE_PROJECT_ID', 'test_audience') | ||
|
||
# Generate a jwt to include in the request. | ||
jwt = make_jwt( | ||
private_key_bytes, | ||
claims={'exp': int(time.time()) - 60}, | ||
headers={'kid': '1'}) | ||
|
||
# Make a mock request | ||
request = mock.Mock() | ||
request.headers = {'Authorization': 'Bearer {}'.format(jwt)} | ||
|
||
get_cert_patch = mock.patch('firebase_helper.get_firebase_certificates') | ||
with get_cert_patch as get_cert_mock: | ||
# Make get_firebase_certificates return our test certificate. | ||
get_cert_mock.return_value = {'1': certificate_pem} | ||
assert firebase_helper.verify_auth_token(request) is None |
Oops, something went wrong.