Skip to content

Commit 5715f8c

Browse files
authored
Add authenticated-json-api sample. (firebase#3)
1 parent 8dc0081 commit 5715f8c

File tree

8 files changed

+527
-0
lines changed

8 files changed

+527
-0
lines changed

authenticated-json-api/README.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Authenticated JSON API
2+
3+
This sample shows how to authenticate access to a JSON API to only allow
4+
access to data for a specific Firebase user.
5+
6+
Only users who pass a valid Firebase ID token as a Bearer token in the
7+
`Authorization` header of the HTTP request are authorized to use the API.
8+
9+
This sample comes with a web-based API explorer UI whose code is in the [public](public) directory.
10+
It lets you sign in to Firebase with your Google account, and create messages whose sentiments are
11+
detected by the [Cloud Natural Language API](https://cloud.google.com/natural-language/).
12+
13+
## Setting up the sample
14+
15+
1. Create a Firebase Project using the [Firebase Console](https://console.firebase.google.com).
16+
1. Enable the **Google** Provider in the **Auth** section.
17+
1. Enable Billing on your project (to connect to the Natural Language API) by switching to the Blaze or Flame plan.
18+
1. Clone or download this repo and open the `authenticated-json-api` directory.
19+
1. You must have the Firebase CLI installed. If you don't have it install it with `npm install -g firebase-tools` and then configure it with `firebase login`.
20+
1. Configure the CLI locally by using `firebase use --add` and select your project in the list.
21+
1. Install dependencies locally by running: `cd functions; npm install; cd -`
22+
1. Enable the Google Cloud Natural Language API: https://console.cloud.google.com/apis/api/language.googleapis.com/overview?project=_
23+
24+
## Deploy and test
25+
26+
This sample comes with a web-based UI for testing the function. To test it out:
27+
28+
1. Deploy your project using `firebase deploy`
29+
1. Open the app using `firebase open hosting:site`, this will open a browser.
30+
1. Sign in to the web app in the browser using Google Sign-In
31+
1. Create messages and explore them using the List and Detail sections.
32+
1. Sign out. You should no longer be able to access the API.
33+
34+
## Contributing
35+
36+
We'd love that you contribute to the project. Before doing so please read our [Contributor guide](../CONTRIBUTING.md).
37+
38+
## License
39+
40+
© Google, 2017. Licensed under an [Apache-2](../LICENSE) license.
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"rules": {
3+
"users": {
4+
"$uid": {
5+
".read": "auth.uid === $uid",
6+
".write": "auth.uid === $uid",
7+
"messages": {
8+
".indexOn": ["category"]
9+
}
10+
}
11+
}
12+
}
13+
}

authenticated-json-api/firebase.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"hosting": {
3+
"public": "public",
4+
"rewrites": [
5+
{ "source": "/api/**", "function": "api" }
6+
]
7+
},
8+
"database": {
9+
"rules": "database.rules.json"
10+
}
11+
}
+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Copyright 2017 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
'use strict';
18+
19+
const functions = require('firebase-functions');
20+
const admin = require('firebase-admin');
21+
const Language = require('@google-cloud/language');
22+
const express = require('express');
23+
24+
const app = express();
25+
const language = new Language({projectId: process.env.GCLOUD_PROJECT});
26+
27+
admin.initializeApp(functions.config().firebase);
28+
29+
// Express middleware that validates Firebase ID Tokens passed in the Authorization HTTP header.
30+
// The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
31+
// `Authorization: Bearer <Firebase ID Token>`.
32+
// when decoded successfully, the ID Token content will be added as `req.user`.
33+
const authenticate = (req, res, next) => {
34+
if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) {
35+
res.status(403).send('Unauthorized');
36+
return;
37+
}
38+
const idToken = req.headers.authorization.split('Bearer ')[1];
39+
admin.auth().verifyIdToken(idToken).then(decodedIdToken => {
40+
req.user = decodedIdToken;
41+
next();
42+
}).catch(error => {
43+
res.status(403).send('Unauthorized');
44+
});
45+
};
46+
47+
app.use(authenticate);
48+
49+
// POST /api/messages
50+
// Create a new message, get its sentiment using Google Cloud NLP,
51+
// and categorize the sentiment before saving.
52+
app.post('/api/messages', (req, res) => {
53+
const message = req.body.message;
54+
55+
language.detectSentiment(message).then(results => {
56+
const category = categorizeScore(results[0].score);
57+
const data = {message: message, sentiment: results, category: category};
58+
return admin.database().ref(`/users/${req.user.uid}/messages`).push(data);
59+
}).then(snapshot => {
60+
return snapshot.ref.once('value');
61+
}).then(snapshot => {
62+
const val = snapshot.val();
63+
res.status(201).json({message: val.message, category: val.category});
64+
}).catch(error => {
65+
console.log('Error detecting sentiment or saving message', error.message);
66+
res.sendStatus(500);
67+
});
68+
});
69+
70+
// GET /api/messages?category={category}
71+
// Get all messages, optionally specifying a category to filter on
72+
app.get('/api/messages', (req, res) => {
73+
const category = req.query.category;
74+
let query = admin.database().ref(`/users/${req.user.uid}/messages`);
75+
76+
if (category && ['positive', 'negative', 'neutral'].indexOf(category) > -1) {
77+
// Update the query with the valid category
78+
query = query.orderByChild('category').equalTo(category);
79+
} else if (category) {
80+
return res.status(404).json({errorCode: 404, errorMessage: `category '${category}' not found`});
81+
}
82+
83+
query.once('value').then(snapshot => {
84+
var messages = [];
85+
snapshot.forEach(childSnapshot => {
86+
messages.push({key: childSnapshot.key, message: childSnapshot.val().message});
87+
});
88+
89+
return res.status(200).json(messages);
90+
}).catch(error => {
91+
console.log('Error getting messages', error.message);
92+
res.sendStatus(500);
93+
});
94+
});
95+
96+
// GET /api/message/{messageId}
97+
// Get details about a message
98+
app.get('/api/message/:messageId', (req, res) => {
99+
const messageId = req.params.messageId;
100+
admin.database().ref(`/users/${req.user.uid}/messages/${messageId}`).once('value').then(snapshot => {
101+
if (snapshot.val() !== null) {
102+
// Cache details in the browser for 5 minutes
103+
res.set('Cache-Control', 'private, max-age=300');
104+
res.status(200).json(snapshot.val());
105+
} else {
106+
res.status(404).json({errorCode: 404, errorMessage: `message '${messageId}' not found`});
107+
}
108+
}).catch(error => {
109+
console.log('Error getting message details', messageId, error.message);
110+
res.sendStatus(500);
111+
});
112+
});
113+
114+
// Expose the API as a function
115+
exports.api = functions.https.onRequest(app);
116+
117+
// Helper function to categorize a sentiment score as positive, negative, or neutral
118+
const categorizeScore = score => {
119+
if (score > 0.25) {
120+
return 'positive';
121+
} else if (score < -0.25) {
122+
return 'negative';
123+
} else {
124+
return 'neutral';
125+
}
126+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "functions",
3+
"description": "Cloud Functions for Firebase",
4+
"dependencies": {
5+
"@google-cloud/language": "^0.10.3",
6+
"express": "^4.15.2",
7+
"firebase-admin": "~4.1.2",
8+
"firebase-functions": "^0.5"
9+
},
10+
"private": true
11+
}
+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<!DOCTYPE html>
2+
<!--
3+
Copyright 2017 Google Inc. All rights reserved.
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
https://www.apache.org/licenses/LICENSE-2.0
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License
13+
-->
14+
<html lang="en">
15+
<head>
16+
<meta charset="utf-8">
17+
<meta name="viewport" content="width=device-width, initial-scale=1">
18+
<title>Authenticated JSON API</title>
19+
20+
<!-- Material Design Lite -->
21+
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
22+
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
23+
<link rel="stylesheet" href="https://code.getmdl.io/1.1.3/material.light_blue-pink.min.css">
24+
25+
<link rel="stylesheet" href="main.css">
26+
</head>
27+
<body>
28+
<header class="mdl-layout__header mdl-color-text--white mdl-color--light-blue-700">
29+
<div class="mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-grid">
30+
<div class="mdl-layout__header-row mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-cell--8-col-desktop">
31+
<h3>Authenticated JSON API Demo</h3>
32+
</div>
33+
</div>
34+
</header>
35+
36+
<div class="mdl-grid">
37+
<div class="mdl-cell mdl-cell--2-offset--desktop mdl-cell--4-col mdl-shadow--2dp">
38+
<div id="demo-sign-in-card" class="mdl-card">
39+
<div class="mdl-card__title mdl-color--light-blue-100">
40+
<h2 class="mdl-card__title-text">Sign in with Google</h2>
41+
</div>
42+
<div class="mdl-card__supporting-text mdl-color-text--grey-600">
43+
This web application demonstrates how to expose an authenticated JSON API via Firebase Hosting and
44+
Cloud Functions for Firebase.
45+
</div>
46+
<div class="mdl-card__actions mdl-card--border">
47+
<button id="demo-sign-in-button" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect">
48+
<i class="material-icons">account_circle</i>
49+
Sign in with Google
50+
</button>
51+
<button id="demo-sign-out-button" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect">
52+
Sign out
53+
</button>
54+
</div>
55+
</div>
56+
</div>
57+
</div>
58+
59+
<div class="mdl-grid">
60+
<div class="mdl-cell">
61+
<h4>Explore the API <small>Requires Sign In</small></h4>
62+
</div>
63+
</div>
64+
65+
<div class="mdl-grid">
66+
<div class="mdl-cell mdl-cell--8-col mdl-shadow--2dp demo-create-message">
67+
<h5>New Message</h5>
68+
<div class="mdl-textfield mdl-js-textfield">
69+
<textarea class="mdl-textfield__input" type="text" rows="1" id="demo-message" ></textarea>
70+
<label class="mdl-textfield__label" for="demo-message">Type a message to analyze...</label>
71+
</div>
72+
<a id="demo-create-message" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect">
73+
Save and Analyze
74+
</a>
75+
<span id="demo-create-message-result"></span>
76+
</div>
77+
</div>
78+
79+
<div class="mdl-grid">
80+
<div class="mdl-cell mdl-cell--4-col mdl-shadow--2dp">
81+
<div class="demo-message-list">
82+
<h5>List Messages</h5>
83+
<button id="message-list-button-all" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect message-list-button">All</button>
84+
<button class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect message-list-button">Positive</button>
85+
<button class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect message-list-button">Negative</button>
86+
<button class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect message-list-button">Neutral</button>
87+
88+
<ul id="demo-message-list" class="mdl-list"></ul>
89+
</div>
90+
</div>
91+
92+
<div class="mdl-cell mdl-cell--4-col mdl-shadow--2dp">
93+
<div class="demo-message-details">
94+
<h5>Message Details</h5>
95+
<pre id="demo-message-details"></pre>
96+
</div>
97+
</div>
98+
</div>
99+
100+
<script src="https://code.getmdl.io/1.1.3/material.min.js"></script>
101+
<script src="/__/firebase/3.9.0/firebase-app.js"></script>
102+
<script src="/__/firebase/3.9.0/firebase-auth.js"></script>
103+
<script src="/__/firebase/3.9.0/firebase-database.js"></script>
104+
<script src="/__/firebase/init.js"></script>
105+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.0/jquery.min.js"></script>
106+
<script src="main.js"></script>
107+
</body>
108+
</html>
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Copyright 2017 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
.mdl-card {
18+
width: 100%;
19+
}
20+
21+
#demo-sign-in-card > .mdl-card__title {
22+
height: 125px;
23+
}
24+
25+
#demo-create-message-result {
26+
margin-left: 10px;
27+
}
28+
29+
.mdl-textfield, .mdl-textfield > textarea {
30+
width: 100%;
31+
resize: none;
32+
}
33+
34+
h5 {
35+
margin: 20px 5px;
36+
}
37+
38+
button:not(:first-child) {
39+
margin-left: 5px;
40+
}
41+
42+
li:hover {
43+
background-color: rgba(158,158,158,.2);
44+
cursor: pointer;
45+
}
46+
47+
li.selected {
48+
background-color: rgba(158,158,158,.4);
49+
}
50+
51+
.demo-create-message {
52+
padding: 10px;
53+
}
54+
55+
.demo-message-list,
56+
.demo-message-details {
57+
padding-left: 10px;
58+
}
59+
60+
.demo-message-list > ul {
61+
height: 335px;
62+
overflow: auto;
63+
}
64+
65+
.demo-message-details > pre {
66+
height: 400px;
67+
overflow: auto;
68+
}

0 commit comments

Comments
 (0)