Skip to content

Commit aca4aa2

Browse files
tristanvanechJon Wayne Parrott
authored and
Jon Wayne Parrott
committed
Add Firebase Auth sample (#491)
1 parent ee148dc commit aca4aa2

File tree

15 files changed

+895
-0
lines changed

15 files changed

+895
-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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
# Replace with your Firebase project ID.
18+
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: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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+
import json
16+
import logging
17+
import os
18+
import ssl
19+
20+
from Crypto.Util import asn1
21+
from google.appengine.api import urlfetch
22+
from google.appengine.api import urlfetch_errors
23+
import jwt
24+
from jwt.contrib.algorithms.pycrypto import RSAAlgorithm
25+
import jwt.exceptions
26+
27+
28+
# For App Engine, pyjwt needs to use PyCrypto instead of Cryptography.
29+
jwt.register_algorithm('RS256', RSAAlgorithm(RSAAlgorithm.SHA256))
30+
31+
# [START fetch_certificates]
32+
# This URL contains a list of active certificates used to sign Firebase
33+
# auth tokens.
34+
FIREBASE_CERTIFICATES_URL = (
35+
'https://www.googleapis.com/robot/v1/metadata/x509/'
36+
37+
38+
39+
# [START get_firebase_certificates]
40+
def get_firebase_certificates():
41+
"""Fetches the current Firebase certificates.
42+
43+
Note: in a production application, you should cache this for at least
44+
an hour.
45+
"""
46+
try:
47+
result = urlfetch.Fetch(
48+
FIREBASE_CERTIFICATES_URL,
49+
validate_certificate=True)
50+
data = result.content
51+
except urlfetch_errors.Error:
52+
logging.error('Error while fetching Firebase certificates.')
53+
raise
54+
55+
certificates = json.loads(data)
56+
57+
return certificates
58+
# [END get_firebase_certificates]
59+
# [END fetch_certificates]
60+
61+
62+
# [START extract_public_key_from_certificate]
63+
def extract_public_key_from_certificate(x509_certificate):
64+
"""Extracts the PEM public key from an x509 certificate."""
65+
der_certificate_string = ssl.PEM_cert_to_DER_cert(x509_certificate)
66+
67+
# Extract subjectPublicKeyInfo field from X.509 certificate (see RFC3280)
68+
der_certificate = asn1.DerSequence()
69+
der_certificate.decode(der_certificate_string)
70+
tbs_certification = asn1.DerSequence() # To Be Signed certificate
71+
tbs_certification.decode(der_certificate[0])
72+
73+
subject_public_key_info = tbs_certification[6]
74+
75+
return subject_public_key_info
76+
# [EMD extract_public_key_from_certificate]
77+
78+
79+
# [START verify_auth_token]
80+
def verify_auth_token(request):
81+
"""Verifies the JWT auth token in the request.
82+
83+
If no token is found or if the token is invalid, returns None.
84+
Otherwise, it returns a dictionary containing the JWT claims.
85+
"""
86+
if 'Authorization' not in request.headers:
87+
return None
88+
89+
# Auth header is in format 'Bearer {jwt}'.
90+
request_jwt = request.headers['Authorization'].split(' ').pop()
91+
92+
# Determine which certificate was used to sign the JWT.
93+
header = jwt.get_unverified_header(request_jwt)
94+
kid = header['kid']
95+
96+
certificates = get_firebase_certificates()
97+
98+
try:
99+
certificate = certificates[kid]
100+
except KeyError:
101+
logging.warning('JWT signed with unkown kid {}'.format(header['kid']))
102+
return None
103+
104+
# Get the public key from the certificate. This is used to verify the
105+
# JWT signature.
106+
public_key = extract_public_key_from_certificate(certificate)
107+
108+
# [START decrypt_token]
109+
try:
110+
claims = jwt.decode(
111+
request_jwt,
112+
public_key,
113+
algorithms=['RS256'],
114+
audience=os.environ['FIREBASE_PROJECT_ID'])
115+
except jwt.exceptions.InvalidTokenError as e:
116+
logging.warning('JWT verification failed: {}'.format(e))
117+
return None
118+
# [END decrypt_token]
119+
120+
return claims
121+
# [END verify_auth_token]
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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 datetime
16+
import os
17+
import time
18+
19+
# Remove any existing pyjwt handlers, as firebase_helper will register
20+
# its own.
21+
try:
22+
import jwt
23+
jwt.unregister_algorithm('RS256')
24+
except KeyError:
25+
pass
26+
27+
import mock
28+
import pytest
29+
30+
import firebase_helper
31+
32+
33+
def test_get_firebase_certificates(testbed):
34+
certs = firebase_helper.get_firebase_certificates()
35+
assert certs
36+
assert len(certs.keys())
37+
38+
39+
@pytest.fixture
40+
def test_certificate():
41+
from cryptography import utils
42+
from cryptography import x509
43+
from cryptography.hazmat.backends import default_backend
44+
from cryptography.hazmat.primitives import hashes
45+
from cryptography.hazmat.primitives.asymmetric import rsa
46+
from cryptography.hazmat.primitives import serialization
47+
from cryptography.x509.oid import NameOID
48+
49+
one_day = datetime.timedelta(1, 0, 0)
50+
private_key = rsa.generate_private_key(
51+
public_exponent=65537,
52+
key_size=2048,
53+
backend=default_backend())
54+
public_key = private_key.public_key()
55+
builder = x509.CertificateBuilder()
56+
57+
builder = builder.subject_name(x509.Name([
58+
x509.NameAttribute(NameOID.COMMON_NAME, u'example.com'),
59+
]))
60+
builder = builder.issuer_name(x509.Name([
61+
x509.NameAttribute(NameOID.COMMON_NAME, u'example.com'),
62+
]))
63+
builder = builder.not_valid_before(datetime.datetime.today() - one_day)
64+
builder = builder.not_valid_after(datetime.datetime.today() + one_day)
65+
builder = builder.serial_number(
66+
utils.int_from_bytes(os.urandom(20), "big") >> 1)
67+
builder = builder.public_key(public_key)
68+
69+
builder = builder.add_extension(
70+
x509.BasicConstraints(ca=False, path_length=None), critical=True)
71+
72+
certificate = builder.sign(
73+
private_key=private_key, algorithm=hashes.SHA256(),
74+
backend=default_backend())
75+
76+
certificate_pem = certificate.public_bytes(serialization.Encoding.PEM)
77+
public_key_bytes = certificate.public_key().public_bytes(
78+
serialization.Encoding.DER,
79+
serialization.PublicFormat.SubjectPublicKeyInfo)
80+
private_key_bytes = private_key.private_bytes(
81+
encoding=serialization.Encoding.PEM,
82+
format=serialization.PrivateFormat.TraditionalOpenSSL,
83+
encryption_algorithm=serialization.NoEncryption())
84+
85+
yield certificate, certificate_pem, public_key_bytes, private_key_bytes
86+
87+
88+
def test_extract_public_key_from_certificate(test_certificate):
89+
_, certificate_pem, public_key_bytes, _ = test_certificate
90+
public_key = firebase_helper.extract_public_key_from_certificate(
91+
certificate_pem)
92+
assert public_key == public_key_bytes
93+
94+
95+
def make_jwt(private_key_bytes, claims=None, headers=None):
96+
jwt_claims = {
97+
'iss': 'http://example.com',
98+
'aud': 'test_audience',
99+
'user_id': '123',
100+
'sub': '123',
101+
'iat': int(time.time()),
102+
'exp': int(time.time()) + 60,
103+
'email': '[email protected]'
104+
}
105+
106+
jwt_claims.update(claims if claims else {})
107+
if not headers:
108+
headers = {}
109+
110+
return jwt.encode(
111+
jwt_claims, private_key_bytes, algorithm='RS256',
112+
headers=headers)
113+
114+
115+
def test_verify_auth_token(test_certificate, monkeypatch):
116+
_, certificate_pem, _, private_key_bytes = test_certificate
117+
118+
# The Firebase project ID is used as the JWT audience.
119+
monkeypatch.setenv('FIREBASE_PROJECT_ID', 'test_audience')
120+
121+
# Generate a jwt to include in the request.
122+
jwt = make_jwt(private_key_bytes, headers={'kid': '1'})
123+
124+
# Make a mock request
125+
request = mock.Mock()
126+
request.headers = {'Authorization': 'Bearer {}'.format(jwt)}
127+
128+
get_cert_patch = mock.patch('firebase_helper.get_firebase_certificates')
129+
with get_cert_patch as get_cert_mock:
130+
# Make get_firebase_certificates return our test certificate.
131+
get_cert_mock.return_value = {'1': certificate_pem}
132+
claims = firebase_helper.verify_auth_token(request)
133+
134+
assert claims['user_id'] == '123'
135+
136+
137+
def test_verify_auth_token_no_auth_header():
138+
request = mock.Mock()
139+
request.headers = {}
140+
assert firebase_helper.verify_auth_token(request) is None
141+
142+
143+
def test_verify_auth_token_invalid_key_id(test_certificate):
144+
_, _, _, private_key_bytes = test_certificate
145+
jwt = make_jwt(private_key_bytes, headers={'kid': 'invalid'})
146+
request = mock.Mock()
147+
request.headers = {'Authorization': 'Bearer {}'.format(jwt)}
148+
149+
get_cert_patch = mock.patch('firebase_helper.get_firebase_certificates')
150+
with get_cert_patch as get_cert_mock:
151+
# Make get_firebase_certificates return no certificates
152+
get_cert_mock.return_value = {}
153+
assert firebase_helper.verify_auth_token(request) is None
154+
155+
156+
def test_verify_auth_token_expired(test_certificate, monkeypatch):
157+
_, certificate_pem, _, private_key_bytes = test_certificate
158+
159+
# The Firebase project ID is used as the JWT audience.
160+
monkeypatch.setenv('FIREBASE_PROJECT_ID', 'test_audience')
161+
162+
# Generate a jwt to include in the request.
163+
jwt = make_jwt(
164+
private_key_bytes,
165+
claims={'exp': int(time.time()) - 60},
166+
headers={'kid': '1'})
167+
168+
# Make a mock request
169+
request = mock.Mock()
170+
request.headers = {'Authorization': 'Bearer {}'.format(jwt)}
171+
172+
get_cert_patch = mock.patch('firebase_helper.get_firebase_certificates')
173+
with get_cert_patch as get_cert_mock:
174+
# Make get_firebase_certificates return our test certificate.
175+
get_cert_mock.return_value = {'1': certificate_pem}
176+
assert firebase_helper.verify_auth_token(request) is None

0 commit comments

Comments
 (0)