JCLOUDS-750 Remove need for custom json type adapters on OAuth.

This commit is contained in:
Adrian Cole 2014-11-01 12:31:04 -07:00 committed by Adrian Cole
parent 35156560dc
commit eb8b154869
13 changed files with 75 additions and 250 deletions

View File

@ -25,7 +25,6 @@ import java.util.Properties;
import org.jclouds.apis.ApiMetadata;
import org.jclouds.oauth.v2.config.OAuthHttpApiModule;
import org.jclouds.oauth.v2.config.OAuthModule;
import org.jclouds.oauth.v2.config.OAuthParserModule;
import org.jclouds.rest.internal.BaseHttpApiMetadata;
import com.google.auto.service.AutoService;
@ -65,8 +64,7 @@ public class OAuthApiMetadata extends BaseHttpApiMetadata<OAuthApi> {
.documentation(URI.create("TODO"))
.version("2")
.defaultProperties(OAuthApiMetadata.defaultProperties())
.defaultModules(ImmutableSet
.<Class<? extends Module>>of(OAuthModule.class, OAuthParserModule.class, OAuthHttpApiModule.class));
.defaultModules(ImmutableSet.<Class<? extends Module>>of(OAuthModule.class, OAuthHttpApiModule.class));
}
@Override

View File

@ -1,159 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.config;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.inject.Singleton;
import org.jclouds.oauth.v2.domain.ClaimSet;
import org.jclouds.oauth.v2.domain.Header;
import org.jclouds.oauth.v2.domain.Token;
import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
/** Configures type adapter factories for {@link Header}, {@link ClaimSet}, and {@link Token}. */
public final class OAuthParserModule extends AbstractModule {
@Override protected void configure() {
}
// TODO: change jclouds core to use collaborative set bindings
@Provides @Singleton public Set<TypeAdapterFactory> typeAdapterFactories() {
return ImmutableSet
.<TypeAdapterFactory>of(new HeaderTypeAdapter(), new ClaimSetTypeAdapter(), new TokenAdapter());
}
private static final class HeaderTypeAdapter extends SubtypeAdapterFactory<Header> {
HeaderTypeAdapter() {
super(Header.class);
}
@Override public void write(JsonWriter out, Header value) throws IOException {
out.beginObject();
out.name("alg");
out.value(value.signerAlgorithm());
out.name("typ");
out.value(value.type());
out.endObject();
}
@Override public Header read(JsonReader in) throws IOException {
in.beginObject();
in.nextName();
String signerAlgorithm = in.nextString();
in.nextName();
String type = in.nextString();
in.endObject();
return Header.create(signerAlgorithm, type);
}
}
private static final class ClaimSetTypeAdapter extends SubtypeAdapterFactory<ClaimSet> {
ClaimSetTypeAdapter() {
super(ClaimSet.class);
}
@Override public void write(JsonWriter out, ClaimSet value) throws IOException {
out.beginObject();
for (Map.Entry<String, String> entry : value.claims().entrySet()) {
out.name(entry.getKey());
out.value(entry.getValue());
}
out.name("exp");
out.value(value.expirationTime());
out.name("iat");
out.value(value.emissionTime());
out.endObject();
}
@Override public ClaimSet read(JsonReader in) throws IOException {
Map<String, String> claims = new LinkedHashMap<String, String>();
in.beginObject();
while (in.hasNext()) {
claims.put(in.nextName(), in.nextString());
}
in.endObject();
return ClaimSet.create(0, 0, Collections.unmodifiableMap(claims));
}
}
/** OAuth is used in apis that may not default to snake case. Explicity control case format. */
private static final class TokenAdapter extends SubtypeAdapterFactory<Token> {
TokenAdapter() {
super(Token.class);
}
@Override public void write(JsonWriter out, Token value) throws IOException {
out.beginObject();
out.name("access_token");
out.value(value.accessToken());
out.name("token_type");
out.value(value.tokenType());
out.name("expires_in");
out.value(value.expiresIn());
out.endObject();
}
@Override public Token read(JsonReader in) throws IOException {
String accessToken = null;
String tokenType = null;
int expiresIn = 0;
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
if (name.equals("access_token")) {
accessToken = in.nextString();
} else if (name.equals("token_type")) {
tokenType = in.nextString();
} else if (name.equals("expires_in")) {
expiresIn = in.nextInt();
} else {
in.skipValue();
}
}
in.endObject();
return Token.create(accessToken, tokenType, expiresIn);
}
}
private abstract static class SubtypeAdapterFactory<T> extends TypeAdapter<T> implements TypeAdapterFactory {
private final Class<T> baseClass;
private SubtypeAdapterFactory(Class<T> baseClass) {
this.baseClass = baseClass;
}
@Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
if (!(baseClass.isAssignableFrom(typeToken.getRawType()))) {
return null;
}
return (TypeAdapter<T>) this;
}
}
}

