Skip to content

Commit 888c61c

Browse files
authored
feat: allow arbitrary additional claims for JwtClaims (#331)
* chore: move JwtClaims tests to their own file * feat: allow arbitrary additional claims for JwtClaims * chore: fix lint * chore: cleanup test method names * chore: update immutable map warning documentation
1 parent 08c1bea commit 888c61c

File tree

4 files changed

+204
-67
lines changed

4 files changed

+204
-67
lines changed

oauth2_http/java/com/google/auth/oauth2/JwtClaims.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
package com.google.auth.oauth2;
3333

3434
import com.google.auto.value.AutoValue;
35+
import com.google.common.collect.ImmutableMap;
3536
import java.io.Serializable;
37+
import java.util.Map;
3638
import javax.annotation.Nullable;
3739

3840
/**
@@ -61,8 +63,15 @@ public abstract class JwtClaims implements Serializable {
6163
@Nullable
6264
abstract String getSubject();
6365

66+
/**
67+
* Returns additional claims for this object. The returned map is not guaranteed to be mutable.
68+
*
69+
* @return additional claims
70+
*/
71+
abstract Map<String, String> getAdditionalClaims();
72+
6473
static Builder newBuilder() {
65-
return new AutoValue_JwtClaims.Builder();
74+
return new AutoValue_JwtClaims.Builder().setAdditionalClaims(ImmutableMap.<String, String>of());
6675
}
6776

6877
/**
@@ -74,10 +83,15 @@ static Builder newBuilder() {
7483
* @return new claims
7584
*/
7685
public JwtClaims merge(JwtClaims other) {
86+
ImmutableMap.Builder<String, String> newClaimsBuilder = ImmutableMap.builder();
87+
newClaimsBuilder.putAll(getAdditionalClaims());
88+
newClaimsBuilder.putAll(other.getAdditionalClaims());
89+
7790
return newBuilder()
7891
.setAudience(other.getAudience() == null ? getAudience() : other.getAudience())
7992
.setIssuer(other.getIssuer() == null ? getIssuer() : other.getIssuer())
8093
.setSubject(other.getSubject() == null ? getSubject() : other.getSubject())
94+
.setAdditionalClaims(newClaimsBuilder.build())
8195
.build();
8296
}
8397

@@ -103,6 +117,8 @@ abstract static class Builder {
103117

104118
abstract Builder setSubject(String subject);
105119

120+
abstract Builder setAdditionalClaims(Map<String, String> additionalClaims);
121+
106122
abstract JwtClaims build();
107123
}
108124
}

oauth2_http/java/com/google/auth/oauth2/JwtCredentials.java

