From 0420741690a484c5badc9fd7eb83d714e5edf2cc Mon Sep 17 00:00:00 2001 From: Jim Spring Date: Tue, 29 Mar 2016 12:35:04 -0700 Subject: [PATCH] Add support for Azure AD authentication using Service Principal and Password --- apis/oauth/README | 34 +++++-- .../jclouds/oauth/v2/AuthorizationApi.java | 13 +++ .../oauth/v2/config/CredentialType.java | 5 +- .../jclouds/oauth/v2/config/OAuthModule.java | 6 +- .../oauth/v2/config/OAuthProperties.java | 9 ++ .../jclouds/oauth/v2/domain/ClientSecret.java | 49 +++++++++ .../filters/ClientCredentialsSecretFlow.java | 99 +++++++++++++++++++ .../oauth/v2/filters/JWTBearerTokenFlow.java | 11 ++- .../oauth/v2/AuthorizationApiLiveTest.java | 71 +++++++++++-- .../oauth/v2/AuthorizationApiMockTest.java | 45 ++++++++- .../org/jclouds/oauth/v2/OAuthTestUtils.java | 3 + 11 files changed, 316 insertions(+), 29 deletions(-) create mode 100644 apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientSecret.java create mode 100644 apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/ClientCredentialsSecretFlow.java diff --git a/apis/oauth/README b/apis/oauth/README index 40b039d991..7d039b1230 100644 --- a/apis/oauth/README +++ b/apis/oauth/README @@ -1,16 +1,34 @@ In order to use oauth applications must specify the following properties: -Mandatory: +Mandatory, when using non-Azure Active Directory oauth: .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) -Running the live test: +Mandatory, when using oauth to authenticate against Azure Active Directory: +.identity - the application GUID set up for as a Service Principal for your Azure account +.credential - the password associated with the application GUID +oauth.endpoint - the endpoint to use for Azure AD authentication (URL of the form the URL "https://login.microsoftonline.com//oauth2/token") + +For Azure Active Directory, setting up a service principal to work with Azure Resource Manager and Azure AD as well as finding out the is described at https://azure.microsoft.com/en-us/documentation/articles/resource-group-authenticate-service-principal/ + +Running the live test on for non client_credentials oauth grant type: + +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.scope=https://www.googleapis.com/auth/prediction \ + + +To Run the live test against Azure Active Directory which uses the client_credentials grant type: + +mvn clean install -Plive \ +-Dtest.oauth.identity= \ +-Dtest.oauth.credential= \ +-Dtest.oauth.endpoint=https://login.microsoftonline.com//oauth2/token \ +-Dtest.jclouds.oauth.resource=https://management.azure.com/ \ +-Dtest.jclouds.oauth.credential-type=clientCredentialsSecret -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.scope=https://www.googleapis.com/auth/prediction \ No newline at end of file diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java index fae1761e76..b77a8deb18 100644 --- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java @@ -25,6 +25,7 @@ import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; import javax.ws.rs.POST; +import org.jclouds.javax.annotation.Nullable; import org.jclouds.oauth.v2.OAuthFallbacks.AuthorizationExceptionOn4xx; import org.jclouds.oauth.v2.config.Authorization; import org.jclouds.oauth.v2.domain.Claims; @@ -46,4 +47,16 @@ public interface AuthorizationApi extends Closeable { @Consumes(APPLICATION_JSON) @Fallback(AuthorizationExceptionOn4xx.class) Token authorize(@FormParam("assertion") @ParamParser(ClaimsToAssertion.class) Claims claims); + + @Named("oauth2:authorize_client_secret") + @POST + @FormParams(keys = "grant_type", values = "client_credentials") + @Consumes(APPLICATION_JSON) + @Fallback(AuthorizationExceptionOn4xx.class) + Token authorizeClientSecret( + @FormParam("client_id") String client_id, + @FormParam("client_secret") String client_secret, + @FormParam("resource") String resource, + @FormParam("scope") @Nullable String scope + ); } diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java index 141564b57d..b80f4c4813 100644 --- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java @@ -26,7 +26,10 @@ public enum CredentialType { BEARER_TOKEN_CREDENTIALS, /** Contents are a PEM-encoded P12 Private Key. */ - P12_PRIVATE_KEY_CREDENTIALS; + P12_PRIVATE_KEY_CREDENTIALS, + + /** Contents are an ID and Secret */ + CLIENT_CREDENTIALS_SECRET; @Override public String toString() { return UPPER_UNDERSCORE.to(LOWER_CAMEL, name()); diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java index 8a3568ce99..dc18c5d3b9 100644 --- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java @@ -28,6 +28,7 @@ import javax.inject.Singleton; import org.jclouds.oauth.v2.AuthorizationApi; import org.jclouds.oauth.v2.filters.BearerTokenFromCredentials; +import org.jclouds.oauth.v2.filters.ClientCredentialsSecretFlow; import org.jclouds.oauth.v2.filters.JWTBearerTokenFlow; import org.jclouds.oauth.v2.filters.OAuthFilter; @@ -69,12 +70,15 @@ public final class OAuthModule extends AbstractModule { @Singleton protected OAuthFilter authenticationFilterForCredentialType(CredentialType credentialType, JWTBearerTokenFlow serviceAccountAuth, - BearerTokenFromCredentials bearerTokenAuth) { + BearerTokenFromCredentials bearerTokenAuth, + ClientCredentialsSecretFlow clientCredentialAuth) { switch (credentialType) { case P12_PRIVATE_KEY_CREDENTIALS: return serviceAccountAuth; case BEARER_TOKEN_CREDENTIALS: return bearerTokenAuth; + case CLIENT_CREDENTIALS_SECRET: + return clientCredentialAuth; default: throw new IllegalArgumentException("Unsupported credential type: " + credentialType); } diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java index 7faed0cbad..87b5ca9599 100644 --- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java @@ -36,6 +36,15 @@ public final class OAuthProperties { */ public static final String CREDENTIAL_TYPE = "jclouds.oauth.credential-type"; + /** + * When using oauth with Azure Active Direction and Client Credentials, a "resource" must + * be specified as part of the request. + * + * @see doc + */ + public static final String RESOURCE = "jclouds.oauth.resource"; + private OAuthProperties() { } } + diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientSecret.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientSecret.java new file mode 100644 index 0000000000..d66469552b --- /dev/null +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientSecret.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jclouds.oauth.v2.domain; + +import org.jclouds.json.SerializedNames; +import com.google.auto.value.AutoValue; + +/** + * Details corresponding the a client_credential Azure AD Oauth request + */ +@AutoValue +public abstract class ClientSecret { + /** The ID of the client. **/ + public abstract String clientId(); + + /** The secret of the client. **/ + public abstract String clientSecret(); + + /** The resource to authorize against. **/ + public abstract String resource(); + + /** The scope(s) to authorize against. **/ + public abstract String scope(); + + /** When does the token expire. **/ + public abstract long expire(); + + @SerializedNames({ "client_id", "client_secret", "resource", "scope", "expire" }) + public static ClientSecret create(String clientId, String clientSecret, String resource, String scope, long expire) { + return new AutoValue_ClientSecret(clientId, clientSecret, resource, scope, expire); + } + + ClientSecret() { + } +} diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/ClientCredentialsSecretFlow.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/ClientCredentialsSecretFlow.java new file mode 100644 index 0000000000..562e6f09bc --- /dev/null +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/ClientCredentialsSecretFlow.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jclouds.oauth.v2.filters; + +import com.google.common.base.Joiner; +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 org.jclouds.oauth.v2.AuthorizationApi; +import org.jclouds.oauth.v2.domain.ClientSecret; +import org.jclouds.oauth.v2.config.OAuthScopes; +import org.jclouds.oauth.v2.domain.Token; +import org.jclouds.domain.Credentials; +import org.jclouds.http.HttpException; +import org.jclouds.http.HttpRequest; +import org.jclouds.location.Provider; + +import javax.inject.Named; +import com.google.inject.Inject; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL; +import static org.jclouds.oauth.v2.config.OAuthProperties.RESOURCE; + +/** + * Authorizes new Bearer Tokens at runtime by sending up for the http request. + * + * To retrieve the Bearer Token, a request of grant_type=client_credentials is + * used. The credential supplied is a password. + * + *

