-
Notifications
You must be signed in to change notification settings - Fork 6.5k
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
7fd8a2d
commit 6405576
Showing
13 changed files
with
578 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,17 @@ | ||
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: | ||
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') |
105 changes: 105 additions & 0 deletions
105
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,105 @@ | ||
# 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 flask import request | ||
from google.appengine.api import urlfetch, 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)) | ||
|
||
FIREBASE_CERTIFICATES_URL = ( | ||
'https://www.googleapis.com/robot/v1/metadata/x509/' | ||
'[email protected]') | ||
|
||
|
||
def get_firebase_certificates(): | ||
"""Fetches the firebase certificates from firebase. | ||
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 | ||
|
||
|
||
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 | ||
|
||
|
||
def verify_auth_token(): | ||
"""Verifies the JWT auth token in the request. | ||
If none 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) | ||
|
||
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 | ||
|
||
return claims |
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,22 @@ | ||
indexes: | ||
|
||
# AUTOGENERATED | ||
|
||
# This index.yaml is automatically updated whenever the dev_appserver | ||
# detects that a new type of query is run. If you want to manage the | ||
# index.yaml file manually, remove the above marker line (the line | ||
# saying "# AUTOGENERATED"). If you want to manage some indexes | ||
# manually, move them above the marker line. The index.yaml file is | ||
# automatically uploaded to the admin console when you next deploy | ||
# your application using appcfg.py. | ||
|
||
- kind: Note | ||
ancestor: yes | ||
properties: | ||
- name: created | ||
|
||
- kind: Note | ||
ancestor: yes | ||
properties: | ||
- name: created | ||
direction: desc |
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,104 @@ | ||
# 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. | ||
|
||
# [START app] | ||
import logging | ||
|
||
from flask import Flask, jsonify, request | ||
import flask_cors | ||
from google.appengine.ext import ndb | ||
|
||
import firebase_helper | ||
|
||
|
||
app = Flask(__name__) | ||
flask_cors.CORS(app) | ||
|
||
|
||
class Note(ndb.Model): | ||
"""NDB model class for a user's note. | ||
Key is user id from decrypted token.""" | ||
friendly_id = ndb.StringProperty() | ||
message = ndb.TextProperty() | ||
created = ndb.DateTimeProperty(auto_now_add=True) | ||
|
||
|
||
def query_database(user_id): | ||
"""Fetches all notes associated with user_id and orders them | ||
by date created, with most recent note processed first.""" | ||
ancestor_key = ndb.Key(Note, user_id) | ||
query = Note.query(ancestor=ancestor_key).order(-Note.created) | ||
notes = query.fetch() | ||
|
||
note_messages = [] | ||
|
||
for note in notes: | ||
note_messages.append({'friendly_id': note.friendly_id, | ||
'message': note.message, | ||
'created': note.created}) | ||
|
||
return note_messages | ||
|
||
|
||
@app.route('/notes', methods=['GET']) | ||
def list_notes(): | ||
"""Queries database for user's notes to display.""" | ||
claims = firebase_helper.verify_auth_token() | ||
if not claims: | ||
return 'Unauthorized', 401 | ||
|
||
notes = query_database(claims['sub']) | ||
|
||
return jsonify(notes) | ||
|
||
|
||
@app.route('/notes', methods=['POST', 'PUT']) | ||
def add_note(): | ||
""" | ||
Adds a note to the user's notebook. The request should be in this format: | ||
{ | ||
"message": "note message." | ||
} | ||
""" | ||
|
||
claims = firebase_helper.verify_auth_token() | ||
if not claims: | ||
return 'Unauthorized', 401 | ||
|
||
data = request.get_json() | ||
|
||
# Populates note properties according to the model, | ||
# with the user ID as the key. | ||
note = Note(parent=ndb.Key(Note, claims['sub']), | ||
message=data['message']) | ||
|
||
# Some providers do not provide one of these so either can be used. | ||
if 'name' in claims: | ||
note.friendly_id = claims['name'] | ||
else: | ||
note.friendly_id = claims['email'] | ||
|
||
# Stores note in database. | ||
note.put() | ||
|
||
return 'OK', 200 | ||
|
||
|
||
@app.errorhandler(500) | ||
def server_error(e): | ||
# Log the error and stacktrace. | ||
logging.exception('An error occurred during a request.') | ||
return 'An internal error occurred.', 500 | ||
# [END app] |
27 changes: 27 additions & 0 deletions
27
appengine/standard/firebase/firenotes/backend/main_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,27 @@ | ||
# 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 pytest | ||
|
||
|
||
@pytest.fixture | ||
def app(): | ||
import main | ||
main.app.testing = True | ||
return main.app.test_client() | ||
|
||
|
||
def test_index(app): | ||
r = app.get('/') | ||
assert r.status_code == 200 |
3 changes: 3 additions & 0 deletions
3
appengine/standard/firebase/firenotes/backend/requirements.txt
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,3 @@ | ||
Flask==0.11.1 | ||
pyjwt==1.4.1 | ||
flask-cors==3.0.0 |
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,15 @@ | ||
runtime: python27 | ||
api_version: 1 | ||
service: default | ||
threadsafe: true | ||
|
||
handlers: | ||
|
||
# root | ||
- url: / | ||
static_files: index.html | ||
upload: index.html | ||
|
||
- url: /(.+) | ||
static_files: \1 | ||
upload: (.+) |
Oops, something went wrong.