Skip to content

Commit 5173029

Browse files
ajaaymanguillanneuf
authored andcommitted
Example for pubsub authenticated push (GoogleCloudPlatform#1407)
1 parent ee82760 commit 5173029

File tree

9 files changed

+327
-48
lines changed

9 files changed

+327
-48
lines changed

appengine-java8/pubsub/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ gcloud beta pubsub subscriptions create <your-subscription-name> \
5353
--ack-deadline 30
5454
```
5555

56+
- Create a subscription for authenticated pushes to send messages to a Google Cloud Project URL such as https://<your-project-id>.appspot.com/authenticated-push.
57+
58+
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.
59+
`--push-auth-token-audience` is optional. If set, remember to modify the audience field check in [PubSubAuthenticatedPush.java](src/main/java/com/example/appengine/pubsub/PubSubAuthenticatedPush.java#L36).
60+
61+
```
62+
gcloud beta pubsub subscriptions create <your-subscription-name> \
63+
--topic <your-topic-name> \
64+
--push-endpoint \
65+
https://<your-project-id>.appspot.com/pubsub/authenticated-push?token=<your-verification-token> \
66+
--ack-deadline 30 \
67+
--push-auth-service-account=[your-service-account-email] \
68+
--push-auth-token-audience=example.com
69+
```
70+
5671
## Run locally
5772
Set the following environment variables and run using shown Maven command. You can then
5873
direct your browser to `http://localhost:8080/`
@@ -70,6 +85,15 @@ mvn appengine:run
7085
"localhost:8080/pubsub/push?token=<your-token>"
7186
```
7287

88+
### Authenticated push notifications
89+
90+
Simulating authenticated push requests will fail because requests need to contain a Cloud Pub/Sub-generated JWT in the "Authorization" header.
91+
92+
```
93+
curl -H "Content-Type: application/json" -i --data @sample_message.json
94+
"localhost:8080/pubsub/authenticated-push?token=<your-token>"
95+
```
96+
7397
## Deploy
7498

7599
Update the environment variables `PUBSUB_TOPIC` and `PUBSUB_VERIFICATION_TOKEN` in

appengine-java8/pubsub/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@
4747
<type>jar</type>
4848
<scope>provided</scope>
4949
</dependency>
50+
<dependency>
51+
<groupId>com.googlecode.jatl</groupId>
52+
<artifactId>jatl</artifactId>
53+
<version>0.2.2</version>
54+
</dependency>
5055

5156
<!-- [START dependencies] -->
5257
<dependency>

appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepository.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
1817
package com.example.appengine.pubsub;
1918

2019
import java.util.List;
@@ -26,8 +25,31 @@ public interface MessageRepository {
2625

2726
/**
2827
* Retrieve most recent stored messages.
28+
*
2929
* @param limit number of messages
3030
* @return list of messages
3131
*/
3232
List<Message> retrieve(int limit);
33+
34+
/** Save claim to persistent storage. */
35+
void saveClaim(String claim);
36+
37+
/**
38+
* Retrieve most recent stored claims.
39+
*
40+
* @param limit number of messages
41+
* @return list of claims
42+
*/
43+
List<String> retrieveClaims(int limit);
44+
45+
/** Save token to persistent storage. */
46+
void saveToken(String token);
47+
48+
/**
49+
* Retrieve most recent stored tokens.
50+
*
51+
* @param limit number of messages
52+
* @return list of tokens
53+
*/
54+
List<String> retrieveTokens(int limit);
3355
}

appengine-java8/pubsub/src/main/java/com/example/appengine/pubsub/MessageRepositoryImpl.java

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
1817
package com.example.appengine.pubsub;
1918

2019
import com.google.cloud.datastore.Datastore;
@@ -35,15 +34,21 @@ public class MessageRepositoryImpl implements MessageRepository {
3534

3635
private String messagesKind = "messages";
3736
private KeyFactory keyFactory = getDatastoreInstance().newKeyFactory().setKind(messagesKind);
37+
private String claimsKind = "claims";
38+
private KeyFactory claimsKindKeyFactory =
39+
getDatastoreInstance().newKeyFactory().setKind(claimsKind);
40+
private String tokensKind = "tokens";
41+
private KeyFactory tokensKindKeyFactory =
42+
getDatastoreInstance().newKeyFactory().setKind(tokensKind);
3843

3944
@Override
4045
public void save(Message message) {
4146
// Save message to "messages"
4247
Datastore datastore = getDatastoreInstance();
4348
Key key = datastore.allocateId(keyFactory.newKey());
4449

45-
Entity.Builder messageEntityBuilder = Entity.newBuilder(key)
46-
.set("messageId", message.getMessageId());
50+
Entity.Builder messageEntityBuilder =
51+
Entity.newBuilder(key).set("messageId", message.getMessageId());
4752

4853
if (message.getData() != null) {
4954
messageEntityBuilder = messageEntityBuilder.set("data", message.getData());
@@ -84,15 +89,72 @@ public List<Message> retrieve(int limit) {
8489
return messages;
8590
}
8691

92+
@Override
93+
public void saveClaim(String claim) {
94+
// Save message to "messages"
95+
Datastore datastore = getDatastoreInstance();
96+
Key key = datastore.allocateId(claimsKindKeyFactory.newKey());
97+
98+
Entity.Builder claimEntityBuilder = Entity.newBuilder(key).set("claim", claim);
99+
100+
datastore.put(claimEntityBuilder.build());
101+
}
102+
103+
@Override
104+
public List<String> retrieveClaims(int limit) {
105+
// Get claim saved in Datastore
106+
Datastore datastore = getDatastoreInstance();
107+
Query<Entity> query = Query.newEntityQueryBuilder().setKind(claimsKind).setLimit(limit).build();
108+
QueryResults<Entity> results = datastore.run(query);
109+
110+
List<String> claims = new ArrayList<>();
111+
while (results.hasNext()) {
112+
Entity entity = results.next();
113+
String claim = entity.getString("claim");
114+
if (claim != null) {
115+
claims.add(claim);
116+
}
117+
}
118+
return claims;
119+
}
120+
121+
@Override
122+
public void saveToken(String token) {
123+
// Save message to "messages"
124+
Datastore datastore = getDatastoreInstance();
125+
Key key = datastore.allocateId(tokensKindKeyFactory.newKey());
126+
127+
Entity.Builder tokenEntityBuilder = Entity.newBuilder(key).set("token", token);
128+
129+
datastore.put(tokenEntityBuilder.build());
130+
}
131+
132+
@Override
133+
public List<String> retrieveTokens(int limit) {
134+
// Get token saved in Datastore
135+
Datastore datastore = getDatastoreInstance();
136+
Query<Entity> query = Query.newEntityQueryBuilder().setKind(tokensKind).setLimit(limit).build();
137+
QueryResults<Entity> results = datastore.run(query);
138+
139+
List<String> tokens = new ArrayList<>();
140+
while (results.hasNext()) {
141+
Entity entity = results.next();
142+
String token = entity.getString("token");
143+
if (token != null) {
144+
tokens.add(token);
145+
}
146+
}
147+
return tokens;
148+
}
149+
87150
private Datastore getDatastoreInstance() {
88151
return DatastoreOptions.getDefaultInstance().getService();
89152
}
90153

91-
private MessageRepositoryImpl() {
92-
}
154+
private MessageRepositoryImpl() {}
93155

94156
// retrieve a singleton instance
95-
public static synchronized MessageRepositoryImpl getInstance() {
157+
public static synchronized MessageRepositoryImpl getInstance() {
96158
if (instance == null) {
97159
instance = new MessageRepositoryImpl();
98160
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2019 Google LLC
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+
package com.example.appengine.pubsub;
18+
19+
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
20+
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
21+
import com.google.api.client.http.javanet.NetHttpTransport;
22+
import com.google.api.client.json.jackson2.JacksonFactory;
23+
import com.google.gson.Gson;
24+
import com.google.gson.JsonElement;
25+
import com.google.gson.JsonParser;
26+
import java.io.IOException;
27+
import java.util.Base64;
28+
import java.util.Collections;
29+
import java.util.stream.Collectors;
30+
import javax.servlet.ServletException;
31+
import javax.servlet.annotation.WebServlet;
32+
import javax.servlet.http.HttpServlet;
33+
import javax.servlet.http.HttpServletRequest;
34+
import javax.servlet.http.HttpServletResponse;
35+
36+
// [START gae_standard_pubsub_auth_push]
37+
@WebServlet(value = "/pubsub/authenticated-push")
38+
public class PubSubAuthenticatedPush extends HttpServlet {
39+
private final String pubsubVerificationToken = System.getenv("PUBSUB_VERIFICATION_TOKEN");
40+
private final MessageRepository messageRepository;
41+
private final GoogleIdTokenVerifier verifier =
42+
new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory())
43+
/**
44+
* Please change example.com to match with value you are providing while creating
45+
* subscription as provided in @see <a
46+
* href="https://github.com/GoogleCloudPlatform/java-docs-samples/tree/master/appengine-java8/pubsub">README</a>.
47+
*/
48+
.setAudience(Collections.singletonList("example.com"))
49+
.build();
50+
private final Gson gson = new Gson();
51+
private final JsonParser jsonParser = new JsonParser();
52+
53+
@Override
54+
public void doPost(HttpServletRequest req, HttpServletResponse resp)
55+
throws IOException, ServletException {
56+
57+
// Verify that the request originates from the application.
58+
if (req.getParameter("token").compareTo(pubsubVerificationToken) != 0) {
59+
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
60+
return;
61+
}
62+
// Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.
63+
String authorizationHeader = req.getHeader("Authorization");
64+
if (authorizationHeader == null
65+
|| authorizationHeader.isEmpty()
66+
|| authorizationHeader.split(" ").length != 2) {
67+
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
68+
return;
69+
}
70+
String authorization = authorizationHeader.split(" ")[1];
71+
72+
try {
73+
// Verify and decode the JWT.
74+
GoogleIdToken idToken = verifier.verify(authorization);
75+
messageRepository.saveToken(authorization);
76+
messageRepository.saveClaim(idToken.getPayload().toPrettyString());
77+
// parse message object from "message" field in the request body json
78+
// decode message data from base64
79+
Message message = getMessage(req);
80+
messageRepository.save(message);
81+
// 200, 201, 204, 102 status codes are interpreted as success by the Pub/Sub system
82+
resp.setStatus(102);
83+
super.doPost(req, resp);
84+
} catch (Exception e) {
85+
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
86+
}
87+
}
88+
89+
private Message getMessage(HttpServletRequest request) throws IOException {
90+
String requestBody = request.getReader().lines().collect(Collectors.joining("\n"));
91+
JsonElement jsonRoot = jsonParser.parse(requestBody);
92+
String messageStr = jsonRoot.getAsJsonObject().get("message").toString();
93+
Message message = gson.fromJson(messageStr, Message.class);
94+
// decode from base64
95+
String decoded = decode(message.getData());
96+
message.setData(decoded);
97+
return message;
98+
}
99+
100+
private String decode(String data) {
101+
return new String(Base64.getDecoder().decode(data));
102+
}
103+
104+
PubSubAuthenticatedPush(MessageRepository messageRepository) {
105+
this.messageRepository = messageRepository;
106+
}
107+
108+
public PubSubAuthenticatedPush() {
109+
this(MessageRepositoryImpl.getInstance());
110+
}
111+
}
112+
// [END gae_standard_pubsub_auth_push]

0 commit comments

Comments
 (0)