Cache

+ * This maintains a time-based Bearer Token cache. By default expires after 59 minutes + * (the maximum time a token is valid is 60 minutes). + */ +public class ClientCredentialsSecretFlow implements OAuthFilter { + private static final Joiner ON_SPACE = Joiner.on(" "); + + private final Supplier credentialsSupplier; + private final long tokenDuration; + private final LoadingCache tokenCache; + @Inject(optional = true) @Named(RESOURCE) private String resource; + @Inject(optional = true) private OAuthScopes scopes; + + @Inject + ClientCredentialsSecretFlow(AuthorizeToken loader, @Named(PROPERTY_SESSION_INTERVAL) long tokenDuration, + @Provider Supplier credentialsSupplier) { + this.credentialsSupplier = credentialsSupplier; + this.tokenDuration = tokenDuration; + // 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 + long cacheExpirationSeconds = tokenDuration > 30 ? tokenDuration - 30 : tokenDuration; + this.tokenCache = CacheBuilder.newBuilder().expireAfterWrite(cacheExpirationSeconds, SECONDS).build(loader); + } + + static final class AuthorizeToken extends CacheLoader { + private final AuthorizationApi api; + + @Inject AuthorizeToken(AuthorizationApi api) { + this.api = api; + } + + @Override public Token load(ClientSecret key) throws Exception { + return api.authorizeClientSecret(key.clientId(), key.clientSecret(), key.resource(), key.scope()); + } + } + + @Override public HttpRequest filter(HttpRequest request) throws HttpException { + long now = currentTimeSeconds(); + ClientSecret client = ClientSecret.create( + credentialsSupplier.get().identity, + credentialsSupplier.get().credential, + resource == null ? "" : resource, + scopes == null ? null : ON_SPACE.join(scopes.forRequest(request)), + now + tokenDuration + ); + Token token = tokenCache.getUnchecked(client); + String authorization = String.format("%s %s", token.tokenType(), token.accessToken()); + return request.toBuilder().addHeader("Authorization", authorization).build(); + } + + long currentTimeSeconds() { + return System.currentTimeMillis() / 1000; + } +} diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/JWTBearerTokenFlow.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/JWTBearerTokenFlow.java index b8625168e1..05ccf2ab00 100644 --- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/JWTBearerTokenFlow.java +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/JWTBearerTokenFlow.java @@ -19,6 +19,7 @@ package org.jclouds.oauth.v2.filters; import static java.util.concurrent.TimeUnit.SECONDS; import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL; import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE; +import static com.google.common.base.Preconditions.checkNotNull; import javax.inject.Inject; import javax.inject.Named; @@ -50,7 +51,7 @@ import com.google.common.cache.LoadingCache; public class JWTBearerTokenFlow implements OAuthFilter { private static final Joiner ON_COMMA = Joiner.on(","); - private final String audience; + @com.google.inject.Inject(optional = true) @Named(AUDIENCE) private String audience; private final Supplier credentialsSupplier; private final OAuthScopes scopes; private final long tokenDuration; @@ -59,8 +60,8 @@ public class JWTBearerTokenFlow implements OAuthFilter { public static class TestJWTBearerTokenFlow extends JWTBearerTokenFlow { @Inject TestJWTBearerTokenFlow(AuthorizeToken loader, @Named(PROPERTY_SESSION_INTERVAL) long tokenDuration, - @Named(AUDIENCE) String audience, @Provider Supplier credentialsSupplier, OAuthScopes scopes) { - super(loader, tokenDuration, audience, credentialsSupplier, scopes); + @Provider Supplier credentialsSupplier, OAuthScopes scopes) { + super(loader, tokenDuration, credentialsSupplier, scopes); } /** Constant time for testing. */ @@ -70,8 +71,7 @@ public class JWTBearerTokenFlow implements OAuthFilter { } @Inject JWTBearerTokenFlow(AuthorizeToken loader, @Named(PROPERTY_SESSION_INTERVAL) long tokenDuration, - @Named(AUDIENCE) String audience, @Provider Supplier credentialsSupplier, OAuthScopes scopes) { - this.audience = audience; + @Provider Supplier credentialsSupplier, OAuthScopes scopes) { this.credentialsSupplier = credentialsSupplier; this.scopes = scopes; this.tokenDuration = tokenDuration; @@ -94,6 +94,7 @@ public class JWTBearerTokenFlow implements OAuthFilter { } @Override public HttpRequest filter(HttpRequest request) throws HttpException { + checkNotNull(audience, AUDIENCE); long now = currentTimeSeconds(); Claims claims = Claims.create( // credentialsSupplier.get().identity, // iss diff --git a/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java b/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java index c78277fed1..1ded5dcd0c 100644 --- a/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java +++ b/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java @@ -18,20 +18,24 @@ package org.jclouds.oauth.v2; import static com.google.common.base.Preconditions.checkNotNull; import static org.jclouds.oauth.v2.OAuthTestUtils.setCredential; -import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE; import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG; +import static org.jclouds.oauth.v2.config.OAuthProperties.RESOURCE; +import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE; +import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE; import static org.jclouds.providers.AnonymousProviderMetadata.forApiOnEndpoint; import static org.testng.Assert.assertNotNull; import java.util.Properties; import org.jclouds.apis.BaseApiLiveTest; +import org.jclouds.oauth.v2.config.CredentialType; import org.jclouds.oauth.v2.config.OAuthModule; import org.jclouds.oauth.v2.config.OAuthScopes; import org.jclouds.oauth.v2.config.OAuthScopes.SingleScope; import org.jclouds.oauth.v2.domain.Claims; import org.jclouds.oauth.v2.domain.Token; import org.jclouds.providers.ProviderMetadata; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import com.google.common.collect.ImmutableList; @@ -45,19 +49,34 @@ public class AuthorizationApiLiveTest extends BaseApiLiveTest private final String jwsAlg = "RS256"; private String scope; private String audience; + private String credentialType; + private String resource; public AuthorizationApiLiveTest() { provider = "oauth"; } - public void authenticateJWTToken() throws Exception { + @DataProvider + public Object[][] onlyRunForP12PrivateKeyCredentials() { + return (CredentialType.fromValue(credentialType) == CredentialType.P12_PRIVATE_KEY_CREDENTIALS) ? + OAuthTestUtils.SINGLE_NO_ARG_INVOCATION : OAuthTestUtils.NO_INVOCATIONS; + } + + @DataProvider + public Object[][] onlyRunForClientCredentialsSecret() { + return (CredentialType.fromValue(credentialType) == CredentialType.CLIENT_CREDENTIALS_SECRET) ? + OAuthTestUtils.SINGLE_NO_ARG_INVOCATION : OAuthTestUtils.NO_INVOCATIONS; + } + + @Test(dataProvider = "onlyRunForP12PrivateKeyCredentials") + public void authenticateP12PrivateKeyCredentialsTest() throws Exception { long now = System.currentTimeMillis() / 1000; Claims claims = Claims.create( - identity, // iss - scope, // scope - audience, // aud - now + 3600, // exp - now // iat + identity, // iss + scope, // scope + audience, // aud + now + 3600, // exp + now // iat ); Token token = api.authorize(claims); @@ -65,6 +84,20 @@ public class AuthorizationApiLiveTest extends BaseApiLiveTest assertNotNull(token, "no token when authorizing " + claims); } + @Test(dataProvider = "onlyRunForClientCredentialsSecret") + public void authenticateClientCredentialsSecretTest() throws Exception { + Token token = api.authorizeClientSecret(identity, credential, resource, scope); + + assertNotNull(token, "no token when authorizing " + identity); + } + + @Test(dataProvider = "onlyRunForClientCredentialsSecret") + public void authenticateClientCredentialsSecretNullScopeTest() throws Exception { + Token token = api.authorizeClientSecret(identity, credential, resource, null); + + assertNotNull(token, "no token when authorizing " + identity); + } + /** OAuth isn't registered as a provider intentionally, so we fake one. */ @Override protected ProviderMetadata createProviderMetadata() { return forApiOnEndpoint(AuthorizationApi.class, endpoint).toBuilder().id("oauth").build(); @@ -73,9 +106,27 @@ public class AuthorizationApiLiveTest extends BaseApiLiveTest @Override protected Properties setupProperties() { Properties props = super.setupProperties(); props.setProperty(JWS_ALG, jwsAlg); - credential = setCredential(props, "oauth.credential"); - audience = checkNotNull(setIfTestSystemPropertyPresent(props, AUDIENCE), "test.jclouds.oauth.audience"); - scope = checkNotNull(setIfTestSystemPropertyPresent(props, "jclouds.oauth.scope"), "test.jclouds.oauth.scope"); + + // scope is required for P12_PRIVATE_KEY_CREDENTIALS, optional for CLIENT_CREDENTIALS_SECRET. + // Moved the not-NULL check to P12_PRIVATE_KEY_CREDENTIALS specific parameters. + scope = setIfTestSystemPropertyPresent(props, "jclouds.oauth.scope"); + + // Determine which type of Credential to use, default to P12_PRIVATE_KEY_CREDENTIALS + credentialType = setIfTestSystemPropertyPresent(props, CREDENTIAL_TYPE); + if (credentialType == null) { + credentialType = CredentialType.P12_PRIVATE_KEY_CREDENTIALS.toString(); + props.setProperty(CREDENTIAL_TYPE, credentialType); + } + + // Set the credential specific properties. + if (CredentialType.fromValue(credentialType) == CredentialType.CLIENT_CREDENTIALS_SECRET) { + resource = checkNotNull(setIfTestSystemPropertyPresent(props, RESOURCE), "test." + RESOURCE); + } else if (CredentialType.fromValue(credentialType) == CredentialType.P12_PRIVATE_KEY_CREDENTIALS) { + audience = checkNotNull(setIfTestSystemPropertyPresent(props, AUDIENCE), "test.jclouds.oauth.audience"); + credential = setCredential(props, "oauth.credential"); + checkNotNull(scope, "test.jclouds.oauth.scope"); + } + return props; } diff --git a/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java b/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java index 7e30121743..adc7585d79 100644 --- a/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java +++ b/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java @@ -22,7 +22,9 @@ import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static org.jclouds.Constants.PROPERTY_MAX_RETRIES; import static org.jclouds.oauth.v2.config.CredentialType.P12_PRIVATE_KEY_CREDENTIALS; +import static org.jclouds.oauth.v2.config.CredentialType.CLIENT_CREDENTIALS_SECRET; import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE; +import static org.jclouds.oauth.v2.config.OAuthProperties.RESOURCE; import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE; import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG; import static org.jclouds.util.Strings2.toStringAndClose; @@ -35,6 +37,7 @@ import java.util.Properties; import org.jclouds.ContextBuilder; import org.jclouds.concurrent.config.ExecutorServiceModule; +import org.jclouds.oauth.v2.config.CredentialType; import org.jclouds.oauth.v2.config.OAuthModule; import org.jclouds.oauth.v2.config.OAuthScopes; import org.jclouds.oauth.v2.config.OAuthScopes.SingleScope; @@ -82,7 +85,7 @@ public class AuthorizationApiMockTest { + " \"token_type\" : \"Bearer\",\n" + " \"expires_in\" : 3600\n" + "}")); server.play(); - AuthorizationApi api = api(server.getUrl("/")); + AuthorizationApi api = api(server.getUrl("/"), P12_PRIVATE_KEY_CREDENTIALS); assertEquals(api.authorize(CLAIMS), TOKEN); @@ -115,7 +118,7 @@ public class AuthorizationApiMockTest { server.enqueue(new MockResponse().setResponseCode(400)); server.play(); - AuthorizationApi api = api(server.getUrl("/")); + AuthorizationApi api = api(server.getUrl("/"), P12_PRIVATE_KEY_CREDENTIALS); api.authorize(CLAIMS); fail("An AuthorizationException should have been raised"); } catch (AuthorizationException ex) { @@ -125,14 +128,48 @@ public class AuthorizationApiMockTest { } } + public void testGenerateClientSecretRequest() throws Exception { + MockWebServer server = new MockWebServer(); + + String credential = "password"; + String identity = "user"; + String resource = "http://management.azure.com/"; + String encoded_resource = "http%3A//management.azure.com/"; + + try { + server.enqueue(new MockResponse().setBody("{\n" + + " \"access_token\" : \"1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M\",\n" + + " \"token_type\" : \"Bearer\",\n" + " \"expires_in\" : 3600\n" + "}")); + server.play(); + + AuthorizationApi api = api(server.getUrl("/"), CLIENT_CREDENTIALS_SECRET); + + assertEquals(api.authorizeClientSecret(identity, credential, resource, null), TOKEN); + + RecordedRequest request = server.takeRequest(); + assertEquals(request.getMethod(), "POST"); + assertEquals(request.getHeader("Accept"), APPLICATION_JSON); + assertEquals(request.getHeader("Content-Type"), "application/x-www-form-urlencoded"); + + assertEquals( + new String(request.getBody(), UTF_8), // + "grant_type=client_credentials&client_id=" + identity + + "&client_secret=" + credential + + "&resource=" + encoded_resource); + } finally { + server.shutdown(); + } + } + private final BaseEncoding encoding = base64Url().omitPadding(); - private AuthorizationApi api(URL url) throws IOException { + private AuthorizationApi api(URL url, CredentialType credentialType) throws IOException { Properties overrides = new Properties(); overrides.setProperty("oauth.endpoint", url.toString()); overrides.setProperty(JWS_ALG, "RS256"); - overrides.setProperty(CREDENTIAL_TYPE, P12_PRIVATE_KEY_CREDENTIALS.toString()); + overrides.setProperty(CREDENTIAL_TYPE, credentialType.toString()); overrides.setProperty(AUDIENCE, "https://accounts.google.com/o/oauth2/token"); + overrides.setProperty(RESOURCE, "https://management.azure.com/"); overrides.setProperty(PROPERTY_MAX_RETRIES, "1"); return ContextBuilder.newBuilder(AnonymousHttpApiMetadata.forApi(AuthorizationApi.class)) diff --git a/apis/oauth/src/test/java/org/jclouds/oauth/v2/OAuthTestUtils.java b/apis/oauth/src/test/java/org/jclouds/oauth/v2/OAuthTestUtils.java index e5ec5ec68d..e2ac660734 100644 --- a/apis/oauth/src/test/java/org/jclouds/oauth/v2/OAuthTestUtils.java +++ b/apis/oauth/src/test/java/org/jclouds/oauth/v2/OAuthTestUtils.java @@ -32,6 +32,9 @@ import com.google.common.io.Files; public class OAuthTestUtils { + public static final Object[][] NO_INVOCATIONS = new Object[0][0]; + public static final Object[][] SINGLE_NO_ARG_INVOCATION = { new Object[0] }; + public static Properties defaultProperties(Properties properties) { try { properties = properties == null ? new Properties() : properties;