Skip to content

Commit 6405576

Browse files
committed
Add firenotes firebase auth example
1 parent 7fd8a2d commit 6405576

File tree

13 files changed

+578
-0
lines changed

13 files changed

+578
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Firenotes: Firebase Authentication on Google App Engine
2+
3+
A simple note-taking application that stores users' notes in their own personal
4+
notebooks separated by a unique user ID generated by Firebase. Uses Firebase
5+
Authentication, Google App Engine, and Google Cloud Datastore.
6+
7+
You'll need to have [Python 2.7](https://www.python.org/), the
8+
[App Engine SDK](https://cloud.google.com/appengine/downloads#Google_App_Engine_SDK_for_Python),
9+
and the [Google Cloud SDK](https://cloud.google.com/sdk/?hl=en)
10+
installed and initialized to an App Engine project before running the code in
11+
this sample.
12+
13+
## Setup
14+
15+
1. Clone this repo:
16+
17+
git clone https://github.com/GoogleCloudPlatform/python-docs-samples/tree/master/appengine/standard/firebase/auth/firenotes
18+
19+
1. Within a virtualenv, install the dependencies to the backend service:
20+
21+
pip install -r requirements.txt -t lib
22+
pip install pycrypto
23+
24+
Although the pycrypto library is built in to the App Engine standard
25+
environment, it will not be bundled until deployment since it is
26+
platform-dependent. Thus, the app.yaml file includes the bundled version of
27+
pycrypto at runtime, but you still need to install it manually to run the
28+
application on the App Engine local development server.
29+
30+
1. [Add Firebase to your app.](https://firebase.google.com/docs/web/setup#add_firebase_to_your_app)
31+
1. Add your Firebase Project ID to the backend’s `app.yaml` file as an
32+
environment variable.
33+
1. Select which providers you want to enable. Delete the providers from
34+
`main.js` that you do no want to offer. Enable the providers you chose to keep
35+
in the Firebase console under **Auth** > **SIGN-IN METHOD** >
36+
**Sign-in providers**.
37+
1. In the Firebase console, under **OAuth redirect domains**, click
38+
**ADD DOMAIN** and enter the domain of your app on App Engine:
39+
[PROJECT_ID].appspot.com. Do not include "http://" before the domain name.
40+
41+
## Run Locally
42+
1. Add the backend host URL to `main.js`: http://localhost:8081.
43+
1. Navigate to the root directory of the application and start the development
44+
server with the following command:
45+
46+
dev_appserver.py frontend/app.yaml backend/app.yaml
47+
48+
1. Visit [http://locahost:8080/](http://locahost:8080/) in a web browser.
49+
50+
## Deploy
51+
1. Change the backend host URL in `main.js` to
52+
https://backend-dot-[PROJECT_ID].appspot.com.
53+
1. Deploy the application using the Cloud SDK command-line interface:
54+
55+
gcloud app deploy backend/index.yaml frontend/app.yaml backend/app.yaml
56+
57+
The Cloud Datastore indexes can take a while to update, so the application
58+
might not be fully functional immediately after deployment.
59+
60+
1. View the application live at https://[PROJECT_ID].appspot.com.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lib
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
runtime: python27
2+
api_version: 1
3+
threadsafe: true
4+
service: backend
5+
6+
handlers:
7+
- url: /.*
8+
script: main.app
9+
10+
libraries:
11+
- name: ssl
12+
version: 2.7.11
13+
- name: pycrypto
14+
version: 2.6
15+
16+
env_variables:
17+
FIREBASE_PROJECT_ID: '<PROJECT_ID>'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from google.appengine.ext import vendor
16+
17+
# Add any libraries installed in the "lib" folder.
18+
vendor.add('lib')
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import json
15+
import logging
16+
import os
17+
import ssl
18+
19+
from Crypto.Util import asn1
20+
from flask import request
21+
from google.appengine.api import urlfetch, urlfetch_errors
22+
import jwt
23+
from jwt.contrib.algorithms.pycrypto import RSAAlgorithm
24+
import jwt.exceptions
25+
26+
27+
# For App Engine, pyjwt needs to use PyCrypto instead of Cryptography.
28+
jwt.register_algorithm('RS256', RSAAlgorithm(RSAAlgorithm.SHA256))
29+
30+
FIREBASE_CERTIFICATES_URL = (
31+
'https://www.googleapis.com/robot/v1/metadata/x509/'
32+
33+
34+
35+
def get_firebase_certificates():
36+
"""Fetches the firebase certificates from firebase.
37+
38+
Note: in a production application, you should cache this for at least
39+
an hour.
40+
"""
41+
try:
42+
result = urlfetch.Fetch(FIREBASE_CERTIFICATES_URL,
43+
validate_certificate=True)
44+
data = result.content
45+
except urlfetch_errors.Error:
46+
logging.error('Error while fetching Firebase certificates')
47+
raise
48+
49+
certificates = json.loads(data)
50+
51+
return certificates
52+
53+
54+
def extract_public_key_from_certificate(x509_certificate):
55+
"""Extracts the PEM public key from an x509 certificate."""
56+
der_certificate_string = ssl.PEM_cert_to_DER_cert(x509_certificate)
57+
58+
# Extract subjectPublicKeyInfo field from X.509 certificate (see RFC3280)
59+
der_certificate = asn1.DerSequence()
60+
der_certificate.decode(der_certificate_string)
61+
tbs_certification = asn1.DerSequence() # To Be Signed certificate
62+
tbs_certification.decode(der_certificate[0])
63+
64+
subject_public_key_info = tbs_certification[6]
65+
66+
return subject_public_key_info
67+
68+
69+
def verify_auth_token():
70+
"""Verifies the JWT auth token in the request.
71+
If none is found or if the token is invalid, returns None. Otherwise,
72+
it returns a dictionary containing the JWT claims."""
73+
if 'Authorization' not in request.headers:
74+
return None
75+
76+
# Auth header is in format 'Bearer {jwt}'.
77+
request_jwt = request.headers['Authorization'].split(' ').pop()
78+
79+
# Determine which certificate was used to sign the JWT.
80+
header = jwt.get_unverified_header(request_jwt)
81+
kid = header['kid']
82+
83+
certificates = get_firebase_certificates()
84+
85+
try:
86+
certificate = certificates[kid]
87+
except KeyError:
88+
logging.warning('JWT signed with unkown kid {}'.format(header['kid']))
89+
return None
90+
91+
# Get the public key from the certificate. This is used to verify the
92+
# jwt signature.
93+
public_key = extract_public_key_from_certificate(certificate)
94+
95+
try:
96+
claims = jwt.decode(
97+
request_jwt,
98+
public_key,
99+
algorithms=['RS256'],
100+
audience=os.environ['FIREBASE_PROJECT_ID'])
101+
except jwt.exceptions.InvalidTokenError as e:
102+
logging.warning('JWT verification failed: {}'.format(e))
103+
return None
104+
105+
return claims
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
indexes:
2+
3+
# AUTOGENERATED
4+
5+
# This index.yaml is automatically updated whenever the dev_appserver
6+
# detects that a new type of query is run. If you want to manage the
7+
# index.yaml file manually, remove the above marker line (the line
8+
# saying "# AUTOGENERATED"). If you want to manage some indexes
9+
# manually, move them above the marker line. The index.yaml file is
10+
# automatically uploaded to the admin console when you next deploy
11+
# your application using appcfg.py.
12+
13+
- kind: Note
14+
ancestor: yes
15+
properties:
16+
- name: created
17+
18+
- kind: Note
19+
ancestor: yes
20+
properties:
21+
- name: created
22+
direction: desc
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START app]
16+
import logging
17+
18+
from flask import Flask, jsonify, request
19+
import flask_cors
20+
from google.appengine.ext import ndb
21+
22+
import firebase_helper
23+
24+
25+
app = Flask(__name__)
26+
flask_cors.CORS(app)
27+
28+
29+
class Note(ndb.Model):
30+
"""NDB model class for a user's note.
31+
Key is user id from decrypted token."""
32+
friendly_id = ndb.StringProperty()
33+
message = ndb.TextProperty()
34+
created = ndb.DateTimeProperty(auto_now_add=True)
35+
36+
37+
def query_database(user_id):
38+
"""Fetches all notes associated with user_id and orders them
39+
by date created, with most recent note processed first."""
40+
ancestor_key = ndb.Key(Note, user_id)
41+
query = Note.query(ancestor=ancestor_key).order(-Note.created)
42+
notes = query.fetch()
43+
44+
note_messages = []
45+
46+
for note in notes:
47+
note_messages.append({'friendly_id': note.friendly_id,
48+
'message': note.message,
49+
'created': note.created})
50+
51+
return note_messages
52+
53+
54+
@app.route('/notes', methods=['GET'])
55+
def list_notes():
56+
"""Queries database for user's notes to display."""
57+
claims = firebase_helper.verify_auth_token()
58+
if not claims:
59+
return 'Unauthorized', 401
60+
61+
notes = query_database(claims['sub'])
62+
63+
return jsonify(notes)
64+
65+
66+
@app.route('/notes', methods=['POST', 'PUT'])
67+
def add_note():
68+
"""
69+
Adds a note to the user's notebook. The request should be in this format:
70+
71+
{
72+
"message": "note message."
73+
}
74+
"""
75+
76+
claims = firebase_helper.verify_auth_token()
77+
if not claims:
78+
return 'Unauthorized', 401
79+
80+
data = request.get_json()
81+
82+
# Populates note properties according to the model,
83+
# with the user ID as the key.
84+
note = Note(parent=ndb.Key(Note, claims['sub']),
85+
message=data['message'])
86+
87+
# Some providers do not provide one of these so either can be used.
88+
if 'name' in claims:
89+
note.friendly_id = claims['name']
90+
else:
91+
note.friendly_id = claims['email']
92+
93+
# Stores note in database.
94+
note.put()
95+
96+
return 'OK', 200
97+
98+
99+
@app.errorhandler(500)
100+
def server_error(e):
101+
# Log the error and stacktrace.
102+
logging.exception('An error occurred during a request.')
103+
return 'An internal error occurred.', 500
104+
# [END app]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright 2016 Google Inc. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pytest
16+
17+
18+
@pytest.fixture
19+
def app():
20+
import main
21+
main.app.testing = True
22+
return main.app.test_client()
23+
24+
25+
def test_index(app):
26+
r = app.get('/')
27+
assert r.status_code == 200
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Flask==0.11.1
2+
pyjwt==1.4.1
3+
flask-cors==3.0.0
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
runtime: python27
2+
api_version: 1
3+
service: default
4+
threadsafe: true
5+
6+
handlers:
7+
8+
# root
9+
- url: /
10+
static_files: index.html
11+
upload: index.html
12+
13+
- url: /(.+)
14+
static_files: \1
15+
upload: (.+)

0 commit comments

Comments
 (0)