+3
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ public void refresh() throws IOException {
114114
payload.setIssuedAtTimeSeconds(currentTime / 1000);
115115
payload.setExpirationTimeSeconds(currentTime / 1000 + lifeSpanSeconds);
116116

117+
// Add all additional claims
118+
payload.putAll(jwtClaims.getAdditionalClaims());
119+
117120
synchronized (lock) {
118121
this.expiryInSeconds = payload.getExpirationTimeSeconds();
119122

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2019, Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google LLC nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
package com.google.auth.oauth2;
33+
34+
import static org.junit.Assert.*;
35+
36+
import java.util.Collections;
37+
import java.util.Map;
38+
import org.junit.Test;
39+
40+
public class JwtClaimsTest {
41+
42+
@Test
43+
public void testMergeOverwritesFields() {
44+
JwtClaims claims1 =
45+
JwtClaims.newBuilder()
46+
.setAudience("audience-1")
47+
.setIssuer("issuer-1")
48+
.setSubject("subject-1")
49+
.build();
50+
JwtClaims claims2 =
51+
JwtClaims.newBuilder()
52+
.setAudience("audience-2")
53+
.setIssuer("issuer-2")
54+
.setSubject("subject-2")
55+
.build();
56+
JwtClaims merged = claims1.merge(claims2);
57+
58+
assertEquals("audience-2", merged.getAudience());
59+
assertEquals("issuer-2", merged.getIssuer());
60+
assertEquals("subject-2", merged.getSubject());
61+
}
62+
63+
@Test
64+
public void testMergeDefaultValues() {
65+
JwtClaims claims1 =
66+
JwtClaims.newBuilder()
67+
.setAudience("audience-1")
68+
.setIssuer("issuer-1")
69+
.setSubject("subject-1")
70+
.build();
71+
JwtClaims claims2 = JwtClaims.newBuilder().setAudience("audience-2").build();
72+
JwtClaims merged = claims1.merge(claims2);
73+
74+
assertEquals("audience-2", merged.getAudience());
75+
assertEquals("issuer-1", merged.getIssuer());
76+
assertEquals("subject-1", merged.getSubject());
77+
}
78+
79+
@Test
80+
public void testMergeNull() {
81+
JwtClaims claims1 = JwtClaims.newBuilder().build();
82+
JwtClaims claims2 = JwtClaims.newBuilder().build();
83+
JwtClaims merged = claims1.merge(claims2);
84+
85+
assertNull(merged.getAudience());
86+
assertNull(merged.getIssuer());
87+
assertNull(merged.getSubject());
88+
assertNotNull(merged.getAdditionalClaims());
89+
assertTrue(merged.getAdditionalClaims().isEmpty());
90+
}
91+
92+
@Test
93+
public void testEquals() {
94+
JwtClaims claims1 =
95+
JwtClaims.newBuilder()
96+
.setAudience("audience-1")
97+
.setIssuer("issuer-1")
98+
.setSubject("subject-1")
99+
.build();
100+
JwtClaims claims2 =
101+
JwtClaims.newBuilder()
102+
.setAudience("audience-1")
103+
.setIssuer("issuer-1")
104+
.setSubject("subject-1")
105+
.build();
106+
107+
assertEquals(claims1, claims2);
108+
}
109+
110+
@Test
111+
public void testAdditionalClaimsDefaults() {
112+
JwtClaims claims = JwtClaims.newBuilder().build();
113+
assertNotNull(claims.getAdditionalClaims());
114+
assertTrue(claims.getAdditionalClaims().isEmpty());
115+
}
116+
117+
@Test
118+
public void testMergeAdditionalClaims() {
119+
JwtClaims claims1 =
120+
JwtClaims.newBuilder().setAdditionalClaims(Collections.singletonMap("foo", "bar")).build();
121+
JwtClaims claims2 =
122+
JwtClaims.newBuilder()
123+
.setAdditionalClaims(Collections.singletonMap("asdf", "qwer"))
124+
.build();
125+
JwtClaims merged = claims1.merge(claims2);
126+
127+
assertNull(merged.getAudience());
128+
assertNull(merged.getIssuer());
129+
assertNull(merged.getSubject());
130+
Map<String, String> mergedAdditionalClaims = merged.getAdditionalClaims();
131+
assertNotNull(mergedAdditionalClaims);
132+
assertEquals(2, mergedAdditionalClaims.size());
133+
assertEquals("bar", mergedAdditionalClaims.get("foo"));
134+
assertEquals("qwer", mergedAdditionalClaims.get("asdf"));
135+
}
136+
}

oauth2_http/javatests/com/google/auth/oauth2/JwtCredentialsTest.java

+48-66
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import com.google.auth.http.AuthHttpConstants;
4545
import java.io.IOException;
4646
import java.security.PrivateKey;
47+
import java.util.Collections;
4748
import java.util.List;
4849
import java.util.Map;
4950
import org.junit.Test;
@@ -157,72 +158,6 @@ public void builder_requiresCompleteClaims() {
157158
}
158159
}
159160

160-
@Test
161-
public void claims_merge_overwritesFields() {
162-
JwtClaims claims1 =
163-
JwtClaims.newBuilder()
164-
.setAudience("audience-1")
165-
.setIssuer("issuer-1")
166-
.setSubject("subject-1")
167-
.build();
168-
JwtClaims claims2 =
169-
JwtClaims.newBuilder()
170-
.setAudience("audience-2")
171-
.setIssuer("issuer-2")
172-
.setSubject("subject-2")
173-
.build();
174-
JwtClaims merged = claims1.merge(claims2);
175-
176-
assertEquals("audience-2", merged.getAudience());
177-
assertEquals("issuer-2", merged.getIssuer());
178-
assertEquals("subject-2", merged.getSubject());
179-
}
180-
181-
@Test
182-
public void claims_merge_defaultValues() {
183-
JwtClaims claims1 =
184-
JwtClaims.newBuilder()
185-
.setAudience("audience-1")
186-
.setIssuer("issuer-1")
187-
.setSubject("subject-1")
188-
.build();
189-
JwtClaims claims2 = JwtClaims.newBuilder().setAudience("audience-2").build();
190-
JwtClaims merged = claims1.merge(claims2);
191-
192-
assertEquals("audience-2", merged.getAudience());
193-
assertEquals("issuer-1", merged.getIssuer());
194-
assertEquals("subject-1", merged.getSubject());
195-
}
196-
197-
@Test
198-
public void claims_merge_null() {
199-
JwtClaims claims1 = JwtClaims.newBuilder().build();
200-
JwtClaims claims2 = JwtClaims.newBuilder().build();
201-
JwtClaims merged = claims1.merge(claims2);
202-
203-
assertNull(merged.getAudience());
204-
assertNull(merged.getIssuer());
205-
assertNull(merged.getSubject());
206-
}
207-
208-
@Test
209-
public void claims_equals() {
210-
JwtClaims claims1 =
211-
JwtClaims.newBuilder()
212-
.setAudience("audience-1")
213-
.setIssuer("issuer-1")
214-
.setSubject("subject-1")
215-
.build();
216-
JwtClaims claims2 =
217-
JwtClaims.newBuilder()
218-
.setAudience("audience-1")
219-
.setIssuer("issuer-1")
220-
.setSubject("subject-1")
221-
.build();
222-
223-
assertEquals(claims1, claims2);
224-
}
225-
226161
@Test
227162
public void jwtWithClaims_overwritesClaims() throws IOException {
228163
JwtClaims claims =
@@ -287,13 +222,56 @@ public void getRequestMetadata_hasJwtAccess() throws IOException {
287222
verifyJwtAccess(metadata, "some-audience", "some-issuer", "some-subject", PRIVATE_KEY_ID);
288223
}
289224

225+
@Test
226+
public void getRequestMetadata_withAdditionalClaims_hasJwtAccess() throws IOException {
227+
JwtClaims claims =
228+
JwtClaims.newBuilder()
229+
.setAudience("some-audience")
230+
.setIssuer("some-issuer")
231+
.setSubject("some-subject")
232+
.setAdditionalClaims(Collections.singletonMap("foo", "bar"))
233+
.build();
234+
JwtCredentials credentials =
235+
JwtCredentials.newBuilder()
236+
.setJwtClaims(claims)
237+
.setPrivateKey(getPrivateKey())
238+
.setPrivateKeyId(PRIVATE_KEY_ID)
239+
.build();
240+
241+
Map<String, List<String>> metadata = credentials.getRequestMetadata();
242+
verifyJwtAccess(
243+
metadata,
244+
"some-audience",
245+
"some-issuer",
246+
"some-subject",
247+
PRIVATE_KEY_ID,
248+
Collections.singletonMap("foo", "bar"));
249+
}
250+
290251
private void verifyJwtAccess(
291252
Map<String, List<String>> metadata,
292253
String expectedAudience,
293254
String expectedIssuer,
294255
String expectedSubject,
295256
String expectedKeyId)
296257
throws IOException {
258+
verifyJwtAccess(
259+
metadata,
260+
expectedAudience,
261+
expectedIssuer,
262+
expectedSubject,
263+
expectedKeyId,
264+
Collections.<String, String>emptyMap());
265+
}
266+
267+
private void verifyJwtAccess(
268+
Map<String, List<String>> metadata,
269+
String expectedAudience,
270+
String expectedIssuer,
271+
String expectedSubject,
272+
String expectedKeyId,
273+
Map<String, String> expectedAdditionalClaims)
274+
throws IOException {
297275
assertNotNull(metadata);
298276
List<String> authorizations = metadata.get(AuthHttpConstants.AUTHORIZATION);
299277
assertNotNull("Authorization headers not found", authorizations);
@@ -310,5 +288,9 @@ private void verifyJwtAccess(
310288
assertEquals(expectedSubject, signature.getPayload().getSubject());
311289
assertEquals(expectedAudience, signature.getPayload().getAudience());
312290
assertEquals(expectedKeyId, signature.getHeader().getKeyId());
291+
292+
for (Map.Entry<String, String> entry : expectedAdditionalClaims.entrySet()) {
293+
assertEquals(entry.getValue(), signature.getPayload().get(entry.getKey()));
294+
}
313295
}
314296
}

0 commit comments

Comments
 (0)