Skip to content

Commit eb19c23

Browse files
callmehiphopanguillanneuf
authored andcommitted
feat(pubsub): authenticated push requests (#1256)
* refactor(pubsub): verify authenticated push requests * refactor: remove iss claim check * refactor: move authenticated sample to new region * docs: update README to include authenticated push info * refactor: remove async keyword * doc: remove beta for topic creation * docs: move authenticated push setup to separate step * docs: add section on how to deploy app * refactor: use regex to get bearer token
1 parent 4b2629e commit eb19c23

File tree

7 files changed

+209
-3
lines changed

7 files changed

+209
-3
lines changed

appengine/pubsub/README.md

+42-2
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,23 @@ Before you can run or deploy the sample, you will need to do the following:
1111
1. Enable the Cloud Pub/Sub API in the [Google Developers Console](https://console.developers.google.com/project/_/apiui/apiview/pubsub/overview).
1212
1. Create a topic and subscription.
1313

14-
gcloud beta pubsub topics create <your-topic-name>
15-
gcloud beta pubsub subscriptions create <your-subscription-name> \
14+
gcloud pubsub topics create <your-topic-name>
15+
gcloud pubsub subscriptions create <your-subscription-name> \
1616
--topic <your-topic-name> \
1717
--push-endpoint \
1818
https://<your-project-id>.appspot.com/pubsub/push?token=<your-verification-token> \
1919
--ack-deadline 30
2020

21+
1. Create a subscription for authenticated pushes. The push auth service account must have Service Account Token Creator Role assigned, which can be done in the Cloud Console [IAM & admin](https://console.cloud.google.com/iam-admin/iam) UI. `--push-auth-token-audience` is optional. If set, remember to modify the audience field check in `app.js` (line 112).
22+
23+
gcloud beta pubsub subscriptions create <your-subscription-name> \
24+
--topic <your-topic-name> \
25+
--push-endpoint \
26+
https://<your-project-id>.appspot.com/pubsub/authenticated-push?token=<your-verification-token> \
27+
--ack-deadline 30 \
28+
--push-auth-service-account=[your-service-account-email] \
29+
--push-auth-token-audience=example.com
30+
2131
1. Update the environment variables in `app.standard.yaml` or `app.flexible.yaml`
2232
(depending on your App Engine environment).
2333

@@ -61,3 +71,33 @@ Response:
6171

6272
After the request completes, you can refresh `localhost:8080` and see the
6373
message in the list of received messages.
74+
75+
### Authenticated push notifications
76+
77+
Simulating authenticated push requests will fail because requests need to contain a Cloud Pub/Sub-generated JWT in the "Authorization" header.
78+
79+
http POST ":8080/pubsub/authenticated-push?token=<your-verification-token>" < sample_message.json
80+
81+
Response:
82+
83+
HTTP/1.1 400 Bad Request
84+
Connection: keep-alive
85+
Date: Thu, 25 Apr 2019 17:47:36 GMT
86+
Transfer-Encoding: chunked
87+
X-Powered-By: Express
88+
89+
Invalid token
90+
91+
## Running on App Engine
92+
93+
Note: Not all the files in the current directory are needed to run your code on App Engine. Specifically, the `test` directory, which is for testing purposes only. It SHOULD NOT be included in when deploying your app. When your app is up and running, Cloud Pub/Sub creates tokens using a private key, then the Google Auth Node.js library takes care of verifying and decoding the token using Google's public certs, to confirm that the push requests indeed come from Cloud Pub/Sub.
94+
95+
In the current directory, deploy using `gcloud`:
96+
97+
gcloud app deploy app.standard.yaml
98+
99+
To deploy to App Engine Node.js Flexible Environment, run
100+
101+
gcloud app deploy app.flexible.yaml
102+
103+
You can now access the application at https://[your-app-id].appspot.com. You can use the form to submit messages, but it's non-deterministic which instance of your application will receive the notification. You can send multiple messages and refresh the page to see the received message.

appengine/pubsub/app.js

+44-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
const express = require('express');
1919
const bodyParser = require('body-parser');
20+
const {OAuth2Client} = require('google-auth-library');
2021
const path = require('path');
2122
const Buffer = require('safe-buffer').Buffer;
2223
const process = require('process'); // Required for mocking environment variables
@@ -29,6 +30,7 @@ const process = require('process'); // Required for mocking environment variable
2930
const {PubSub} = require('@google-cloud/pubsub');
3031

3132
// Instantiate a pubsub client
33+
const authClient = new OAuth2Client();
3234
const pubsub = new PubSub();
3335

3436
const app = express();
@@ -40,6 +42,8 @@ const jsonBodyParser = bodyParser.json();
4042

4143
// List of all messages received by this instance
4244
const messages = [];
45+
const claims = [];
46+
const tokens = [];
4347

4448
// The following environment variables are set by app.yaml when running on GAE,
4549
// but will need to be manually set when running locally.
@@ -50,7 +54,7 @@ const topic = pubsub.topic(TOPIC);
5054

5155
// [START gae_flex_pubsub_index]
5256
app.get('/', (req, res) => {
53-
res.render('index', {messages: messages});
57+
res.render('index', {messages, tokens, claims});
5458
});
5559

5660
app.post('/', formBodyParser, async (req, res, next) => {
@@ -87,6 +91,45 @@ app.post('/pubsub/push', jsonBodyParser, (req, res) => {
8791
});
8892
// [END gae_flex_pubsub_push]
8993

94+
// [START gae_flex_pubsub_auth_push]
95+
app.post('/pubsub/authenticated-push', jsonBodyParser, async (req, res) => {
96+
// Verify that the request originates from the application.
97+
if (req.query.token !== PUBSUB_VERIFICATION_TOKEN) {
98+
res.status(400).send('Invalid request');
99+
return;
100+
}
101+
102+
// Verify that the push request originates from Cloud Pub/Sub.
103+
try {
104+
// Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.
105+
const bearer = req.header('Authorization');
106+
const token = bearer.match(/Bearer (.*)/)[1];
107+
tokens.push(token);
108+
109+
// Verify and decode the JWT.
110+
const ticket = await authClient.verifyIdToken({
111+
idToken: token,
112+
audience: 'example.com',
113+
});
114+
115+
const claim = ticket.getPayload();
116+
claims.push(claim);
117+
} catch (e) {
118+
res.status(400).send('Invalid token');
119+
return;
120+
}
121+
122+
// The message is a unicode string encoded in base64.
123+
const message = Buffer.from(req.body.message.data, 'base64').toString(
124+
'utf-8'
125+
);
126+
127+
messages.push(message);
128+
129+
res.status(200).send();
130+
});
131+
// [END gae_flex_pubsub_auth_push]
132+
90133
// Start the server
91134
const PORT = process.env.PORT || 8080;
92135
app.listen(PORT, () => {

appengine/pubsub/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
"@google-cloud/pubsub": "^0.28.0",
1717
"body-parser": "^1.18.3",
1818
"express": "^4.16.3",
19+
"google-auth-library": "^3.1.2",
1920
"pug": "^2.0.1",
2021
"safe-buffer": "^5.1.2"
2122
},
2223
"devDependencies": {
2324
"@google-cloud/nodejs-repo-tools": "^3.0.0",
25+
"jsonwebtoken": "^8.5.1",
2426
"mocha": "^6.0.0",
27+
"sinon": "^7.3.1",
2528
"uuid": "^3.3.2"
2629
},
2730
"cloud-repo-tools": {

appengine/pubsub/test/app.test.js

+65
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
'use strict';
2020

2121
const assert = require('assert');
22+
const fs = require('fs');
23+
const jwt = require('jsonwebtoken');
24+
const {OAuth2Client} = require('google-auth-library');
2225
const path = require('path');
26+
const sinon = require('sinon');
2327
const utils = require('@google-cloud/nodejs-repo-tools');
2428

2529
const message = 'This is a test message sent at: ';
@@ -28,6 +32,38 @@ const payload = message + Date.now();
2832
const cwd = path.join(__dirname, '../');
2933
const requestObj = utils.getRequest({cwd: cwd});
3034

35+
const fixtures = path.join(__dirname, 'fixtures');
36+
const privateKey = fs.readFileSync(path.join(fixtures, 'privatekey.pem'));
37+
const publicCert = fs.readFileSync(path.join(fixtures, 'public_cert.pem'));
38+
39+
const sandbox = sinon.createSandbox();
40+
41+
function createFakeToken() {
42+
const now = Date.now() / 1000;
43+
44+
const payload = {
45+
aud: 'example.com',
46+
azp: '1234567890',
47+
48+
email_verified: true,
49+
iat: now,
50+
exp: now + 3600,
51+
iss: 'https://accounts.google.com',
52+
sub: '1234567890',
53+
};
54+
55+
const options = {
56+
algorithm: 'RS256',
57+
keyid: 'fake_id',
58+
};
59+
60+
return jwt.sign(payload, privateKey, options);
61+
}
62+
63+
afterEach(() => {
64+
sandbox.restore();
65+
});
66+
3167
it('should send a message to Pub/Sub', async () => {
3268
await requestObj
3369
.post('/')
@@ -51,6 +87,35 @@ it('should receive incoming Pub/Sub messages', async () => {
5187
.expect(200);
5288
});
5389

90+
it('should verify incoming Pub/Sub push requests', async () => {
91+
sandbox
92+
.stub(OAuth2Client.prototype, 'getFederatedSignonCertsAsync')
93+
.resolves({
94+
certs: {
95+
fake_id: publicCert,
96+
},
97+
});
98+
99+
await requestObj
100+
.post('/pubsub/authenticated-push')
101+
.set('Authorization', `Bearer ${createFakeToken()}`)
102+
.query({token: process.env.PUBSUB_VERIFICATION_TOKEN})
103+
.send({
104+
message: {
105+
data: Buffer.from(payload).toString('base64'),
106+
},
107+
})
108+
.expect(200);
109+
110+
// Make sure the message is visible on the home page
111+
await requestObj
112+
.get('/')
113+
.expect(200)
114+
.expect(response => {
115+
assert(response.text.includes(payload));
116+
});
117+
});
118+
54119
it('should check for verification token on incoming Pub/Sub messages', async () => {
55120
await requestObj
56121
.post('/pubsub/push')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj
3+
7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/
4+
xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs
5+
SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18
6+
pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk
7+
SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk
8+
nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq
9+
HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y
10+
nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9
11+
IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2
12+
YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU
13+
Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ
14+
vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP
15+
B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl
16+
aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2
17+
eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI
18+
aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk
19+
klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ
20+
CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu
21+
UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg
22+
soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28
23+
bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH
24+
504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL
25+
YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx
26+
BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==
27+
-----END RSA PRIVATE KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV
3+
BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV
4+
MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
5+
CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM
6+
7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer
7+
uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp
8+
gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4
9+
+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3
10+
ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O
11+
gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh
12+
GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD
13+
AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr
14+
odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk
15+
+JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9
16+
ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql
17+
ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT
18+
cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB
19+
-----END CERTIFICATE-----

appengine/pubsub/views/index.pug

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ html(lang='en')
44
title PubSub
55
meta(charset='utf-8')
66
body
7+
p Bearer tokens received by this instance:
8+
ul
9+
each val in tokens
10+
li= val
11+
p Claims received by this instance:
12+
ul
13+
each val in claims
14+
li
15+
code!= JSON.stringify(val)
716
p Messages received by this instance:
817
ul
918
each val in messages

0 commit comments

Comments
 (0)