From fa27c74373f54aead5b15a1c434edd59ff68d10e Mon Sep 17 00:00:00 2001 From: David Ribeiro Alves Date: Sun, 28 Oct 2012 03:03:26 -0500 Subject: [PATCH] oauth v2 implementation --- labs/oauth/README | 22 ++ labs/oauth/pom.xml | 123 +++++++++++ .../java/org/jclouds/oauth/v2/OAuthApi.java | 49 +++++ .../jclouds/oauth/v2/OAuthApiMetadata.java | 93 +++++++++ .../org/jclouds/oauth/v2/OAuthAsyncApi.java | 56 +++++ .../org/jclouds/oauth/v2/OAuthConstants.java | 78 +++++++ .../oauth/v2/config/Authentication.java | 38 ++++ .../v2/config/OAuthAuthenticationModule.java | 55 +++++ .../jclouds/oauth/v2/config/OAuthModule.java | 87 ++++++++ .../oauth/v2/config/OAuthProperties.java | 48 +++++ .../v2/config/OAuthRestClientModule.java | 36 ++++ .../jclouds/oauth/v2/config/OAuthScopes.java | 45 ++++ .../org/jclouds/oauth/v2/domain/ClaimSet.java | 195 ++++++++++++++++++ .../org/jclouds/oauth/v2/domain/Header.java | 131 ++++++++++++ .../oauth/v2/domain/OAuthCredentials.java | 132 ++++++++++++ .../org/jclouds/oauth/v2/domain/Token.java | 153 ++++++++++++++ .../jclouds/oauth/v2/domain/TokenRequest.java | 135 ++++++++++++ .../oauth/v2/domain/TokenRequestFormat.java | 49 +++++ .../oauth/v2/filters/OAuthAuthenticator.java | 67 ++++++ .../oauth/v2/functions/BuildTokenRequest.java | 131 ++++++++++++ .../oauth/v2/functions/FetchToken.java | 47 +++++ .../functions/OAuthCredentialsSupplier.java | 91 ++++++++ .../functions/SignOrProduceMacForToken.java | 123 +++++++++++ .../oauth/v2/handlers/OAuthErrorHandler.java | 68 ++++++ .../oauth/v2/handlers/OAuthTokenBinder.java | 49 +++++ .../oauth/v2/json/ClaimSetTypeAdapter.java | 63 ++++++ .../oauth/v2/json/HeaderTypeAdapter.java | 54 +++++ .../oauth/v2/json/JWTTokenRequestFormat.java | 101 +++++++++ .../services/org.jclouds.apis.ApiMetadata | 1 + ...OauthAuthenticatedRestContextLiveTest.java | 113 ++++++++++ .../oauth/v2/OAuthApiMetadataTest.java | 42 ++++ .../org/jclouds/oauth/v2/OAuthTestUtils.java | 81 ++++++++ .../oauth/v2/features/OAuthApiExpectTest.java | 102 +++++++++ .../oauth/v2/features/OAuthApiLiveTest.java | 95 +++++++++ .../functions/OAuthCredentialsFromPKTest.java | 58 ++++++ .../v2/functions/SignerFunctionTest.java | 65 ++++++ .../v2/handlers/OAuthErrorHandlerTest.java | 97 +++++++++ .../oauth/v2/internal/Base64UrlSafeTest.java | 56 +++++ .../v2/internal/BaseOAuthApiExpectTest.java | 28 +++ .../v2/internal/BaseOAuthApiLiveTest.java | 71 +++++++ .../internal/BaseOAuthAsyncApiExpectTest.java | 36 ++++ .../v2/internal/BaseOAuthExpectTest.java | 31 +++ .../v2/json/JWTTokenRequestFormatTest.java | 73 +++++++ .../oauth/v2/parse/ParseTokenTest.java | 46 +++++ labs/oauth/src/test/resources/logback.xml | 38 ++++ labs/oauth/src/test/resources/testpk.pem | 15 ++ .../src/test/resources/tokenResponse.json | 5 + labs/pom.xml | 6 +- 48 files changed, 3375 insertions(+), 3 deletions(-) create mode 100644 labs/oauth/README create mode 100644 labs/oauth/pom.xml create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthApi.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthApiMetadata.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthAsyncApi.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthConstants.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/config/Authentication.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthAuthenticationModule.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthRestClientModule.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthScopes.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClaimSet.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/Header.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/OAuthCredentials.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/Token.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/TokenRequest.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/TokenRequestFormat.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/filters/OAuthAuthenticator.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/BuildTokenRequest.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/FetchToken.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/OAuthCredentialsSupplier.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/SignOrProduceMacForToken.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/handlers/OAuthErrorHandler.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/handlers/OAuthTokenBinder.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/json/ClaimSetTypeAdapter.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/json/HeaderTypeAdapter.java create mode 100644 labs/oauth/src/main/java/org/jclouds/oauth/v2/json/JWTTokenRequestFormat.java create mode 100644 labs/oauth/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/BaseOauthAuthenticatedRestContextLiveTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/OAuthApiMetadataTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/OAuthTestUtils.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/features/OAuthApiExpectTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/features/OAuthApiLiveTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/functions/OAuthCredentialsFromPKTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/functions/SignerFunctionTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/handlers/OAuthErrorHandlerTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/Base64UrlSafeTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthApiExpectTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthApiLiveTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthAsyncApiExpectTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthExpectTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/json/JWTTokenRequestFormatTest.java create mode 100644 labs/oauth/src/test/java/org/jclouds/oauth/v2/parse/ParseTokenTest.java create mode 100644 labs/oauth/src/test/resources/logback.xml create mode 100644 labs/oauth/src/test/resources/testpk.pem create mode 100644 labs/oauth/src/test/resources/tokenResponse.json diff --git a/labs/oauth/README b/labs/oauth/README new file mode 100644 index 0000000000..32b571a421 --- /dev/null +++ b/labs/oauth/README @@ -0,0 +1,22 @@ +In order to use oauth applications must specify the following properties: + +Mandatory: +.identity - the oauth identity (e.g., service account email in Google Api's) +.credential - the private key used to sign requests, in pem format +oauth.endpoint - the endpoint to use for authentication (e.g., "http://accounts.google.com/o/oauth2/token" in Google Api's) +oauth.audience - the "audience" of the token request (e.g., "http://accounts.google.com/o/oauth2/token" in Google Api's) + +Optional: +- each application may expose a Map of additional claims to be added to the token request, +these should be annotated/named with "oauth.additional-claims" +oauth.signature-or-mac-algorithm - the algorithms to use when signing the token request. + +Running the live test: + +mvn clean install -Plive\ + -Dtest.oauth.identity=\ + -Dtest.oauth.credential=\ + -Dtest.oauth.endpoint=https://accounts.google.com/o/oauth2/token\ + -Dtest.jclouds.oauth.audience=https://accounts.google.com/o/oauth2/token\ + -Dtest.jclouds.oauth.signature-or-mac-algorithm=RS256\ + -Dtest.jclouds.oauth.scopes=https://www.googleapis.com/auth/prediction \ No newline at end of file diff --git a/labs/oauth/pom.xml b/labs/oauth/pom.xml new file mode 100644 index 0000000000..7598503860 --- /dev/null +++ b/labs/oauth/pom.xml @@ -0,0 +1,123 @@ + + + + 4.0.0 + + org.jclouds + jclouds-project + 1.6.0-SNAPSHOT + ../../project/pom.xml + + org.jclouds.labs + oauth + jclouds OAuth core + jclouds components to access OAuth + + + 1.6.0-SNAPSHOT + FIX_ME + FIX_ME + FIX_ME + FIX_ME + FIX_ME + FIX_ME + 2 + + + + + + org.jclouds + jclouds-core + ${jclouds.version} + + + org.jclouds + jclouds-core + ${jclouds.version} + test-jar + test + + + org.jclouds.driver + jclouds-slf4j + ${jclouds.version} + test + + + ch.qos.logback + logback-classic + test + + + + + + + maven-jar-plugin + + + + test-jar + + + + + + + + + + live + + + + org.apache.maven.plugins + maven-surefire-plugin + + + integration + integration-test + + test + + + + ${test.oauth.identity} + ${test.oauth.credential} + ${test.oauth.endpoint} + ${test.oauth.api-version} + ${test.oauth.build-version} + ${test.jclouds.oauth.signature-or-mac-algorithm>} + ${test.jclouds.oauth.audience} + ${test.jclouds.oauth.scopes} + + + + + + + + + + diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthApi.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthApi.java new file mode 100644 index 0000000000..fd0121fae2 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthApi.java @@ -0,0 +1,49 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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; + +import org.jclouds.concurrent.Timeout; +import org.jclouds.oauth.v2.domain.Token; +import org.jclouds.oauth.v2.domain.TokenRequest; +import org.jclouds.rest.AuthorizationException; + +import java.util.concurrent.TimeUnit; + +/** + * Provides synchronous access to OAuth. + *

+ * + * @author David Alves + * @see OAuthAsyncApi + */ +@Timeout(duration = 60, timeUnit = TimeUnit.SECONDS) +public interface OAuthApi { + + /** + * Authenticates/Authorizes access to a resource defined in TokenRequest against an OAuth v2 + * authentication/authorization server. + * + * @param tokenRequest specified the principal and the required permissions + * @return a Token object with the token required to access the resource along with its expiration time + * @throws AuthorizationException if the principal cannot be authenticated or has no permissions for the specifed + * resources. + */ + public Token authenticate(TokenRequest tokenRequest) throws AuthorizationException; + +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthApiMetadata.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthApiMetadata.java new file mode 100644 index 0000000000..bce3f3f886 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthApiMetadata.java @@ -0,0 +1,93 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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; + +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.TypeToken; +import com.google.inject.Module; +import org.jclouds.apis.ApiMetadata; +import org.jclouds.oauth.v2.config.OAuthModule; +import org.jclouds.oauth.v2.config.OAuthRestClientModule; +import org.jclouds.rest.RestContext; +import org.jclouds.rest.internal.BaseRestApiMetadata; + +import java.net.URI; +import java.util.Properties; + +import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL; +import static org.jclouds.oauth.v2.config.OAuthProperties.SIGNATURE_OR_MAC_ALGORITHM; + +/** + * Implementation of {@link ApiMetadata} for OAuth 2 API + * + * @author David Alves + */ +public class OAuthApiMetadata extends BaseRestApiMetadata { + + public static final TypeToken> CONTEXT_TOKEN = new + TypeToken>() {}; + + @Override + public Builder toBuilder() { + return new Builder().fromApiMetadata(this); + } + + public OAuthApiMetadata() { + this(new Builder()); + } + + protected OAuthApiMetadata(Builder builder) { + super(builder); + } + + public static Properties defaultProperties() { + Properties properties = BaseRestApiMetadata.defaultProperties(); + properties.put(SIGNATURE_OR_MAC_ALGORITHM, "RS256"); + properties.put(PROPERTY_SESSION_INTERVAL, 3600); + return properties; + } + + public static class Builder extends BaseRestApiMetadata.Builder { + + protected Builder() { + super(OAuthApi.class, OAuthAsyncApi.class); + id("oauth").name("OAuth API") + .identityName("service_account") + .credentialName("service_key") + .documentation(URI.create("TODO")) + .version("2") + .defaultProperties(OAuthApiMetadata.defaultProperties()) + .defaultModules(ImmutableSet.>of(OAuthModule.class, OAuthRestClientModule + .class)); + } + + @Override + public OAuthApiMetadata build() { + return new OAuthApiMetadata(this); + } + + @Override + public Builder fromApiMetadata(ApiMetadata in) { + super.fromApiMetadata(in); + return this; + } + + } + +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthAsyncApi.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthAsyncApi.java new file mode 100644 index 0000000000..1156bd693b --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthAsyncApi.java @@ -0,0 +1,56 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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; + +import com.google.common.util.concurrent.ListenableFuture; +import org.jclouds.oauth.v2.config.Authentication; +import org.jclouds.oauth.v2.domain.Token; +import org.jclouds.oauth.v2.domain.TokenRequest; +import org.jclouds.oauth.v2.handlers.OAuthTokenBinder; +import org.jclouds.rest.AuthorizationException; +import org.jclouds.rest.annotations.BinderParam; +import org.jclouds.rest.annotations.Endpoint; +import org.jclouds.rest.annotations.SkipEncoding; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.core.MediaType; + +/** + * Provides asynchronous access to OAuth via REST api. + *

+ * Usually this is not directly used by a client, which instead specifies OAuthAuthenticator as a request filter, + * which in turn uses this class to perform token requests. + * + * @author David Alves + * @see OAuthAsyncApi + */ +@SkipEncoding({'/', '='}) +@Endpoint(Authentication.class) +public interface OAuthAsyncApi { + + /** + * @see OAuthApi#authenticate(TokenRequest) + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + public ListenableFuture authenticate(@BinderParam(OAuthTokenBinder.class) TokenRequest tokenRequest) + throws AuthorizationException; + +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthConstants.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthConstants.java new file mode 100644 index 0000000000..59fc43031c --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/OAuthConstants.java @@ -0,0 +1,78 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +/** + * The constants for OAuth \ + * + * @author David Alves + */ +public class OAuthConstants { + + /** + * Selected algorithm when a signature or mac isn't required. + */ + public static final String NO_ALGORITHM = "none"; + + /** + * Static mapping between the oauth algorithm name and the Crypto provider signature algorithm name. + * + * @see doc + * @see org.jclouds.oauth.v2.json.JWTTokenRequestFormat + */ + public static final Map OAUTH_ALGORITHM_NAMES_TO_SIGNATURE_ALGORITHM_NAMES = ImmutableMap + .builder() + .put("RS256", "SHA256withRSA") + .put("RS384", "SHA384withRSA") + .put("RS512", "SHA512withRSA") + .put("HS256", "HmacSHA256") + .put("HS384", "HmacSHA384") + .put("HS512", "HmacSHA512") + .put("ES256", "SHA256withECDSA") + .put("ES384", "SHA384withECDSA") + .put("ES512", "SHA512withECDSA") + .put(NO_ALGORITHM, NO_ALGORITHM).build(); + + /** + * Static mapping between the oauth algorithm name and the Crypto provider KeyFactory algorithm name. + * + * @see doc + */ + public static final Map OAUTH_ALGORITHM_NAMES_TO_KEYFACTORY_ALGORITHM_NAMES = ImmutableMap + .builder() + .put("RS256", "RSA") + .put("RS384", "RSA") + .put("RS512", "RSA") + .put("HS256", "DiffieHellman") + .put("HS384", "DiffieHellman") + .put("HS512", "DiffieHellman") + .put("ES256", "EC") + .put("ES384", "EC") + .put("ES512", "EC") + .put(NO_ALGORITHM, NO_ALGORITHM).build(); + + /** + * The (optional) set of additional claims to use, provided in Map form + */ + public static final String ADDITIONAL_CLAIMS = "jclouds.oauth.additional-claims"; +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/Authentication.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/Authentication.java new file mode 100644 index 0000000000..b0b301d435 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/Authentication.java @@ -0,0 +1,38 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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 javax.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Qualifies OAuth related resources, such as Endpoint. + * + * @author David Alves + * @see org.jclouds.oauth.v2.OAuthAsyncApi + */ +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = {ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Qualifier +public @interface Authentication { +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthAuthenticationModule.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthAuthenticationModule.java new file mode 100644 index 0000000000..7deab11b4e --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthAuthenticationModule.java @@ -0,0 +1,55 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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 com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import org.jclouds.oauth.v2.OAuthApi; +import org.jclouds.oauth.v2.OAuthAsyncApi; + +import javax.inject.Named; +import javax.inject.Singleton; +import java.net.URI; + +import static org.jclouds.rest.config.BinderUtils.bindClientAndAsyncClient; + +/** + * An OAuth module to be used form other providers. + * + * @author David Alves + */ +public class OAuthAuthenticationModule extends AbstractModule { + + @Override + protected void configure() { + bindClientAndAsyncClient(binder(), OAuthApi.class, OAuthAsyncApi.class); + } + + /** + * When oauth is used as a module the oauth endpoint is a normal property + */ + @Provides + @Singleton + @Authentication + protected Supplier provideAuthenticationEndpoint(@Named("oauth.endpoint") String endpoint) { + return Suppliers.ofInstance(URI.create(endpoint)); + } +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java new file mode 100644 index 0000000000..fc55a87da8 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java @@ -0,0 +1,87 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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 com.google.common.base.Function; +import com.google.common.base.Supplier; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.TypeLiteral; +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.Token; +import org.jclouds.oauth.v2.domain.TokenRequest; +import org.jclouds.oauth.v2.functions.BuildTokenRequest; +import org.jclouds.oauth.v2.functions.FetchToken; +import org.jclouds.oauth.v2.functions.OAuthCredentialsSupplier; +import org.jclouds.oauth.v2.functions.SignOrProduceMacForToken; +import org.jclouds.oauth.v2.json.ClaimSetTypeAdapter; +import org.jclouds.oauth.v2.json.HeaderTypeAdapter; +import org.jclouds.rest.internal.GeneratedHttpRequest; + +import javax.inject.Named; +import javax.inject.Singleton; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL; + +/** + * Base OAuth module + * + * @author David Alves + */ +public class OAuthModule extends AbstractModule { + + + @Override + protected void configure() { + bind(new TypeLiteral>() {}).to(SignOrProduceMacForToken.class); + bind(new TypeLiteral>() {}).toInstance(ImmutableMap.of( + Header.class, new HeaderTypeAdapter(), + ClaimSet.class, new ClaimSetTypeAdapter())); + bind(new TypeLiteral>() {}).to(OAuthCredentialsSupplier.class); + bind(new TypeLiteral>() {}).to(BuildTokenRequest.class); + bind(new TypeLiteral>() {}).to(FetchToken.class); + } + + /** + * Provides a cache for tokens. Cache is time based and expires after 59 minutes (the maximum time a token is + * valid is 60 minutes) + */ + @Provides + @Singleton + public LoadingCache provideAccessCache(Function getAccess, + @Named(PROPERTY_SESSION_INTERVAL) long + sessionIntervalInSeconds) { + // since the session interval is also the token expiration time requested to the server make the token expire a + // bit before the deadline to make sure there aren't session expiration exceptions + sessionIntervalInSeconds = sessionIntervalInSeconds > 30 ? sessionIntervalInSeconds - 30 : + sessionIntervalInSeconds; + return CacheBuilder.newBuilder().expireAfterWrite(sessionIntervalInSeconds, TimeUnit.MINUTES).build(CacheLoader + .from(getAccess)); + } + +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java new file mode 100644 index 0000000000..60fb13d726 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java @@ -0,0 +1,48 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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; + +/** + * Configurable properties for jclouds OAuth + * + * @author David Alves + */ +public class OAuthProperties { + + /** + * The selected signature algorithm to use to sign the requests. + *

+ * This refers to the name the oauth provider expects, i.e., "RSA + */ + public static final String SIGNATURE_OR_MAC_ALGORITHM = "jclouds.oauth.signature-or-mac-algorithm"; + + /** + * The oauth audience, who this token is intended for. For instance in JWT and for + * google API's this property maps to: {"aud","https://accounts.google.com/o/oauth2/token"} + * + * @see doc + */ + public static final String AUDIENCE = "jclouds.oauth.audience"; + + /** + * Optional list of comma-separated scopes to use when no OAuthScopes annotation is present. + */ + public static final String SCOPES = "jclouds.oauth.scopes"; +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthRestClientModule.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthRestClientModule.java new file mode 100644 index 0000000000..3f6864aa1b --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthRestClientModule.java @@ -0,0 +1,36 @@ +package org.jclouds.oauth.v2.config; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.reflect.TypeToken; +import com.google.inject.Provides; +import org.jclouds.oauth.v2.OAuthApi; +import org.jclouds.oauth.v2.OAuthAsyncApi; +import org.jclouds.providers.ProviderMetadata; +import org.jclouds.rest.ConfiguresRestClient; +import org.jclouds.rest.config.RestClientModule; + +import javax.inject.Singleton; +import java.net.URI; + +/** + * OAuth module to when accessing OAuth stand-alone. + * + * @author David Alves + */ +@ConfiguresRestClient +public class OAuthRestClientModule extends RestClientModule { + + public OAuthRestClientModule() { + super(TypeToken.class.cast(TypeToken.of(OAuthApi.class)), TypeToken.class.cast(TypeToken.of(OAuthAsyncApi + .class))); + } + + @Provides + @Singleton + @Authentication + protected Supplier provideAuthenticationEndpoint(ProviderMetadata providerMetadata) { + return Suppliers.ofInstance(URI.create(providerMetadata.getEndpoint())); + } + +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthScopes.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthScopes.java new file mode 100644 index 0000000000..f1136fe33b --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthScopes.java @@ -0,0 +1,45 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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 javax.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to annotate REST methods/ifaces that use OAuthAuthentication. + *

+ * Sets the scopes for the token request for that particular method. + * + * @author David Alves + */ +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = {ElementType.TYPE, ElementType.METHOD}) +@Qualifier +public @interface OAuthScopes { + + /** + * @return the OAuth scopes required to access the resource. + */ + String[] value(); + +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClaimSet.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClaimSet.java new file mode 100644 index 0000000000..c316786810 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClaimSet.java @@ -0,0 +1,195 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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 com.google.common.base.Objects; +import com.google.common.base.Splitter; +import com.google.common.collect.ForwardingMap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; + +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Objects.ToStringHelper; +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Objects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * The claimset for the token. + * + * @author David Alves + * @see doc + */ +public class ClaimSet extends ForwardingMap { + + public static Builder builder() { + return new Builder(); + } + + public Builder toBuilder() { + return builder().fromClaimSet(this); + } + + public static class Builder { + + private Set requiredClaims; + private ImmutableMap.Builder claims = new ImmutableMap.Builder(); + private long emissionTime; + private long expirationTime; + + public Builder() { + this(ImmutableSet.of()); + } + + /** + * Constructor that allows to predefine a mandatory set of claims as a comma-separated string, e.g, "iss,iat". + */ + public Builder(String commaSeparatedRequiredClaims) { + this(ImmutableSet.copyOf(Splitter.on(",").split(checkNotNull(commaSeparatedRequiredClaims)))); + } + + /** + * Constructor that allows to predefine a mandatory set of claims as a set of strings. + */ + public Builder(Set requiredClaims) { + this.requiredClaims = ImmutableSet.copyOf(checkNotNull(requiredClaims)); + } + + /** + * Adds a Claim, i.e. key/value pair, e.g., "scope":"all_permissions". + */ + public Builder addClaim(String name, String value) { + claims.put(checkNotNull(name), checkNotNull(value, "value of %s", name)); + return this; + } + + /** + * @see ClaimSet#getEmissionTime() + */ + public Builder emissionTime(long emmissionTime) { + this.emissionTime = emmissionTime; + return this; + } + + /** + * @see ClaimSet#getExpirationTime() + */ + public Builder expirationTime(long expirationTime) { + this.expirationTime = expirationTime; + return this; + } + + /** + * Adds a map containing multiple claims + */ + public Builder addAllClaims(Map claims) { + this.claims.putAll(checkNotNull(claims)); + return this; + } + + public ClaimSet build() { + Map claimsMap = claims.build(); + checkState(Sets.intersection(claimsMap.keySet(), requiredClaims).size() == requiredClaims.size(), + "not all required claims were present"); + if (expirationTime == 0) { + expirationTime = emissionTime + 3600; + } + return new ClaimSet(claimsMap, emissionTime, expirationTime); + } + + public Builder fromClaimSet(ClaimSet claimSet) { + return new Builder().addAllClaims(claimSet.claims).expirationTime(expirationTime).emissionTime(emissionTime); + } + } + + private final Map claims; + private final long emissionTime; + private final long expirationTime; + + private ClaimSet(Map claims, long emissionTime, long expirationTime) { + this.claims = claims; + this.emissionTime = emissionTime; + this.expirationTime = expirationTime; + } + + /** + * The emission time, in seconds since the epoch. + */ + public long getEmissionTime() { + return emissionTime; + } + + /** + * The expiration time, in seconds since the emission time. + */ + public long getExpirationTime() { + return expirationTime; + } + + /** + * @returns the claims. + */ + @Override + protected Map delegate() { + return claims; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hashCode(claims, emissionTime, expirationTime); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ClaimSet other = (ClaimSet) obj; + return equal(claims, other.claims) && equal(this.emissionTime, + other.emissionTime) && equal(this.expirationTime, other.expirationTime); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return string().toString(); + } + + protected ToStringHelper string() { + return toStringHelper(this).omitNullValues().add("claims", claims) + .add("emissionTime", emissionTime).add("expirationTIme", expirationTime); + } +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/Header.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/Header.java new file mode 100644 index 0000000000..ab4cd640a0 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/Header.java @@ -0,0 +1,131 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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 com.google.common.base.Objects; + +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Objects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * The header for the OAuth token, contains the signer algorithm's name and the type of the token + * + * @author David Alves + * @see doc + */ +public class Header { + + public static Builder builder() { + return new Builder(); + } + + public Builder toBuilder() { + return builder().fromHeader(this); + } + + public static class Builder { + + private String signerAlgorithm; + private String type; + + /** + * @see Header#getSignerAlgorithm() + */ + public Builder signerAlgorithm(String signerAlgorithm) { + this.signerAlgorithm = checkNotNull(signerAlgorithm); + return this; + } + + /** + * @see Header#getType() + */ + public Builder type(String type) { + this.type = checkNotNull(type); + return this; + } + + public Header build() { + return new Header(signerAlgorithm, type); + } + + public Builder fromHeader(Header header) { + return new Builder().signerAlgorithm(header.signerAlgorithm).type(header.type); + } + } + + private final String signerAlgorithm; + private final String type; + + protected Header(String signerAlgorithm, String type) { + this.signerAlgorithm = checkNotNull(signerAlgorithm); + this.type = checkNotNull(type); + } + + /** + * The name of the algorithm used to compute the signature, e.g., "RS256" + */ + public String getSignerAlgorithm() { + return signerAlgorithm; + } + + /** + * The type of the token, e.g., "JWT" + */ + public String getType() { + return type; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Header other = (Header) obj; + return equal(this.signerAlgorithm, other.signerAlgorithm) && equal(this.type, + other.type); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hashCode(signerAlgorithm, type); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return string().toString(); + } + + protected Objects.ToStringHelper string() { + return toStringHelper(this).omitNullValues().add("signerAlgorithm", signerAlgorithm) + .add("type", type); + } +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/OAuthCredentials.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/OAuthCredentials.java new file mode 100644 index 0000000000..7f1496266d --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/OAuthCredentials.java @@ -0,0 +1,132 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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 com.google.common.base.Objects; +import org.jclouds.domain.Credentials; + +import java.security.PrivateKey; + +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Objects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Special kind credentials for oauth authentication that includes {@link java.security.PrivateKey} to sign + * requests. + */ +public class OAuthCredentials extends Credentials { + + public static Builder builder() { + return new Builder(); + } + + public Builder toBuilder() { + return builder().fromOauthCredentials(this); + } + + public static class Builder extends Credentials.Builder { + + protected PrivateKey privateKey; + + /** + * @see OAuthCredentials#privateKey + */ + public Builder privateKey(PrivateKey privateKey) { + this.privateKey = checkNotNull(privateKey); + return this; + } + + /** + * @see Credentials#identity + */ + public Builder identity(String identity) { + this.identity = checkNotNull(identity); + return this; + } + + /** + * @see Credentials#credential + */ + public Builder credential(String credential) { + this.credential = credential; + return this; + } + + @SuppressWarnings("unchecked") + public OAuthCredentials build() { + return new OAuthCredentials(checkNotNull(identity), credential, privateKey); + } + + public Builder fromOauthCredentials(OAuthCredentials credentials) { + return new Builder().privateKey(credentials.privateKey).identity(credentials.identity) + .credential(credentials.credential); + } + } + + /** + * The private key associated with Credentials#identity. + * Used to sign token requests. + */ + public final PrivateKey privateKey; + + public OAuthCredentials(String identity, String credential, PrivateKey privateKey) { + super(identity, credential); + this.privateKey = privateKey; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + OAuthCredentials other = (OAuthCredentials) obj; + return equal(this.identity, other.identity) && equal(this.credential, + other.credential) && equal(this.privateKey, + other.privateKey); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hashCode(identity, credential, privateKey); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return string().toString(); + } + + protected Objects.ToStringHelper string() { + return toStringHelper(this).omitNullValues().add("identity", identity) + .add("credential", credential != null ? credential.hashCode() : null).add("privateKey", + privateKey.hashCode()); + } +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/Token.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/Token.java new file mode 100644 index 0000000000..6ad47ea315 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/Token.java @@ -0,0 +1,153 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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 com.google.common.base.Objects; + +import java.beans.ConstructorProperties; + +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Objects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * The oauth token, obtained upon a successful token request and ready to embed in requests. + * + * @author David Alves + */ +public class Token { + + public static Builder builder() { + return new Builder(); + } + + public Builder toBuilder() { + return builder().fromToken(this); + } + + public static class Builder { + + private String accessToken; + private String tokenType; + private long expiresIn; + + /** + * @see Token#getAccessToken() + */ + public Builder accessToken(String accessToken) { + this.accessToken = checkNotNull(accessToken); + return this; + } + + /** + * @see Token#getTokenType() + */ + public Builder tokenType(String tokenType) { + this.tokenType = checkNotNull(tokenType); + return this; + } + + /** + * @see Token#getExpiresIn() + */ + public Builder expiresIn(long expiresIn) { + this.expiresIn = expiresIn; + return this; + } + + public Token build() { + return new Token(accessToken, tokenType, expiresIn); + } + + public Builder fromToken(Token token) { + return new Builder().accessToken(token.accessToken).tokenType(token.tokenType).expiresIn(token.expiresIn); + } + } + + private final String accessToken; + private final String tokenType; + private final long expiresIn; + + @ConstructorProperties({"access_token", "token_type", "expires_in"}) + protected Token(String accessToken, String tokenType, long expiresIn) { + this.accessToken = accessToken; + this.tokenType = tokenType; + this.expiresIn = expiresIn; + } + + /** + * The access token obtained from the OAuth server. + */ + public String getAccessToken() { + return accessToken; + } + + /** + * The type of the token, e.g., "Bearer" + */ + public String getTokenType() { + return tokenType; + } + + /** + * In how many seconds this token expires. + */ + public long getExpiresIn() { + return expiresIn; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Token other = (Token) obj; + return equal(this.accessToken, other.accessToken) && equal(this.tokenType, + other.tokenType) && equal(this.expiresIn, + other.expiresIn); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hashCode(accessToken, tokenType, expiresIn); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return string().toString(); + } + + protected Objects.ToStringHelper string() { + return toStringHelper(this).omitNullValues().add("accessToken", accessToken) + .add("tokenType", tokenType).add("expiresIn", expiresIn); + } + +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/TokenRequest.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/TokenRequest.java new file mode 100644 index 0000000000..d2e6ab5bed --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/TokenRequest.java @@ -0,0 +1,135 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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 com.google.common.base.Objects; + +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Objects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A complete token request. + * + * @author David Alves + */ +public class TokenRequest { + + public static Builder builder() { + return new Builder(); + } + + public Builder toBuilder() { + return builder().fromTokenRequest(this); + } + + public static class Builder { + private Header header; + private ClaimSet claimSet; + + /** + * @see TokenRequest#getClaimSet() + */ + public Builder header(Header header) { + this.header = header; + return this; + } + + /** + * @see TokenRequest#getHeader() + */ + public Builder claimSet(ClaimSet claimSet) { + this.claimSet = claimSet; + return this; + } + + public TokenRequest build() { + return new TokenRequest(header, claimSet); + } + + public Builder fromTokenRequest(TokenRequest tokeRequest) { + return new Builder().header(tokeRequest.header).claimSet(tokeRequest.claimSet); + } + } + + private final Header header; + private final ClaimSet claimSet; + + public TokenRequest(Header header, ClaimSet claimSet) { + this.header = checkNotNull(header); + this.claimSet = checkNotNull(claimSet); + } + + /** + * The header of this token request. + * + * @see Header + */ + public Header getHeader() { + return header; + } + + /** + * The claim set of this token request. + * + * @see ClaimSet + */ + public ClaimSet getClaimSet() { + return claimSet; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TokenRequest other = (TokenRequest) obj; + return equal(this.header, other.header) && equal(this.claimSet, + other.claimSet); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hashCode(header, claimSet); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return string().toString(); + } + + protected Objects.ToStringHelper string() { + return toStringHelper(this).omitNullValues().add("header", header) + .add("claimSet", claimSet); + } + + +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/TokenRequestFormat.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/TokenRequestFormat.java new file mode 100644 index 0000000000..7d4ab63fc6 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/domain/TokenRequestFormat.java @@ -0,0 +1,49 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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 com.google.inject.ImplementedBy; +import org.jclouds.http.HttpRequest; +import org.jclouds.oauth.v2.json.JWTTokenRequestFormat; + +import java.util.Set; + +/** + * Transforms a TokenRequest into a specific format (e.g. JWT token) + * + * @author David Alves + */ +@ImplementedBy(JWTTokenRequestFormat.class) +public interface TokenRequestFormat { + + /** + * Transforms the provided HttpRequest into a particular token request with a specific format. + */ + public R formatRequest(R httpRequest, TokenRequest tokenRequest); + + /** + * The name of the type of the token request, e.g., "JWT" + */ + public String getTypeName(); + + /** + * The claims that must be present in the token request for it to be valid. + */ + public Set requiredClaims(); +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/filters/OAuthAuthenticator.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/filters/OAuthAuthenticator.java new file mode 100644 index 0000000000..22ba6390fd --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/filters/OAuthAuthenticator.java @@ -0,0 +1,67 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.filters; + +import com.google.common.base.Function; +import com.google.common.cache.LoadingCache; +import org.jclouds.http.HttpException; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpRequestFilter; +import org.jclouds.oauth.v2.domain.Token; +import org.jclouds.oauth.v2.domain.TokenRequest; +import org.jclouds.rest.internal.GeneratedHttpRequest; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import static com.google.common.base.Preconditions.checkState; + +/** + * To be used by client applications to embed an OAuth authentication in their REST requests. + *

+ * TODO when we're able to use the OAuthAuthentication an this should be used automatically + * + * @author David Alves + */ +@Singleton +public class OAuthAuthenticator implements HttpRequestFilter { + + private Function tokenRequestBuilder; + private Function tokenFetcher; + + @Inject + OAuthAuthenticator(Function tokenRequestBuilder, LoadingCache tokenFetcher) { + this.tokenRequestBuilder = tokenRequestBuilder; + this.tokenFetcher = tokenFetcher; + } + + @Override + public HttpRequest filter(HttpRequest request) throws HttpException { + checkState(request instanceof GeneratedHttpRequest, "request must be an instance of GeneratedHttpRequest"); + GeneratedHttpRequest generatedHttpRequest = (GeneratedHttpRequest) request; + TokenRequest tokenRequest = tokenRequestBuilder.apply(generatedHttpRequest); + Token token = tokenFetcher.apply(tokenRequest); + return request.toBuilder().addHeader("Authorization", String.format("%s %s", + token.getTokenType(), token.getAccessToken())).build(); + + } + + +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/BuildTokenRequest.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/BuildTokenRequest.java new file mode 100644 index 0000000000..872e959432 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/BuildTokenRequest.java @@ -0,0 +1,131 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.functions; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.base.Supplier; +import com.google.common.base.Ticker; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Inject; +import com.google.inject.name.Named; +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; +import org.jclouds.oauth.v2.domain.TokenRequestFormat; +import org.jclouds.rest.internal.GeneratedHttpRequest; + +import javax.inject.Singleton; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkState; +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; + +/** + * The default authenticator. + *

+ * Builds the default token request with the following claims: iss,scope,aud,iat,exp. + *

+ * TODO scopes etc should come from the REST method and not from a global property + * + * @author David Alves + */ +@Singleton +public class BuildTokenRequest implements Function { + + private final String assertionTargetDescription; + private final String signatureAlgorithm; + private final TokenRequestFormat tokenRequestFormat; + private final Supplier credentialsSupplier; + private final long tokenDuration; + + @Inject(optional = true) + @Named(ADDITIONAL_CLAIMS) + protected Map additionalClaims = ImmutableMap.of(); + + @Inject(optional = true) + @Named(SCOPES) + protected String globalScopes = null; + + @Inject(optional = true) + public Ticker ticker = Ticker.systemTicker(); + + + @Inject + public BuildTokenRequest(@Named(AUDIENCE) String assertionTargetDescription, + @Named(SIGNATURE_OR_MAC_ALGORITHM) String signatureAlgorithm, + TokenRequestFormat tokenRequestFormat, Supplier credentialsSupplier, + @Named(Constants.PROPERTY_SESSION_INTERVAL) long tokenDuration) { + this.assertionTargetDescription = assertionTargetDescription; + this.signatureAlgorithm = signatureAlgorithm; + this.tokenRequestFormat = tokenRequestFormat; + this.credentialsSupplier = credentialsSupplier; + this.tokenDuration = tokenDuration; + } + + @Override + public TokenRequest apply(GeneratedHttpRequest request) { + long now = TimeUnit.SECONDS.convert(ticker.read(), TimeUnit.NANOSECONDS); + + // fetch the token + Header header = new Header.Builder() + .signerAlgorithm(signatureAlgorithm) + .type(tokenRequestFormat.getTypeName()) + .build(); + + ClaimSet claimSet = new ClaimSet.Builder(this.tokenRequestFormat.requiredClaims()) + .addClaim("iss", credentialsSupplier.get().identity) + .addClaim("scope", getOAuthScopes(request)) + .addClaim("aud", assertionTargetDescription) + .emissionTime(now) + .expirationTime(now + tokenDuration) + .addAllClaims(additionalClaims) + .build(); + + return new TokenRequest.Builder() + .header(header) + .claimSet(claimSet) + .build(); + } + + protected String getOAuthScopes(GeneratedHttpRequest request) { + OAuthScopes classScopes = request.getDeclaring().getAnnotation(OAuthScopes.class); + OAuthScopes methodScopes = request.getJavaMethod().getAnnotation(OAuthScopes.class); + + // if no annotations are present the rely on globally set scopes + if (classScopes == null && methodScopes == null) { + checkState(globalScopes != null, String.format("REST class or method should be annotated " + + "with OAuthScopes specifying required permissions. Alternatively a global property " + + "\"oauth.scopes\" may be set to define scopes globally. REST Class: %s, Method: %s", + request.getDeclaring().getName(), + request.getJavaMethod().getName())); + return globalScopes; + } + + OAuthScopes scopes = methodScopes != null ? methodScopes : classScopes; + return Joiner.on(",").join(scopes.value()); + } +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/FetchToken.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/FetchToken.java new file mode 100644 index 0000000000..a7975d92e4 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/FetchToken.java @@ -0,0 +1,47 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.functions; + +import com.google.common.base.Function; +import org.jclouds.oauth.v2.OAuthApi; +import org.jclouds.oauth.v2.domain.Token; +import org.jclouds.oauth.v2.domain.TokenRequest; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * @author David Alves + */ +@Singleton +public class FetchToken implements Function { + + private OAuthApi oAuthApi; + + @Inject + public FetchToken(OAuthApi oAuthApi) { + this.oAuthApi = oAuthApi; + } + + @Override + public Token apply(TokenRequest input) { + return this.oAuthApi.authenticate(input); + } +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/OAuthCredentialsSupplier.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/OAuthCredentialsSupplier.java new file mode 100644 index 0000000000..8a0032379a --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/OAuthCredentialsSupplier.java @@ -0,0 +1,91 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.functions; + +import com.google.common.base.Supplier; +import org.jclouds.crypto.Pems; +import org.jclouds.io.Payloads; +import org.jclouds.oauth.v2.domain.OAuthCredentials; +import org.jclouds.rest.annotations.Credential; +import org.jclouds.rest.annotations.Identity; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; + +import static com.google.common.base.Preconditions.checkState; +import static java.lang.String.format; +import static org.jclouds.oauth.v2.OAuthConstants.NO_ALGORITHM; +import static org.jclouds.oauth.v2.OAuthConstants.OAUTH_ALGORITHM_NAMES_TO_KEYFACTORY_ALGORITHM_NAMES; +import static org.jclouds.oauth.v2.config.OAuthProperties.SIGNATURE_OR_MAC_ALGORITHM; + +/** + * Loads {@link OAuthCredentials} from a pem private key using the KeyFactory obtained from the + * JWT Algorithm Name<->KeyFactory name mapping in OAuthConstants. The pem pk algorithm must match the KeyFactory + * algorithm. + * + * @author David Alves + * @see org.jclouds.oauth.v2.OAuthConstants#OAUTH_ALGORITHM_NAMES_TO_KEYFACTORY_ALGORITHM_NAMES + */ +@Singleton +public class OAuthCredentialsSupplier implements Supplier { + + + private final String identity; + private final String privateKeyInPemFormat; + private final String keyFactoryAlgorithm; + private OAuthCredentials credentials; + + @Inject + public OAuthCredentialsSupplier(@Identity String identity, + @Credential String privateKeyInPemFormat, + @Named(SIGNATURE_OR_MAC_ALGORITHM) String signatureOrMacAlgorithm) { + this.identity = identity; + this.privateKeyInPemFormat = privateKeyInPemFormat; + checkState(OAUTH_ALGORITHM_NAMES_TO_KEYFACTORY_ALGORITHM_NAMES.containsKey(signatureOrMacAlgorithm), + format("No mapping for key factory for algorithm: %s", signatureOrMacAlgorithm)); + this.keyFactoryAlgorithm = OAUTH_ALGORITHM_NAMES_TO_KEYFACTORY_ALGORITHM_NAMES.get(signatureOrMacAlgorithm); + } + + @PostConstruct + public void loadPrivateKey() throws NoSuchAlgorithmException, IOException, InvalidKeySpecException { + if (keyFactoryAlgorithm.equals(NO_ALGORITHM)) { + this.credentials = new OAuthCredentials.Builder().identity(identity).credential + (privateKeyInPemFormat).build(); + return; + } + KeyFactory keyFactory = KeyFactory.getInstance(keyFactoryAlgorithm); + PrivateKey privateKey = keyFactory.generatePrivate(Pems.privateKeySpec(Payloads.newStringPayload + (privateKeyInPemFormat))); + this.credentials = new OAuthCredentials.Builder().identity(identity).credential + (privateKeyInPemFormat).privateKey(privateKey).build(); + } + + @Override + public OAuthCredentials get() { + return this.credentials; + } + +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/SignOrProduceMacForToken.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/SignOrProduceMacForToken.java new file mode 100644 index 0000000000..0d590a836e --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/functions/SignOrProduceMacForToken.java @@ -0,0 +1,123 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.functions; + +import com.google.common.base.Function; +import com.google.common.base.Supplier; +import com.google.common.base.Throwables; +import org.jclouds.oauth.v2.domain.OAuthCredentials; + +import javax.annotation.PostConstruct; +import javax.crypto.Mac; +import javax.inject.Inject; +import javax.inject.Named; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; + +import static com.google.common.base.Preconditions.checkState; +import static java.lang.String.format; +import static org.jclouds.oauth.v2.OAuthConstants.NO_ALGORITHM; +import static org.jclouds.oauth.v2.OAuthConstants.OAUTH_ALGORITHM_NAMES_TO_SIGNATURE_ALGORITHM_NAMES; +import static org.jclouds.oauth.v2.config.OAuthProperties.SIGNATURE_OR_MAC_ALGORITHM; + +/** + * Function that signs/produces mac's for OAuth tokens, provided a {@link Signature} or a {@link Mac} algorithm and + * {@link PrivateKey} + * + * @author David Alves + */ +public class SignOrProduceMacForToken implements Function { + + private final Supplier credentials; + private final String signatureOrMacAlgorithm; + private Function signatureOrMacFunction; + + + @Inject + public SignOrProduceMacForToken(@Named(SIGNATURE_OR_MAC_ALGORITHM) String signatureOrMacAlgorithm, + Supplier credentials) { + checkState(OAUTH_ALGORITHM_NAMES_TO_SIGNATURE_ALGORITHM_NAMES.containsKey(signatureOrMacAlgorithm), + format("the signature algorithm %s is not supported", signatureOrMacAlgorithm)); + this.signatureOrMacAlgorithm = OAUTH_ALGORITHM_NAMES_TO_SIGNATURE_ALGORITHM_NAMES.get(signatureOrMacAlgorithm); + this.credentials = credentials; + } + + @PostConstruct + public void loadSignatureOrMacOrNone() throws InvalidKeyException, NoSuchAlgorithmException { + if (signatureOrMacAlgorithm.equals(NO_ALGORITHM)) { + this.signatureOrMacFunction = new Function() { + @Override + public byte[] apply(byte[] input) { + return null; + } + }; + } else if (signatureOrMacAlgorithm.startsWith("SHA")) { + this.signatureOrMacFunction = new SignatureGenerator(signatureOrMacAlgorithm, credentials.get().privateKey); + } else { + this.signatureOrMacFunction = new MessageAuthenticationCodeGenerator(signatureOrMacAlgorithm, + credentials.get().privateKey); + } + } + + @Override + public byte[] apply(byte[] input) { + return signatureOrMacFunction.apply(input); + } + + private static class MessageAuthenticationCodeGenerator implements Function { + + private Mac mac; + + private MessageAuthenticationCodeGenerator(String macAlgorithm, PrivateKey privateKey) throws + NoSuchAlgorithmException, InvalidKeyException { + this.mac = Mac.getInstance(macAlgorithm); + this.mac.init(privateKey); + } + + @Override + public byte[] apply(byte[] input) { + this.mac.update(input); + return this.mac.doFinal(); + } + } + + private static class SignatureGenerator implements Function { + + private Signature signature; + + private SignatureGenerator(String signatureAlgorithm, PrivateKey privateKey) throws NoSuchAlgorithmException, + InvalidKeyException { + this.signature = Signature.getInstance(signatureAlgorithm); + this.signature.initSign(privateKey); + } + + @Override + public byte[] apply(byte[] input) { + try { + signature.update(input); + return signature.sign(); + } catch (SignatureException e) { + throw Throwables.propagate(e); + } + } + } +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/handlers/OAuthErrorHandler.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/handlers/OAuthErrorHandler.java new file mode 100644 index 0000000000..f83c83373a --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/handlers/OAuthErrorHandler.java @@ -0,0 +1,68 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.handlers; + +import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpErrorHandler; +import org.jclouds.http.HttpResponse; +import org.jclouds.http.HttpResponseException; +import org.jclouds.rest.AuthorizationException; +import org.jclouds.rest.ResourceNotFoundException; + +import javax.inject.Singleton; + +import static javax.ws.rs.core.Response.Status; +import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream; + +/** + * This will parse and set an appropriate exception on the command object. + * + * @author David Alves + */ +@Singleton +public class OAuthErrorHandler implements HttpErrorHandler { + public void handleError(HttpCommand command, HttpResponse response) { + // it is important to always read fully and close streams + byte[] data = closeClientButKeepContentStream(response); + String message = data != null ? new String(data) : null; + + Exception exception = message != null ? new HttpResponseException(command, response, message) + : new HttpResponseException(command, response); + message = message != null ? message : String.format("%s -> %s", command.getCurrentRequest().getRequestLine(), + response.getStatusLine()); + Status status = Status.fromStatusCode(response.getStatusCode()); + switch (status) { + case BAD_REQUEST: + break; + case UNAUTHORIZED: + case FORBIDDEN: + exception = new AuthorizationException(message, exception); + break; + case NOT_FOUND: + if (!command.getCurrentRequest().getMethod().equals("DELETE")) { + exception = new ResourceNotFoundException(message, exception); + } + break; + case CONFLICT: + exception = new IllegalStateException(message, exception); + break; + } + command.setException(exception); + } +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/handlers/OAuthTokenBinder.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/handlers/OAuthTokenBinder.java new file mode 100644 index 0000000000..e41ebe6df7 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/handlers/OAuthTokenBinder.java @@ -0,0 +1,49 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.handlers; + +import org.jclouds.http.HttpRequest; +import org.jclouds.oauth.v2.domain.TokenRequest; +import org.jclouds.oauth.v2.domain.TokenRequestFormat; +import org.jclouds.rest.Binder; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Generic implementation of a token binder. Uses a provided {@link TokenRequestFormat} to actually bind tokens to + * requests. + * + * @author David Alves + */ +@Singleton +public class OAuthTokenBinder implements Binder { + + private final TokenRequestFormat tokenRequestFormat; + + @Inject + OAuthTokenBinder(TokenRequestFormat tokenRequestFormat) { + this.tokenRequestFormat = tokenRequestFormat; + } + + @Override + public R bindToRequest(R request, Object input) { + return tokenRequestFormat.formatRequest(request, (TokenRequest) input); + } +} \ No newline at end of file diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/json/ClaimSetTypeAdapter.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/json/ClaimSetTypeAdapter.java new file mode 100644 index 0000000000..b41a6b5030 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/json/ClaimSetTypeAdapter.java @@ -0,0 +1,63 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.json; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import org.jclouds.oauth.v2.domain.ClaimSet; + +import java.io.IOException; +import java.util.Map; + +/** + * JSON TypeAdapter for the ClaimSet type. Pull the claims maps to the root level and adds two properties for the + * expiration time and issuing time. + * + * @author David Alves + */ +public class ClaimSetTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, ClaimSet value) throws IOException { + out.beginObject(); + for (Map.Entry entry : value.entrySet()) { + out.name(entry.getKey()); + out.value(entry.getValue()); + } + out.name("exp"); + out.value(value.getExpirationTime()); + out.name("iat"); + out.value(value.getEmissionTime()); + out.endObject(); + } + + @Override + public ClaimSet read(JsonReader in) throws IOException { + ClaimSet.Builder builder = new ClaimSet.Builder(); + in.beginObject(); + while (in.hasNext()) { + String claimName = in.nextName(); + String claimValue = in.nextString(); + builder.addClaim(claimName, claimValue); + } + in.endObject(); + return builder.build(); + } +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/json/HeaderTypeAdapter.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/json/HeaderTypeAdapter.java new file mode 100644 index 0000000000..a23d0c9cbd --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/json/HeaderTypeAdapter.java @@ -0,0 +1,54 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.json; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import org.jclouds.oauth.v2.domain.Header; + +import java.io.IOException; + +/** + * JSON TypeAdapter for the Header type. Simply transforms the field names. + */ +public class HeaderTypeAdapter extends TypeAdapter

{ + + @Override + public void write(JsonWriter out, Header value) throws IOException { + out.beginObject(); + out.name("alg"); + out.value(value.getSignerAlgorithm()); + out.name("typ"); + out.value(value.getType()); + out.endObject(); + } + + @Override + public Header read(JsonReader in) throws IOException { + Header.Builder builder = new Header.Builder(); + in.beginObject(); + in.nextName(); + builder.signerAlgorithm(in.nextString()); + in.nextName(); + builder.type(in.nextString()); + in.endObject(); + return builder.build(); + } +} diff --git a/labs/oauth/src/main/java/org/jclouds/oauth/v2/json/JWTTokenRequestFormat.java b/labs/oauth/src/main/java/org/jclouds/oauth/v2/json/JWTTokenRequestFormat.java new file mode 100644 index 0000000000..c581b02426 --- /dev/null +++ b/labs/oauth/src/main/java/org/jclouds/oauth/v2/json/JWTTokenRequestFormat.java @@ -0,0 +1,101 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.json; + +import com.google.common.base.Charsets; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import org.jclouds.crypto.CryptoStreams; +import org.jclouds.http.HttpRequest; +import org.jclouds.io.Payloads; +import org.jclouds.json.Json; +import org.jclouds.oauth.v2.domain.TokenRequest; +import org.jclouds.oauth.v2.domain.TokenRequestFormat; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Set; + +import static com.google.common.base.Joiner.on; + +/** + * Formats a token request into JWT format namely: + * - transforms the token request to json + * - creates the base64 header.claimset portions of the payload. + * - uses the provided signer function to create a signature + * - creates the full url encoded payload as described in: + * https://developers.google.com/accounts/docs/OAuth2ServiceAccount + *

+ * + * @author David Alves + */ +@Singleton +public class JWTTokenRequestFormat implements TokenRequestFormat { + + private static final String ASSERTION_FORM_PARAM = "assertion"; + private static final String GRANT_TYPE_FORM_PARAM = "grant_type"; + private static final String GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + + private final Function signer; + private final Json json; + + @Inject + public JWTTokenRequestFormat(Function signer, Json json) { + this.signer = signer; + this.json = json; + } + + @Override + public R formatRequest(R httpRequest, TokenRequest tokenRequest) { + HttpRequest.Builder builder = httpRequest.toBuilder(); + + // transform to json and encode in base 64 + // use commons-codec base 64 which properly encodes urls (jclouds's Base64 does not) + String encodedHeader = json.toJson(tokenRequest.getHeader()); + String encodedClaimSet = json.toJson(tokenRequest.getClaimSet()); + + String encodedSignature = null; + + encodedHeader = CryptoStreams.base64Url(encodedHeader.getBytes(Charsets.UTF_8)); + encodedClaimSet = CryptoStreams.base64Url(encodedClaimSet.getBytes(Charsets.UTF_8)); + + byte[] signature = signer.apply(on(".").join(encodedHeader, encodedClaimSet).getBytes(Charsets.UTF_8)); + encodedSignature = signature != null ? CryptoStreams.base64Url(signature) : ""; + + // the final assertion in base 64 encoded {header}.{claimSet}.{signature} format + String assertion = on(".").join(encodedHeader, encodedClaimSet, encodedSignature); + + builder.payload(Payloads.newUrlEncodedFormPayload(ImmutableMultimap.of(GRANT_TYPE_FORM_PARAM, + GRANT_TYPE_JWT_BEARER, ASSERTION_FORM_PARAM, assertion))); + + return (R) builder.build(); + } + + @Override + public String getTypeName() { + return "JWT"; + } + + @Override + public Set requiredClaims() { + // exp and ist (expiration and emission times) are assumed mandatory already + return ImmutableSet.of("iss", "scope", "aud"); + } +} diff --git a/labs/oauth/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata b/labs/oauth/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata new file mode 100644 index 0000000000..d5a96e463b --- /dev/null +++ b/labs/oauth/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata @@ -0,0 +1 @@ +org.jclouds.oauth.v2.OAuthApiMetadata diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/BaseOauthAuthenticatedRestContextLiveTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/BaseOauthAuthenticatedRestContextLiveTest.java new file mode 100644 index 0000000000..645c23ede8 --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/BaseOauthAuthenticatedRestContextLiveTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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; + +import com.google.common.base.Ticker; +import com.google.inject.Key; +import com.google.inject.name.Names; +import org.jclouds.apis.BaseContextLiveTest; +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; +import org.jclouds.rest.RestContext; +import org.testng.annotations.Test; + +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkState; +import static org.jclouds.oauth.v2.OAuthTestUtils.setCredentialFromPemFile; +import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE; +import static org.jclouds.oauth.v2.config.OAuthProperties.SIGNATURE_OR_MAC_ALGORITHM; +import static org.testng.Assert.assertNotNull; + +/** + * A base test of oauth authenticated rest providers. Providers must set the following properties: + *

+ * - oauth.endpoint + * - oauth.audience + * - oauth.signature-or-mac-algorithm + *

+ * - oauth.scopes is provided by the subclass + *

+ * This test asserts that a provider can authenticate with oauth for a given scope, or more simply + * that authentication/authorization is working. + * + * @author David Alves + */ + +@Test(groups = "live") +public abstract class BaseOauthAuthenticatedRestContextLiveTest extends BaseContextLiveTest> { + + + private OAuthApi oauthApi; + + @Override + protected Properties setupProperties() { + Properties props = super.setupProperties(); + setCredentialFromPemFile(props, provider + ".credential"); + return props; + } + + public void testAuthenticate() { + + try { + oauthApi = context.utils().injector().getInstance(OAuthApi.class); + } catch (Exception e) { + throw new IllegalStateException("Provider has no OAuthApi bound. Was the OAuthAuthenticationModule added?"); + } + + // obtain the necessary properties from the context + String signatureAlgorithm = getContextPropertyOrFail(SIGNATURE_OR_MAC_ALGORITHM); + checkState(OAuthConstants.OAUTH_ALGORITHM_NAMES_TO_SIGNATURE_ALGORITHM_NAMES.containsKey(signatureAlgorithm) + , String.format("Algorithm not supported: " + signatureAlgorithm)); + + String audience = getContextPropertyOrFail(AUDIENCE); + + // obtain the scopes from the subclass + String scopes = getScopes(); + + Header header = Header.builder().signerAlgorithm(signatureAlgorithm).type("JWT").build(); + + long now = TimeUnit.SECONDS.convert(Ticker.systemTicker().read(), TimeUnit.NANOSECONDS); + + ClaimSet claimSet = ClaimSet.builder().addClaim("aud", audience).addClaim("scope", scopes).addClaim("iss", + identity).emissionTime(now).expirationTime(now + 3600).build(); + + TokenRequest tokenRequest = TokenRequest.builder().header(header).claimSet(claimSet).build(); + + Token token = oauthApi.authenticate(tokenRequest); + + assertNotNull(token); + } + + public abstract String getScopes(); + + private String getContextPropertyOrFail(String property) { + try { + return context.utils().injector().getInstance(Key.get(String.class, Names.named(property))); + } catch (Exception e) { + throw new IllegalStateException("Provider " + provider + " must have a named property: " + property + " for " + + "oauth to work"); + } + } + +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/OAuthApiMetadataTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/OAuthApiMetadataTest.java new file mode 100644 index 0000000000..5e4712e2d3 --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/OAuthApiMetadataTest.java @@ -0,0 +1,42 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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; + +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.TypeToken; +import org.jclouds.View; +import org.jclouds.apis.internal.BaseApiMetadataTest; +import org.testng.annotations.Test; + +/** + * Tests that OAuthApiMetadata is properly registered in ServiceLoader + *

+ *

+ * META-INF/services/org.jclouds.apis.ApiMetadata
+ * 
+ * + * @author Adrian Cole + */ +@Test(groups = "unit") +public class OAuthApiMetadataTest extends BaseApiMetadataTest { + + public OAuthApiMetadataTest() { + super(new OAuthApiMetadata(), ImmutableSet.>of()); + } +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/OAuthTestUtils.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/OAuthTestUtils.java new file mode 100644 index 0000000000..2d5fb5fa6f --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/OAuthTestUtils.java @@ -0,0 +1,81 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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; + +import com.google.common.base.Charsets; +import com.google.common.base.Throwables; +import com.google.common.io.Files; +import org.jclouds.util.Strings2; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE; + +/** + * Utils for OAuth tests. + * + * @author David Alves + */ +public class OAuthTestUtils { + + public static Properties defaultProperties(Properties properties) { + try { + properties = properties == null ? new Properties() : properties; + properties.put("oauth.identity", "foo"); + properties.put("oauth.credential", Strings2.toStringAndClose(new FileInputStream("src/test/resources/testpk" + + ".pem"))); + properties.put("oauth.endpoint", "http://localhost:5000/o/oauth2/token"); + properties.put(AUDIENCE, "https://accounts.google.com/o/oauth2/token"); + return properties; + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + + public static String setCredentialFromPemFile(Properties overrides, String key) { + String val = null; + String credentialFromFile = null; + String testKey = "test." + key; + + if (System.getProperties().containsKey(testKey)) { + val = System.getProperty(testKey); + } + checkNotNull(val, String.format("the property %s must be set (pem private key path)", testKey)); + + try { + credentialFromFile = Files.toString(new File(val), Charsets.UTF_8); + } catch (IOException e) { + throw Throwables.propagate(e); + } + overrides.setProperty(key, credentialFromFile); + return credentialFromFile; + } + + public static String getMandatoryProperty(Properties properties, String key) { + checkNotNull(properties); + checkNotNull(key); + String value = properties.getProperty(key); + return checkNotNull(value, String.format("mandatory property %s or test.%s was not present", key, key)); + } + +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/features/OAuthApiExpectTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/features/OAuthApiExpectTest.java new file mode 100644 index 0000000000..f902a46bff --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/features/OAuthApiExpectTest.java @@ -0,0 +1,102 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.features; + +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.jclouds.oauth.v2.OAuthApi; +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; +import org.jclouds.oauth.v2.internal.BaseOAuthApiExpectTest; +import org.testng.annotations.Test; + +import javax.ws.rs.core.MediaType; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.Properties; + +import static org.jclouds.crypto.CryptoStreams.base64Url; +import static org.testng.Assert.assertEquals; + +/** + * Tests that a token requess is well formed. + * + * @author David Alves + */ +@Test(groups = "unit") +public class OAuthApiExpectTest extends BaseOAuthApiExpectTest { + + private static final String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}"; + + private static final String claims = "{\"iss\":\"761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer" + + ".gserviceaccount.com\"," + + "\"scope\":\"https://www.googleapis.com/auth/prediction\",\"aud\":\"https://accounts.google" + + ".com/o/oauth2/token\",\"exp\":1328573381,\"iat\":1328569781}"; + + private static final Token TOKEN = new Token.Builder().accessToken + ("1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M").tokenType("Bearer").expiresIn(3600).build(); + + private static final ClaimSet CLAIM_SET = new ClaimSet.Builder().addClaim("iss", + "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer" + + ".gserviceaccount.com") + .addClaim("scope", "https://www.googleapis.com/auth/prediction") + .addClaim("aud", "https://accounts.google.com/o/oauth2/token") + .expirationTime(1328573381) + .emissionTime(1328569781).build(); + + private static final Header HEADER = new Header.Builder().signerAlgorithm("RS256").type("JWT").build(); + + private static final String URL_ENCODED_TOKEN_REQUEST = + "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&" + + // Base64 Encoded Header + "assertion=" + base64Url(header.getBytes(Charset.forName("UTF-8"))) + "." + + // Base64 Encoded Claims + base64Url(claims.getBytes(Charset.forName("UTF-8"))) + "." + + // Base64 encoded {header}.{claims} signature (using SHA256) + "W2Lesr_98AzVYiMbzxFqmwcOjpIWlwqkC6pNn1fXND9oSDNNnFhy-AAR6DKH-x9ZmxbY80" + + "R5fH-OCeWumXlVgceKN8Z2SmgQsu8ElTpypQA54j_5j8vUImJ5hsOUYPeyF1U2BUzZ3L5g" + + "03PXBA0YWwRU9E1ChH28dQBYuGiUmYw"; + + private static final HttpRequest TOKEN_REQUEST = HttpRequest.builder() + .method("POST") + .endpoint(URI.create("http://localhost:5000/o/oauth2/token")) + .addHeader("Accept", MediaType.APPLICATION_JSON) + .payload(payloadFromStringWithContentType(URL_ENCODED_TOKEN_REQUEST, "application/x-www-form-urlencoded")) + .build(); + + private static final HttpResponse TOKEN_RESPONSE = HttpResponse.builder().statusCode(200).payload( + payloadFromString("{\n" + + " \"access_token\" : \"1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M\",\n" + + " \"token_type\" : \"Bearer\",\n" + + " \"expires_in\" : 3600\n" + + "}")).build(); + + @Override + protected Properties setupProperties() { + return OAuthTestUtils.defaultProperties(super.setupProperties()); + } + + public void testGenerateJWTRequest() { + OAuthApi api = requestSendsResponse(TOKEN_REQUEST, TOKEN_RESPONSE); + assertEquals(api.authenticate(new TokenRequest(HEADER, CLAIM_SET)), TOKEN); + } +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/features/OAuthApiLiveTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/features/OAuthApiLiveTest.java new file mode 100644 index 0000000000..5956652c5d --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/features/OAuthApiLiveTest.java @@ -0,0 +1,95 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.features; + +import com.google.common.reflect.TypeToken; +import org.jclouds.oauth.v2.OAuthApi; +import org.jclouds.oauth.v2.OAuthApiMetadata; +import org.jclouds.oauth.v2.OAuthAsyncApi; +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; +import org.jclouds.oauth.v2.internal.BaseOAuthApiLiveTest; +import org.jclouds.rest.RestContext; +import org.testng.annotations.Test; + +import java.util.Properties; + +import static com.google.common.base.Preconditions.checkState; +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.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * A live test for authentication. Requires the following properties to be set: + * - test.oauth.endpoint + * - test.oauth.identity + * - test.oauth.credential + * - test.jclouds.oauth.audience + * - test.jclouds.oauth.scopes + * - test.jclouds.oauth.signature-or-mac-algorithm + * + * @author David Alves + */ +@Test(groups = "live", singleThreaded = true) +public class OAuthApiLiveTest extends BaseOAuthApiLiveTest { + + private Properties properties; + + @Override + protected Properties setupProperties() { + properties = super.setupProperties(); + return properties; + + } + + @Test(groups = "live", singleThreaded = true) + public void testAuthenticateJWTToken() throws Exception { + assertTrue(properties != null, "properties were not set"); + String signatureAlgorithm = getMandatoryProperty(properties, SIGNATURE_OR_MAC_ALGORITHM); + checkState(OAuthConstants.OAUTH_ALGORITHM_NAMES_TO_SIGNATURE_ALGORITHM_NAMES.containsKey(signatureAlgorithm) + , String.format("Algorithm not supported: " + signatureAlgorithm)); + + Header header = Header.builder().signerAlgorithm(signatureAlgorithm).type("JWT").build(); + + String scopes = getMandatoryProperty(properties, SCOPES); + String audience = getMandatoryProperty(properties, AUDIENCE); + + long now = nowInSeconds(); + + ClaimSet claimSet = ClaimSet.builder().addClaim("aud", audience).addClaim("scope", scopes).addClaim("iss", + identity).emissionTime(now).expirationTime(now + 3600).build(); + + TokenRequest tokenRequest = TokenRequest.builder().header(header).claimSet(claimSet).build(); + Token token = context.getApi().authenticate(tokenRequest); + + assertNotNull(token); + } + + @Override + protected TypeToken> contextType() { + return OAuthApiMetadata.CONTEXT_TOKEN; + } +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/functions/OAuthCredentialsFromPKTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/functions/OAuthCredentialsFromPKTest.java new file mode 100644 index 0000000000..40e5b45cb0 --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/functions/OAuthCredentialsFromPKTest.java @@ -0,0 +1,58 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.functions; + +import org.jclouds.oauth.v2.domain.OAuthCredentials; +import org.jclouds.util.Strings2; +import org.testng.annotations.Test; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +/** + * Test loading the credentials by extracting a pk from a PKCS12 keystore. + */ +@Test(groups = "unit") +public class OAuthCredentialsFromPKTest { + + public static OAuthCredentials loadOAuthCredentials() throws IOException, NoSuchAlgorithmException, + CertificateException, InvalidKeySpecException { + OAuthCredentialsSupplier loader = new OAuthCredentialsSupplier("foo", + Strings2.toStringAndClose(new FileInputStream("src/test/resources/testpk.pem")), "RS256"); + loader.loadPrivateKey(); + return loader.get(); + } + + + public void testLoadPKString() throws IOException, NoSuchAlgorithmException, KeyStoreException, + CertificateException, UnrecoverableKeyException, InvalidKeySpecException { + OAuthCredentials creds = loadOAuthCredentials(); + assertNotNull(creds); + assertEquals(creds.identity, "foo"); + assertNotNull(creds.privateKey); + } +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/functions/SignerFunctionTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/functions/SignerFunctionTest.java new file mode 100644 index 0000000000..db023d394e --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/functions/SignerFunctionTest.java @@ -0,0 +1,65 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.functions; + +import org.jclouds.crypto.CryptoStreams; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; + +import static com.google.common.base.Suppliers.ofInstance; +import static org.testng.Assert.assertNotNull; +import static org.testng.AssertJUnit.assertEquals; + + +/** + * Tests the SignOrProduceMacForToken + * + * @author David Alves + */ +@Test(groups = "unit") +public class SignerFunctionTest { + + private static final String PAYLOAD = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.\n" + + "eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZ" + + "GV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2ds" + + "ZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2x" + + "lLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ"; + + private static final String SHA256withRSA_PAYLOAD_SIGNATURE_RESULT = + "bmQrCv4gjkLWDK1JNJni74_kPiSDUMF_FImgqKJMUIgkDX1m2Sg3bH1yjF-cjBN7CvfAscnageo" + + "GtL2TGbwoTjJgUO5Yy0esavUUF-mBQHQtSw-2nL-9TNyM4SNi6fHPbgr83GGKOgA86r" + + "I9-nj3oUGd1fQty2k4Lsd-Zdkz6es"; + + + public void testSignPayload() throws InvalidKeyException, IOException, NoSuchAlgorithmException, + CertificateException, InvalidKeySpecException { + SignOrProduceMacForToken signer = new SignOrProduceMacForToken("RS256", + ofInstance(OAuthCredentialsFromPKTest + .loadOAuthCredentials())); + signer.loadSignatureOrMacOrNone(); + byte[] payloadSignature = signer.apply(PAYLOAD.getBytes("UTF-8")); + assertNotNull(payloadSignature); + assertEquals(CryptoStreams.base64Url(payloadSignature), SHA256withRSA_PAYLOAD_SIGNATURE_RESULT); + } +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/handlers/OAuthErrorHandlerTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/handlers/OAuthErrorHandlerTest.java new file mode 100644 index 0000000000..4b34c7ec24 --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/handlers/OAuthErrorHandlerTest.java @@ -0,0 +1,97 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.handlers; + +import org.easymock.IArgumentMatcher; +import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.testng.annotations.Test; + +import java.net.URI; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reportMatcher; +import static org.easymock.EasyMock.verify; + +/** + * @author Adrian Cole + */ +@Test(groups = "unit") +public class OAuthErrorHandlerTest { + + @Test + public void test409MakesIllegalStateException() { + assertCodeMakes( + "POST", + URI.create("http://oauth.org"), + 409, + "HTTP/1.1 409 Conflict", + "\"{\"code\":\"InvalidState\",\"message\":\"An incompatible transition has already been queued for this" + + " resource\"}\"", + IllegalStateException.class); + } + + private void assertCodeMakes(String method, URI uri, int statusCode, String message, String content, + Class expected) { + assertCodeMakes(method, uri, statusCode, message, "application/json", content, expected); + } + + private void assertCodeMakes(String method, URI uri, int statusCode, String message, String contentType, + String content, Class expected) { + + OAuthErrorHandler function = new OAuthErrorHandler(); + + HttpCommand command = createMock(HttpCommand.class); + HttpRequest request = HttpRequest.builder().method(method).endpoint(uri).build(); + HttpResponse response = HttpResponse.builder().statusCode(statusCode).message(message).payload(content).build(); + response.getPayload().getContentMetadata().setContentType(contentType); + + expect(command.getCurrentRequest()).andReturn(request).atLeastOnce(); + command.setException(classEq(expected)); + + replay(command); + + function.handleError(command, response); + + verify(command); + } + + public static Exception classEq(final Class in) { + reportMatcher(new IArgumentMatcher() { + + @Override + public void appendTo(StringBuffer buffer) { + buffer.append("classEq("); + buffer.append(in); + buffer.append(")"); + } + + @Override + public boolean matches(Object arg) { + return arg.getClass() == in; + } + + }); + return null; + } + +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/Base64UrlSafeTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/Base64UrlSafeTest.java new file mode 100644 index 0000000000..5d08d832cf --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/Base64UrlSafeTest.java @@ -0,0 +1,56 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.internal; + +import com.google.common.base.Charsets; +import com.sun.jersey.core.util.Base64; +import org.jclouds.crypto.CryptoStreams; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertTrue; + +/** + * Tests that the Base64 implementations used to Base64 encode the tokens are Url safe. + * + * @author David Alves + */ +@Test(groups = "unit") +public class Base64UrlSafeTest { + + public static final String STRING_THAT_GENERATES_URL_UNSAFE_BASE64_ENCODING = "§1234567890'+±!\"#$%&/()" + + "=?*qwertyuiopº´WERTYUIOPªàsdfghjklç~ASDFGHJKLÇ^ZXCVBNM;:_@€"; + + + public void testJcloudsCoreBase64IsNotUrlSafe() { + String encoded = new String(Base64.encode(STRING_THAT_GENERATES_URL_UNSAFE_BASE64_ENCODING.getBytes(Charsets + .UTF_8)), Charsets.UTF_8); + assertTrue(encoded.contains("+"), encoded); + assertTrue(encoded.contains("/"), encoded); + } + + public void testUsedBase64IsUrlSafe() { + String encoded = CryptoStreams.base64Url( + STRING_THAT_GENERATES_URL_UNSAFE_BASE64_ENCODING.getBytes(Charsets.UTF_8)); + assertTrue(!encoded.contains("+")); + assertTrue(!encoded.contains("/")); + assertTrue(!encoded.endsWith("=")); + } +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthApiExpectTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthApiExpectTest.java new file mode 100644 index 0000000000..47ce105d6b --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthApiExpectTest.java @@ -0,0 +1,28 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.internal; + +import org.jclouds.oauth.v2.OAuthApi; + +/** + * @author Adrian Cole + */ +public class BaseOAuthApiExpectTest extends BaseOAuthExpectTest { + +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthApiLiveTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthApiLiveTest.java new file mode 100644 index 0000000000..8fa1e98dca --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthApiLiveTest.java @@ -0,0 +1,71 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.internal; + +import com.google.common.base.Ticker; +import com.google.common.reflect.TypeToken; +import org.jclouds.apis.BaseContextLiveTest; +import org.jclouds.oauth.v2.OAuthApi; +import org.jclouds.oauth.v2.OAuthApiMetadata; +import org.jclouds.oauth.v2.OAuthAsyncApi; +import org.jclouds.rest.RestContext; +import org.testng.annotations.Test; + +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.jclouds.oauth.v2.OAuthTestUtils.setCredentialFromPemFile; +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; + + +/** + * @author David Alves + */ +@Test(groups = "live") +public class BaseOAuthApiLiveTest extends BaseContextLiveTest> { + + public BaseOAuthApiLiveTest() { + provider = "oauth"; + } + + @Override + protected TypeToken> contextType() { + return OAuthApiMetadata.CONTEXT_TOKEN; + } + + @Override + protected Properties setupProperties() { + Properties props = super.setupProperties(); + setCredentialFromPemFile(props, "oauth.credential"); + checkNotNull(setIfTestSystemPropertyPresent(props, "oauth.endpoint"), "test.oauth.endpoint must be set"); + checkNotNull(setIfTestSystemPropertyPresent(props, AUDIENCE), "test.jclouds.oauth.audience must be set"); + setIfTestSystemPropertyPresent(props, SCOPES); + setIfTestSystemPropertyPresent(props, SIGNATURE_OR_MAC_ALGORITHM); + return props; + } + + protected long nowInSeconds() { + return TimeUnit.SECONDS.convert(Ticker.systemTicker().read(), TimeUnit.NANOSECONDS); + } + +} + diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthAsyncApiExpectTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthAsyncApiExpectTest.java new file mode 100644 index 0000000000..363ccfc22c --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthAsyncApiExpectTest.java @@ -0,0 +1,36 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.internal; + +import com.google.common.base.Function; +import com.google.inject.Module; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.jclouds.oauth.v2.OAuthAsyncApi; + +import java.util.Properties; + +/** + * @author Adrian Cole + */ +public class BaseOAuthAsyncApiExpectTest extends BaseOAuthExpectTest { + public OAuthAsyncApi createClient(Function fn, Module module, Properties props) { + return createInjector(fn, module, props).getInstance(OAuthAsyncApi.class); + } +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthExpectTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthExpectTest.java new file mode 100644 index 0000000000..4bb2da34a7 --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/internal/BaseOAuthExpectTest.java @@ -0,0 +1,31 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.internal; + +import org.jclouds.rest.internal.BaseRestApiExpectTest; + +/** + * @author Adrian Cole + */ +public class BaseOAuthExpectTest extends BaseRestApiExpectTest { + + public BaseOAuthExpectTest() { + provider = "oauth"; + } +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/json/JWTTokenRequestFormatTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/json/JWTTokenRequestFormatTest.java new file mode 100644 index 0000000000..a39a26db36 --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/json/JWTTokenRequestFormatTest.java @@ -0,0 +1,73 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.json; + +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; +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.oauth.v2.domain.TokenRequestFormat; +import org.jclouds.util.Strings2; +import org.testng.annotations.Test; + +import java.io.IOException; + +import static org.jclouds.oauth.v2.internal.Base64UrlSafeTest.STRING_THAT_GENERATES_URL_UNSAFE_BASE64_ENCODING; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; + +/** + * @author David Alves + */ +@Test(groups = "unit") +public class JWTTokenRequestFormatTest { + + public void testPayloadIsUrlSafe() throws IOException { + + + TokenRequestFormat tokenRequestFormat = ContextBuilder.newBuilder(new OAuthApiMetadata()).overrides + (OAuthTestUtils.defaultProperties(null)).build().utils() + .injector().getInstance(TokenRequestFormat.class); + Header header = new Header.Builder().signerAlgorithm("a").type("b").build(); + ClaimSet claimSet = new ClaimSet.Builder().addClaim("ist", STRING_THAT_GENERATES_URL_UNSAFE_BASE64_ENCODING) + .build(); + TokenRequest tokenRequest = new TokenRequest.Builder().claimSet(claimSet).header(header).build(); + HttpRequest request = tokenRequestFormat.formatRequest(HttpRequest.builder().method("GET").endpoint + ("http://localhost").build(), tokenRequest); + + assertNotNull(request.getPayload()); + + String payload = Strings2.toStringAndClose(request.getPayload().getInput()); + + // make sure the paylod is in the format {header}.{claims}.{signature} + Iterable parts = Splitter.on(".").split(payload); + + assertSame(Iterables.size(parts), 3); + + assertTrue(!payload.contains("+")); + assertTrue(!payload.contains("/")); + } +} diff --git a/labs/oauth/src/test/java/org/jclouds/oauth/v2/parse/ParseTokenTest.java b/labs/oauth/src/test/java/org/jclouds/oauth/v2/parse/ParseTokenTest.java new file mode 100644 index 0000000000..c550e8b3c7 --- /dev/null +++ b/labs/oauth/src/test/java/org/jclouds/oauth/v2/parse/ParseTokenTest.java @@ -0,0 +1,46 @@ +/* + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.parse; + +import org.jclouds.json.BaseItemParserTest; +import org.jclouds.oauth.v2.domain.Token; +import org.testng.annotations.Test; + +import javax.ws.rs.Consumes; +import javax.ws.rs.core.MediaType; + +/** + * @author David Alves + */ +@Test(groups = "unit") +public class ParseTokenTest extends BaseItemParserTest { + + @Override + public String resource() { + return "/tokenResponse.json"; + } + + @Override + @Consumes(MediaType.APPLICATION_JSON) + public Token expected() { + return Token.builder().expiresIn(3600).tokenType("Bearer").accessToken + ("1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M").build(); + } +} diff --git a/labs/oauth/src/test/resources/logback.xml b/labs/oauth/src/test/resources/logback.xml new file mode 100644 index 0000000000..9679b2e03a --- /dev/null +++ b/labs/oauth/src/test/resources/logback.xml @@ -0,0 +1,38 @@ + + + + target/test-data/jclouds.log + + + %d %-5p [%c] [%thread] %m%n + + + + + target/test-data/jclouds-wire.log + + + %d %-5p [%c] [%thread] %m%n + + + + + + + + + + + + + + + + + + + + + + + diff --git a/labs/oauth/src/test/resources/testpk.pem b/labs/oauth/src/test/resources/testpk.pem new file mode 100644 index 0000000000..1443074c78 --- /dev/null +++ b/labs/oauth/src/test/resources/testpk.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQCwqwzakEPP+U9vx9JCuMHebFIVQZ4Sjaj2RU9dJ6YT2s3u7dC6 +/0fGM5xm4fXmSHqyGC6PC8weQSkxnSpbU+R4jMWPM8ML4TIr5wP0avbg+wy3+WWI +of0MN7YHkCfqpaaiKiMw7niK1y07YvxJN8LX1xLpE7aXgIpn6L/qtJdHnQIDAQAB +AoGBAIAHlcsW3W3smPrC7sdXqXeNPHcXFH0RmC7Qz9EMmLiuyqqqQagitFsYr/GH +M3Ltd611BNi5jfUm97ly0m1ZAKp/nkTMVhfKfRIVsBDHtjQHcUOR9tr0LslptmaN +TG0bovbUohe5KwOqMK4YOjUQbInChVBrf7VrNQtv8e0eShdpAkEA3lzLP9QYfP1i +C4iYXqS7cgMDrs3qujC7PoyB54maen+Uvgyau1ZJpKMzXYkORPcYk+b71bl9jF80 +U+7LDnJjPwJBAMtksvL1V8DC5DYL43497JW4KBN4YZ3K7YWx/9gkvc3Q9VdXiUGu +6WKjmcbmsPI/jFdeO71uy934N8qEXLJcyiMCQQCTNKcxWD3l8PCJZiJI9ZFKBwjX +Hmb4X+51mBsfpw7nbbKQplOBFbynC4ujrmoN6e8RaubpNGUTGqvPrNQsejmNAkEA +lUDEAH4BczaQ+QgoXI9ceVG2NvNzzrMHMcC5Ggd8MPhR0VIvKsAMC5I6WjcXSe1Q +Mxy3gf84Ix7u8fHHhCuLOQJAQRhrlXiQUk4cJumNhjza5/+KtaV4FPbEQi+qcWE6 +tGoHPEBfbXyUdcUD4ae8X1W0yri0BuyVNaOCpGCBRIhPZA== +-----END RSA PRIVATE KEY----- diff --git a/labs/oauth/src/test/resources/tokenResponse.json b/labs/oauth/src/test/resources/tokenResponse.json new file mode 100644 index 0000000000..6717a550e9 --- /dev/null +++ b/labs/oauth/src/test/resources/tokenResponse.json @@ -0,0 +1,5 @@ +{ + "access_token" : "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M", + "token_type" : "Bearer", + "expires_in" : 3600 +} \ No newline at end of file diff --git a/labs/pom.xml b/labs/pom.xml index ecd833cf74..d31b5132a8 100644 --- a/labs/pom.xml +++ b/labs/pom.xml @@ -18,8 +18,7 @@ specific language governing permissions and limitations under the License. ---> - +--> 4.0.0 jclouds-project @@ -61,5 +60,6 @@ fgcp-au fgcp-de abiquo - + oauth +