Skip to content

Commit

Permalink
Add firenotes firebase auth example
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanvanech committed Sep 1, 2016
1 parent 7fd8a2d commit 6405576
Show file tree
Hide file tree
Showing 13 changed files with 578 additions and 0 deletions.
60 changes: 60 additions & 0 deletions appengine/standard/firebase/firenotes/README.md
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.
1 change: 1 addition & 0 deletions appengine/standard/firebase/firenotes/backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib
17 changes: 17 additions & 0 deletions appengine/standard/firebase/firenotes/backend/app.yaml
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 appengine/standard/firebase/firenotes/backend/appengine_config.py
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 appengine/standard/firebase/firenotes/backend/firebase_helper.py
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
22 changes: 22 additions & 0 deletions appengine/standard/firebase/firenotes/backend/index.yaml
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
104 changes: 104 additions & 0 deletions appengine/standard/firebase/firenotes/backend/main.py
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 appengine/standard/firebase/firenotes/backend/main_test.py
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
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
15 changes: 15 additions & 0 deletions appengine/standard/firebase/firenotes/frontend/app.yaml
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: (.+)
Loading

0 comments on commit 6405576

Please sign in to comment.