Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend type support on JWT claims #451

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
126 changes: 120 additions & 6 deletions oauth2_http/java/com/google/auth/oauth2/JwtClaims.java
Expand Up @@ -31,9 +31,14 @@

package com.google.auth.oauth2;

import com.google.api.client.util.Preconditions;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.Serializable;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

Expand Down Expand Up @@ -68,10 +73,10 @@ public abstract class JwtClaims implements Serializable {
*
* @return additional claims
*/
abstract Map<String, String> getAdditionalClaims();
abstract Map<String, ?> getAdditionalClaims();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a breaking change. It should be possible to keep the old one marked as Deprecated and calling this new signature internally


public static Builder newBuilder() {
return new AutoValue_JwtClaims.Builder().setAdditionalClaims(ImmutableMap.<String, String>of());
return new AutoValue_JwtClaims.Builder().setAdditionalClaims(ImmutableMap.<String, Object>of());
}

/**
Expand All @@ -83,7 +88,7 @@ public static Builder newBuilder() {
* @return new claims
*/
public JwtClaims merge(JwtClaims other) {
ImmutableMap.Builder<String, String> newClaimsBuilder = ImmutableMap.builder();
ImmutableMap.Builder<String, Object> newClaimsBuilder = ImmutableMap.builder();
newClaimsBuilder.putAll(getAdditionalClaims());
newClaimsBuilder.putAll(other.getAdditionalClaims());

Expand Down Expand Up @@ -111,14 +116,123 @@ public boolean isComplete() {

@AutoValue.Builder
public abstract static class Builder {
/** Basic types supported by JSON standard. */
private static List<Class<? extends Serializable>> SUPPORTED_BASIC_TYPES =
ImmutableList.of(
String.class,
Integer.class,
Double.class,
Float.class,
Boolean.class,
Date.class,
String[].class,
Integer[].class,
Double[].class,
Float[].class,
Boolean[].class,
Date[].class);

private static final String ERROR_MESSAGE =
"Invalid type on additional claims. Valid types are String, Integer, "
+ "Double, Float, Boolean, Date, List and Map. Map keys must be Strings.";

public abstract Builder setAudience(String audience);

public abstract Builder setIssuer(String issuer);

public abstract Builder setSubject(String subject);

public abstract Builder setAdditionalClaims(Map<String, String> additionalClaims);

public abstract JwtClaims build();
public abstract Builder setAdditionalClaims(Map<String, ?> additionalClaims);

protected abstract JwtClaims autoBuild();

public JwtClaims build() {
JwtClaims claims = autoBuild();
Preconditions.checkState(validateClaims(claims.getAdditionalClaims()), ERROR_MESSAGE);
return claims;
}

/**
* Validate if the objects on a Map are valid for a JWT claim.
*
* @param claims Map of claim objects to be validated
*/
private static boolean validateClaims(Map<String, ?> claims) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting code. I wonder if there's a library for this somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked a fellow programmer friends who are more experience than me on Java before coming up with this and they didn't know of any.

I ultimately got inspired by the logic used on java-jwt though I wanted to provide a more robust support than that implementation.

Open to any suggestions you may have on how to improve this code!

if (!validateKeys(claims)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really required here given that the key type is explicitly defined as String?

return false;
}

for (Object claim : claims.values()) {
if (!validateObject(claim)) {
return false;
}
}

return true;
}

/**
* Validates if the object is a valid JSON supported type.
*
* @param object to be evaluated
*/
private static final boolean validateObject(@Nullable Object object) {
// According to JSON spec, null is a valid value.
if (object == null) {
return true;
}

if (object instanceof List) {
return validateCollection((List) object);
} else if (object instanceof Map) {
return validateKeys((Map) object) && validateCollection(((Map) object).values());
}

return isSupportedValue(object);
}

/**
* Validates the keys on a given map. Keys must be Strings.
*
* @param map map to be evaluated
*/
private static final boolean validateKeys(Map map) {
for (Object key : map.keySet()) {
if (!(key instanceof String)) {
return false;
}
}

return true;
}

/**
* Validates if a collection is a valid JSON value. Empty collections are considered valid.
*
* @param collection collection to be evaluated
*/
private static final boolean validateCollection(Collection collection) {
if (collection.isEmpty()) {
return true;
}

for (Object item : collection) {
if (!validateObject(item)) {
return false;
}
}

return true;
}

/**
* Validates if the given object is an instance of a valid JSON basic type.
*
* @param value object to be evaluated.
*/
private static final boolean isSupportedValue(Object value) {
Class clazz = value.getClass();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can remove this and pass into contains directly? since its trivial and not reused

return SUPPORTED_BASIC_TYPES.contains(clazz);
}
}
}
59 changes: 56 additions & 3 deletions oauth2_http/javatests/com/google/auth/oauth2/JwtClaimsTest.java
Expand Up @@ -31,11 +31,21 @@

package com.google.auth.oauth2;

import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;

public class JwtClaimsTest {

Expand Down Expand Up @@ -114,6 +124,43 @@ public void testAdditionalClaimsDefaults() {
assertTrue(claims.getAdditionalClaims().isEmpty());
}

@Test
public void testComplexAdditionalClaims() {
Map<String, String> map = ImmutableMap.of("aaa", "bbb");

Map<String, Object> complexClaims = new HashMap();
complexClaims.put("foo", "bar");
complexClaims.put("abc", 123);
complexClaims.put("def", 12.3);
complexClaims.put("ghi", map);
complexClaims.put("jkl", ImmutableList.of(1, 2, 3));
complexClaims.put("mno", new Date());

JwtClaims claims = JwtClaims.newBuilder().setAdditionalClaims(complexClaims).build();

Map<String, ?> additionalClaims = claims.getAdditionalClaims();
assertEquals(additionalClaims.size(), 6);
assertEquals(additionalClaims.get("ghi"), map);
}

@Test
public void testValidateAdditionalClaims() {
Map<String, Object> complexClaims = new HashMap<>();
complexClaims.put("abc", new HashSet<>());

final JwtClaims.Builder claimsBuilder =
JwtClaims.newBuilder().setAdditionalClaims(complexClaims);

assertThrows(
IllegalStateException.class,
new ThrowingRunnable() {
@Override
public void run() {
claimsBuilder.build();
}
});
}

@Test
public void testMergeAdditionalClaims() {
JwtClaims claims1 =
Expand All @@ -124,13 +171,19 @@ public void testMergeAdditionalClaims() {
.build();
JwtClaims merged = claims1.merge(claims2);

Map<String, String> value = Collections.singletonMap("key2", "val2");
JwtClaims claims3 =
JwtClaims.newBuilder().setAdditionalClaims(Collections.singletonMap("def", value)).build();
JwtClaims complexMerged = merged.merge(claims3);

assertNull(merged.getAudience());
assertNull(merged.getIssuer());
assertNull(merged.getSubject());
Map<String, String> mergedAdditionalClaims = merged.getAdditionalClaims();
Map<String, ?> mergedAdditionalClaims = complexMerged.getAdditionalClaims();
assertNotNull(mergedAdditionalClaims);
assertEquals(2, mergedAdditionalClaims.size());
assertEquals(3, mergedAdditionalClaims.size());
assertEquals("bar", mergedAdditionalClaims.get("foo"));
assertEquals("qwer", mergedAdditionalClaims.get("asdf"));
assertEquals(value, mergedAdditionalClaims.get("def"));
}
}