View File

@ -1,42 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.domain;
import java.util.Map;
import com.google.auto.value.AutoValue;
/**
* The claimset for the {@linkplain Token}.
*
* @see <a href="https://developers.google.com/accounts/docs/OAuth2ServiceAccount">doc</a>
*/
@AutoValue
public abstract class ClaimSet {
/** The emission time, in seconds since the epoch. */
public abstract long emissionTime();
/** The expiration time, in seconds since the emission time. */
public abstract long expirationTime();
public abstract Map<String, String> claims();
public static ClaimSet create(long emissionTime, long expirationTime, Map<String, String> claims) {
return new AutoValue_ClaimSet(emissionTime, expirationTime, claims);
}
}

View File

@ -14,17 +14,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.internal;
package org.jclouds.oauth.v2.domain;
import org.jclouds.json.BaseItemParserTest;
import org.jclouds.json.config.GsonModule;
import org.jclouds.oauth.v2.config.OAuthParserModule;
/**
* Description of Claims corresponding to a {@linkplain Token JWT Token}.
*
* @see <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4">registered list</a>
*/
public final class Claims {
/** The time at which the JWT was issued, in seconds since the epoch. */
public static final String ISSUED_AT = "iat";
import com.google.inject.Guice;
import com.google.inject.Injector;
/** The expiration time, in seconds since {@link #ISSUED_AT}. */
public static final String EXPIRATION_TIME = "exp";
public abstract class BaseOAuthParseTest<T> extends BaseItemParserTest<T> {
@Override protected Injector injector() {
return Guice.createInjector(new GsonModule(), new OAuthParserModule());
private Claims(){
throw new AssertionError("intentionally unimplemented");
}
}

View File

@ -16,6 +16,8 @@
*/
package org.jclouds.oauth.v2.domain;
import org.jclouds.json.SerializedNames;
import com.google.auto.value.AutoValue;
/**
@ -32,6 +34,7 @@ public abstract class Header {
/** The type of the token, e.g., {@code JWT}. */
public abstract String type();
@SerializedNames({ "alg", "typ" })
public static Header create(String signerAlgorithm, String type){
return new AutoValue_Header(signerAlgorithm, type);
}

View File

@ -16,6 +16,8 @@
*/
package org.jclouds.oauth.v2.domain;
import org.jclouds.json.SerializedNames;
import com.google.auto.value.AutoValue;
/**
@ -32,8 +34,8 @@ public abstract class Token {
/** In how many seconds this token expires. */
public abstract long expiresIn();
public static Token
create(String accessToken, String tokenType, long expiresIn) {
@SerializedNames({"access_token", "token_type", "expires_in"})
public static Token create(String accessToken, String tokenType, long expiresIn) {
return new AutoValue_Token(accessToken, tokenType, expiresIn);
}
}

View File

@ -16,14 +16,16 @@
*/
package org.jclouds.oauth.v2.domain;
import java.util.Map;
import com.google.auto.value.AutoValue;
@AutoValue
public abstract class TokenRequest {
public abstract Header header();
public abstract ClaimSet claimSet();
public abstract Map<String, Object> claimSet();
public static TokenRequest create(Header header, ClaimSet claimSet) {
return new AutoValue_TokenRequest(header, claimSet);
public static TokenRequest create(Header header, Map<String, Object> claims) {
return new AutoValue_TokenRequest(header, claims);
}
}

View File

@ -21,15 +21,15 @@ import static org.jclouds.oauth.v2.OAuthConstants.ADDITIONAL_CLAIMS;
import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE;
import static org.jclouds.oauth.v2.config.OAuthProperties.SCOPES;
import static org.jclouds.oauth.v2.config.OAuthProperties.SIGNATURE_OR_MAC_ALGORITHM;
import static org.jclouds.oauth.v2.domain.Claims.EXPIRATION_TIME;
import static org.jclouds.oauth.v2.domain.Claims.ISSUED_AT;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.jclouds.Constants;
import org.jclouds.oauth.v2.config.OAuthScopes;
import org.jclouds.oauth.v2.domain.ClaimSet;
import org.jclouds.oauth.v2.domain.Header;
import org.jclouds.oauth.v2.domain.OAuthCredentials;
import org.jclouds.oauth.v2.domain.TokenRequest;
@ -38,7 +38,6 @@ import org.jclouds.rest.internal.GeneratedHttpRequest;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.reflect.Invokable;
import com.google.inject.Inject;
import com.google.inject.name.Named;
@ -51,9 +50,6 @@ import com.google.inject.name.Named;
* TODO scopes etc should come from the REST method and not from a global property
*/
public final class BuildTokenRequest implements Function<GeneratedHttpRequest, TokenRequest> {
// exp and ist (expiration and emission times) are assumed mandatory already
private static final List<String> REQUIRED_CLAIMS = ImmutableList.of("iss", "scope", "aud");
private final String assertionTargetDescription;
private final String signatureAlgorithm;
private final Supplier<OAuthCredentials> credentialsSupplier;
@ -92,18 +88,15 @@ public final class BuildTokenRequest implements Function<GeneratedHttpRequest, T
// fetch the token
Header header = Header.create(signatureAlgorithm, "JWT");
Map<String, String> claims = new LinkedHashMap<String, String>();
Map<String, Object> claims = new LinkedHashMap<String, Object>();
claims.put("iss", credentialsSupplier.get().identity);
claims.put("scope", getOAuthScopes(request));
claims.put("aud", assertionTargetDescription);
claims.put(EXPIRATION_TIME, now + tokenDuration);
claims.put(ISSUED_AT, now);
claims.putAll(additionalClaims);
checkState(claims.keySet().containsAll(REQUIRED_CLAIMS),
"not all required claims were present");
ClaimSet claimSet = ClaimSet.create(now, now + tokenDuration, Collections.unmodifiableMap(claims));
return TokenRequest.create(header, claimSet);
return TokenRequest.create(header, claims);
}
private String getOAuthScopes(GeneratedHttpRequest request) {

View File

@ -16,17 +16,19 @@
*/
package org.jclouds.oauth.v2.binders;
import static org.jclouds.oauth.v2.domain.Claims.EXPIRATION_TIME;
import static org.jclouds.oauth.v2.domain.Claims.ISSUED_AT;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
import java.io.IOException;
import java.util.Map;
import org.jclouds.ContextBuilder;
import org.jclouds.http.HttpRequest;
import org.jclouds.oauth.v2.OAuthApiMetadata;
import org.jclouds.oauth.v2.OAuthTestUtils;
import org.jclouds.oauth.v2.domain.ClaimSet;
import org.jclouds.oauth.v2.domain.Header;
import org.jclouds.oauth.v2.domain.TokenRequest;
import org.jclouds.util.Strings2;
@ -48,9 +50,13 @@ public class OAuthTokenBinderTest {
(OAuthTestUtils.defaultProperties(null)).build().utils()
.injector().getInstance(OAuthTokenBinder.class);
Header header = Header.create("a", "b");
ClaimSet claimSet = ClaimSet.create(0, 0,
ImmutableMap.of("ist", STRING_THAT_GENERATES_URL_UNSAFE_BASE64_ENCODING));
TokenRequest tokenRequest = TokenRequest.create(header, claimSet);
Map<String, Object> claims = ImmutableMap.<String, Object>builder()
.put(ISSUED_AT, 0)
.put(EXPIRATION_TIME, 0)
.put("ist", STRING_THAT_GENERATES_URL_UNSAFE_BASE64_ENCODING).build();
TokenRequest tokenRequest = TokenRequest.create(header, claims);
HttpRequest request = tokenRequestFormat.bindToRequest(
HttpRequest.builder().method("GET").endpoint("http://localhost").build(), tokenRequest);

View File

@ -21,13 +21,15 @@ import static org.jclouds.oauth.v2.OAuthTestUtils.getMandatoryProperty;
import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE;
import static org.jclouds.oauth.v2.config.OAuthProperties.SCOPES;
import static org.jclouds.oauth.v2.config.OAuthProperties.SIGNATURE_OR_MAC_ALGORITHM;
import static org.jclouds.oauth.v2.domain.Claims.EXPIRATION_TIME;
import static org.jclouds.oauth.v2.domain.Claims.ISSUED_AT;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import java.util.Map;
import java.util.Properties;
import org.jclouds.oauth.v2.OAuthConstants;
import org.jclouds.oauth.v2.domain.ClaimSet;
import org.jclouds.oauth.v2.domain.Header;
import org.jclouds.oauth.v2.domain.Token;
import org.jclouds.oauth.v2.domain.TokenRequest;
@ -71,10 +73,14 @@ public class OAuthApiLiveTest extends BaseOAuthApiLiveTest {
long now = nowInSeconds();
ClaimSet claimSet = ClaimSet.create(now, now + 3600,
ImmutableMap.of("aud", audience, "scope", scopes, "iss", identity));
Map<String, Object> claims = ImmutableMap.<String, Object>builder()
.put("iss", identity)
.put("scope", scopes)
.put("aud", audience)
.put(EXPIRATION_TIME, now + 3600)
.put(ISSUED_AT, now).build();
TokenRequest tokenRequest = TokenRequest.create(header, claimSet);
TokenRequest tokenRequest = TokenRequest.create(header, claims);
Token token = api.authenticate(tokenRequest);
assertNotNull(token, "no token when authenticating " + tokenRequest);

View File

@ -22,11 +22,14 @@ import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.jclouds.Constants.PROPERTY_MAX_RETRIES;
import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE;
import static org.jclouds.oauth.v2.domain.Claims.EXPIRATION_TIME;
import static org.jclouds.oauth.v2.domain.Claims.ISSUED_AT;
import static org.jclouds.util.Strings2.toStringAndClose;
import static org.testng.Assert.assertEquals;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
import java.util.Properties;
import org.jclouds.ContextBuilder;
@ -34,7 +37,6 @@ import org.jclouds.concurrent.config.ExecutorServiceModule;
import org.jclouds.oauth.v2.OAuthApi;
import org.jclouds.oauth.v2.OAuthApiMetadata;
import org.jclouds.oauth.v2.OAuthTestUtils;
import org.jclouds.oauth.v2.domain.ClaimSet;
import org.jclouds.oauth.v2.domain.Header;
import org.jclouds.oauth.v2.domain.Token;
import org.jclouds.oauth.v2.domain.TokenRequest;
@ -61,9 +63,12 @@ public class OAuthApiMockTest {
private static final Token TOKEN = Token.create("1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M", "Bearer", 3600);
private static final ClaimSet CLAIM_SET = ClaimSet.create(1328569781, 1328573381, ImmutableMap
.of("iss", "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com", "scope",
"https://www.googleapis.com/auth/prediction", "aud", "https://accounts.google.com/o/oauth2/token"));
private static final Map<String, Object> CLAIMS = ImmutableMap.<String, Object>builder()
.put("iss", "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com")
.put("scope", "https://www.googleapis.com/auth/prediction")
.put("aud", "https://accounts.google.com/o/oauth2/token")
.put(EXPIRATION_TIME, 1328573381)
.put(ISSUED_AT, 1328569781).build();
private static final Header HEADER = Header.create("RS256", "JWT");
@ -78,7 +83,7 @@ public class OAuthApiMockTest {
OAuthApi api = api(server.getUrl("/"));
assertEquals(api.authenticate(TokenRequest.create(HEADER, CLAIM_SET)), TOKEN);
assertEquals(api.authenticate(TokenRequest.create(HEADER, CLAIMS)), TOKEN);
RecordedRequest request = server.takeRequest();
assertEquals(request.getMethod(), "POST");

View File

@ -15,22 +15,25 @@
* limitations under the License.
*/
package org.jclouds.oauth.v2.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE;
import static org.jclouds.oauth.v2.config.OAuthProperties.SIGNATURE_OR_MAC_ALGORITHM;
import static org.jclouds.oauth.v2.domain.Claims.EXPIRATION_TIME;
import static org.jclouds.oauth.v2.domain.Claims.ISSUED_AT;
import static org.testng.Assert.assertNotNull;
import java.io.Closeable;
import java.util.Map;
import java.util.Properties;
import org.jclouds.apis.BaseApiLiveTest;
import org.jclouds.config.ValueOfConfigurationKeyOrNull;
import org.jclouds.oauth.v2.OAuthApi;
import org.jclouds.oauth.v2.OAuthConstants;
import org.jclouds.oauth.v2.domain.ClaimSet;
import org.jclouds.oauth.v2.domain.Header;
import org.jclouds.oauth.v2.domain.Token;
import org.jclouds.oauth.v2.domain.TokenRequest;
@ -80,10 +83,14 @@ public abstract class BaseOAuthAuthenticatedApiLiveTest<A extends Closeable> ext
long now = SECONDS.convert(System.currentTimeMillis(), MILLISECONDS);
ClaimSet claimSet = ClaimSet.create(now, now + 3600,
ImmutableMap.of("aud", audience, "scope", scopes, "iss", identity));
Map<String, Object> claims = ImmutableMap.<String, Object>builder()
.put("iss", identity)
.put("scope", scopes)
.put("aud", audience)
.put(EXPIRATION_TIME, now + 3600)
.put(ISSUED_AT, now).build();
TokenRequest tokenRequest = TokenRequest.create(header, claimSet);
TokenRequest tokenRequest = TokenRequest.create(header, claims);
Token token = oauthApi.authenticate(tokenRequest);

View File

@ -20,12 +20,12 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import javax.ws.rs.Consumes;
import org.jclouds.json.BaseItemParserTest;
import org.jclouds.oauth.v2.domain.Token;
import org.jclouds.oauth.v2.internal.BaseOAuthParseTest;
import org.testng.annotations.Test;
@Test(groups = "unit", testName = "ParseTokenTest")
public class ParseTokenTest extends BaseOAuthParseTest<Token> {
public class ParseTokenTest extends BaseItemParserTest<Token> {
@Override
public String resource() {