* Refactor OAuth to be less complex.

* Remove oauth as a standalone api.
 * Rename redundant OAuthAuthenticationFilter to OAuthFilter.
 * Make AuthorizationApi more simple by using form semantics.
 * Simplified OAuth by only permitting RS256 and none algos.
This commit is contained in:
Adrian Cole 2014-11-17 19:50:48 -08:00
parent a35d73c6d0
commit 46a7351a8a
36 changed files with 429 additions and 1494 deletions

View File

@ -35,7 +35,6 @@
<test.oauth.identity>FIX_ME</test.oauth.identity>
<test.oauth.credential>FIX_ME</test.oauth.credential>
<test.oauth.endpoint>FIX_ME</test.oauth.endpoint>
<test.jclouds.oauth.jws-alg>RS256</test.jclouds.oauth.jws-alg>
<test.jclouds.oauth.audience>FIX_ME</test.jclouds.oauth.audience>
<test.jclouds.oauth.scope>FIX_ME</test.jclouds.oauth.scope>
<test.oauth.api-version>2</test.oauth.api-version>

View File

@ -14,27 +14,33 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.parse;
package org.jclouds.oauth.v2;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import java.io.Closeable;
import javax.inject.Named;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import org.jclouds.json.BaseItemParserTest;
import org.jclouds.oauth.v2.functions.ClaimsToAssertion;
import org.jclouds.oauth.v2.config.Authorization;
import org.jclouds.oauth.v2.domain.Claims;
import org.jclouds.oauth.v2.domain.Token;
import org.testng.annotations.Test;
import org.jclouds.rest.annotations.Endpoint;
import org.jclouds.rest.annotations.FormParams;
import org.jclouds.rest.annotations.ParamParser;
@Test(groups = "unit", testName = "ParseTokenTest")
public class ParseTokenTest extends BaseItemParserTest<Token> {
@Override
public String resource() {
return "/tokenResponse.json";
}
@Override
/**
* Binds to an OAuth2 <a href="http://tools.ietf.org/html/rfc6749#section-3.1">authorization endpoint</a>.
*/
@Endpoint(Authorization.class)
public interface AuthorizationApi extends Closeable {
@Named("oauth2:authorize")
@POST
@FormParams(keys = "grant_type", values = "urn:ietf:params:oauth:grant-type:jwt-bearer")
@Consumes(APPLICATION_JSON)
public Token expected() {
return Token.create("1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M", "Bearer", 3600);
}
Token authorize(@FormParam("assertion") @ParamParser(ClaimsToAssertion.class) Claims claims);
}

View File

@ -1,75 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import com.google.common.collect.ImmutableList;
/**
* JSON Web Signature Algorithms
* <p/>
* We only support <a href="http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-36#section-3.1">required
* or recommended algorithms</a>, with the exception of {@code none}, which is only supported in tests.
*/
public final class JWSAlgorithms {
/** This is a marker algorithm only supported in tests. */
public static final String NONE = "none";
private static final List<String> SUPPORTED_ALGS = ImmutableList.of("ES256", "RS256", "HS256", NONE);
/**
* Static mapping between the oauth algorithm name and the Crypto provider signature algorithm name and KeyFactory.
*/
private static final List<List<String>> ALG_TO_SIGNATURE_ALG_AND_KEY_FACTORY = ImmutableList.<List<String>>of( //
ImmutableList.of(SUPPORTED_ALGS.get(0), "SHA256withECDSA", "EC"), // ECDSA using P-256 and SHA-256
ImmutableList.of(SUPPORTED_ALGS.get(1), "SHA256withRSA", "RSA"), // RSASSA-PKCS-v1_5 using SHA-256
ImmutableList.of(SUPPORTED_ALGS.get(2), "HmacSHA256", "DiffieHellman") // HMAC using SHA-256
);
/** Ordered list of supported algorithms by recommendation. */
public static List<String> supportedAlgs() {
return SUPPORTED_ALGS;
}
public static String macOrSignature(String jwsAlg) {
return ALG_TO_SIGNATURE_ALG_AND_KEY_FACTORY.get(indexOf(jwsAlg)).get(1);
}
public static KeyFactory keyFactory(String jwsAlg) {
String keyFactoryAlgorithm = ALG_TO_SIGNATURE_ALG_AND_KEY_FACTORY.get(indexOf(jwsAlg)).get(2);
try {
return KeyFactory.getInstance(keyFactoryAlgorithm);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("Invalid contents in JWSAlgorithms! " + e.getMessage());
}
}
private static int indexOf(String jwsAlg) {
int result = SUPPORTED_ALGS.indexOf(checkNotNull(jwsAlg, "jwsAlg"));
checkArgument(result != -1, "JSON Web Signature alg %s is not in the supported list %s", jwsAlg, SUPPORTED_ALGS);
return result;
}
private JWSAlgorithms() {
}
}

View File

@ -1,61 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import java.io.Closeable;
import javax.inject.Named;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import org.jclouds.oauth.v2.binders.TokenBinder;
import org.jclouds.oauth.v2.config.OAuth;
import org.jclouds.oauth.v2.domain.Token;
import org.jclouds.oauth.v2.domain.TokenRequest;
import org.jclouds.rest.AuthorizationException;
import org.jclouds.rest.annotations.BinderParam;
import org.jclouds.rest.annotations.Endpoint;
/**
* Provides access to OAuth via REST api.
* <p/>
* 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.
*/
@Endpoint(OAuth.class)
public interface OAuthApi extends Closeable {
/**
* 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.
*/
@Named("authenticate")
@POST
@Consumes(APPLICATION_JSON)
Token authenticate(@BinderParam(TokenBinder.class) TokenRequest tokenRequest) throws AuthorizationException;
}

View File

@ -1,83 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2;
import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
import static org.jclouds.oauth.v2.config.CredentialType.P12_PRIVATE_KEY_CREDENTIALS;
import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE;
import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
import java.net.URI;
import java.util.Properties;
import org.jclouds.apis.ApiMetadata;
import org.jclouds.oauth.v2.config.OAuthHttpApiModule;
import org.jclouds.oauth.v2.config.OAuthModule;
import org.jclouds.rest.internal.BaseHttpApiMetadata;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Module;
@AutoService(ApiMetadata.class)
public class OAuthApiMetadata extends BaseHttpApiMetadata<OAuthApi> {
@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 = BaseHttpApiMetadata.defaultProperties();
properties.put(JWS_ALG, "RS256");
properties.put(CREDENTIAL_TYPE, P12_PRIVATE_KEY_CREDENTIALS);
properties.put(PROPERTY_SESSION_INTERVAL, 3600);
return properties;
}
public static class Builder extends BaseHttpApiMetadata.Builder<OAuthApi, Builder> {
protected Builder() {
id("oauth")
.name("OAuth API")
.identityName("service_account")
.credentialName("service_key")
.documentation(URI.create("TODO"))
.version("2")
.defaultProperties(OAuthApiMetadata.defaultProperties())
.defaultModules(ImmutableSet.<Class<? extends Module>>of(OAuthModule.class, OAuthHttpApiModule.class));
}
@Override
public OAuthApiMetadata build() {
return new OAuthApiMetadata(this);
}
@Override
protected Builder self() {
return this;
}
}
}

View File

@ -1,77 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.binders;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Joiner.on;
import static com.google.common.io.BaseEncoding.base64Url;
import static org.jclouds.io.Payloads.newUrlEncodedFormPayload;
import javax.inject.Inject;
import org.jclouds.http.HttpRequest;
import org.jclouds.io.Payload;
import org.jclouds.json.Json;
import org.jclouds.oauth.v2.domain.TokenRequest;
import org.jclouds.rest.Binder;
import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMultimap;
/**
* Formats a token request into JWT format namely:
* <ol>
* <li>Transforms the token request to json.</li>
* <li>Creates the base64 header.claimset portions of the payload.</li>
* <li>Uses the provided signer function to create a signature.</li>
* <li>Creates the full url encoded payload as described in: <a href="https://developers.google.com/accounts/docs/OAuth2ServiceAccount">OAuth2ServiceAccount</a></li>
* </ol>
*/
public final class TokenBinder implements Binder {
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 Supplier<Function<byte[], byte[]>> signer;
private final Json json;
@Inject TokenBinder(Supplier<Function<byte[], byte[]>> signer, Json json) {
this.signer = signer;
this.json = json;
}
@Override public <R extends HttpRequest> R bindToRequest(R request, Object input) {
TokenRequest tokenRequest = (TokenRequest) input;
String encodedHeader = json.toJson(tokenRequest.header());
String encodedClaimSet = json.toJson(tokenRequest.claimSet());
encodedHeader = base64Url().omitPadding().encode(encodedHeader.getBytes(UTF_8));
encodedClaimSet = base64Url().omitPadding().encode(encodedClaimSet.getBytes(UTF_8));
byte[] signature = signer.get().apply(on(".").join(encodedHeader, encodedClaimSet).getBytes(UTF_8));
String encodedSignature = signature != null ? base64Url().omitPadding().encode(signature) : "";
// the final assertion in base 64 encoded {header}.{claimSet}.{signature} format
String assertion = on(".").join(encodedHeader, encodedClaimSet, encodedSignature);
Payload payload = newUrlEncodedFormPayload(ImmutableMultimap.<String, String> builder()
.put(GRANT_TYPE_FORM_PARAM, GRANT_TYPE_JWT_BEARER)
.put(ASSERTION_FORM_PARAM, assertion).build());
return (R) request.toBuilder().payload(payload).build();
}
}

View File

@ -23,13 +23,8 @@ import java.lang.annotation.Target;
import javax.inject.Qualifier;
/**
* Qualifies OAuth related resources, such as Endpoint.
*
* @see org.jclouds.oauth.v2.OAuthApi
*/
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Qualifier
public @interface OAuth {
public @interface Authorization {
}

View File

@ -1,46 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.config;
import static org.jclouds.rest.config.BinderUtils.bindHttpApi;
import java.net.URI;
import javax.inject.Named;
import javax.inject.Singleton;
import org.jclouds.oauth.v2.OAuthApi;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
public class OAuthAuthenticationModule extends AbstractModule {
@Override
protected void configure() {
bindHttpApi(binder(), OAuthApi.class);
}
@Provides
@Singleton
@OAuth
protected Supplier<URI> oauthEndpoint(@Named("oauth.endpoint") String endpoint) {
return Suppliers.ofInstance(URI.create(endpoint));
}
}

View File

@ -1,42 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.config;
import java.net.URI;
import javax.inject.Singleton;
import org.jclouds.oauth.v2.OAuthApi;
import org.jclouds.providers.ProviderMetadata;
import org.jclouds.rest.ConfiguresHttpApi;
import org.jclouds.rest.config.HttpApiModule;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.inject.Provides;
/** Api module to when accessing OAuth stand-alone. */
@ConfiguresHttpApi
public class OAuthHttpApiModule extends HttpApiModule<OAuthApi> {
@Provides
@Singleton
@OAuth
protected Supplier<URI> provideAuthenticationEndpoint(ProviderMetadata providerMetadata) {
return Suppliers.ofInstance(URI.create(providerMetadata.getEndpoint()));
}
}

View File

@ -16,82 +16,48 @@
*/
package org.jclouds.oauth.v2.config;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
import static org.jclouds.oauth.v2.JWSAlgorithms.NONE;
import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
import static org.jclouds.oauth.v2.config.CredentialType.P12_PRIVATE_KEY_CREDENTIALS;
import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE;
import static org.jclouds.rest.config.BinderUtils.bindHttpApi;
import java.net.URI;
import java.security.PrivateKey;
import org.jclouds.http.HttpRequest;
import org.jclouds.oauth.v2.domain.Token;
import org.jclouds.oauth.v2.domain.TokenRequest;
import org.jclouds.oauth.v2.filters.BearerTokenAuthenticator;
import org.jclouds.oauth.v2.filters.OAuthAuthenticationFilter;
import org.jclouds.oauth.v2.filters.OAuthAuthenticator;
import org.jclouds.oauth.v2.functions.BuildTokenRequest;
import org.jclouds.oauth.v2.functions.FetchToken;
import org.jclouds.oauth.v2.functions.PrivateKeySupplier;
import org.jclouds.oauth.v2.functions.SignOrProduceMacForToken;
import javax.inject.Named;
import javax.inject.Singleton;
import org.jclouds.oauth.v2.AuthorizationApi;
import org.jclouds.oauth.v2.filters.BearerTokenFromCredentials;
import org.jclouds.oauth.v2.filters.JWTBearerTokenFlow;
import org.jclouds.oauth.v2.filters.OAuthFilter;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
public class OAuthModule extends AbstractModule {
public final class OAuthModule extends AbstractModule {
@Override protected void configure() {
bindHttpApi(binder(), AuthorizationApi.class);
bind(CredentialType.class).toProvider(CredentialTypeFromPropertyOrDefault.class);
bind(new TypeLiteral<Function<HttpRequest, TokenRequest>>() {}).to(BuildTokenRequest.class);
bind(new TypeLiteral<Function<TokenRequest, Token>>() {}).to(FetchToken.class);
bind(new TypeLiteral<Supplier<PrivateKey>>() {}).annotatedWith(OAuth.class).to(PrivateKeySupplier.class);
bind(new TypeLiteral<Supplier<PrivateKey>>() {}).annotatedWith(Authorization.class).to(PrivateKeySupplier.class);
}
/**
* Provides a cache for tokens. Cache is time based and by default expires after 59 minutes
* (the maximum time a token is valid is 60 minutes).
* This cache and expiry period is system-wide and does not attend to per-instance expiry time
* (e.g. "expires_in" from Google Compute -- which is set to the standard 3600 seconds).
*/
// NB: If per-instance expiry time is required, significant refactoring will be needed.
@Provides
@Singleton
public LoadingCache<TokenRequest, Token> provideAccessCache(Function<TokenRequest, Token> getAccess,
@Named(PROPERTY_SESSION_INTERVAL) long expirationSeconds) {
// 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
expirationSeconds = expirationSeconds > 30 ? expirationSeconds - 30 : expirationSeconds;
return CacheBuilder.newBuilder().expireAfterWrite(expirationSeconds, SECONDS).build(CacheLoader.from(getAccess));
}
/**
* Defers instantiation of {@linkplain SignOrProduceMacForToken} so as to avoid requiring private keys when the alg
* is set to {@linkplain org.jclouds.oauth.v2.JWSAlgorithms#NONE}.
*/
@Provides @Singleton Supplier<Function<byte[], byte[]>> signOrProduceMacForToken(@Named(JWS_ALG) String jwsAlg,
Provider<SignOrProduceMacForToken> in) {
if (jwsAlg.equals(NONE)) { // Current implementation requires we return null on none.
return (Supplier) Suppliers.ofInstance(Functions.constant(null));
}
return Suppliers.memoize(in.get());
@Authorization
protected Supplier<URI> oauthEndpoint(@javax.inject.Named("oauth.endpoint") String endpoint) {
return Suppliers.ofInstance(URI.create(endpoint));
}
@Singleton
public static class CredentialTypeFromPropertyOrDefault implements Provider<CredentialType> {
@Inject(optional = true)
@Named(OAuthProperties.CREDENTIAL_TYPE)
String credentialType = CredentialType.P12_PRIVATE_KEY_CREDENTIALS.toString();
@Named(CREDENTIAL_TYPE)
String credentialType = P12_PRIVATE_KEY_CREDENTIALS.toString();
@Override
public CredentialType get() {
@ -101,9 +67,9 @@ public class OAuthModule extends AbstractModule {
@Provides
@Singleton
protected OAuthAuthenticationFilter authenticationFilterForCredentialType(CredentialType credentialType,
OAuthAuthenticator serviceAccountAuth,
BearerTokenAuthenticator bearerTokenAuth) {
protected OAuthFilter authenticationFilterForCredentialType(CredentialType credentialType,
JWTBearerTokenFlow serviceAccountAuth,
BearerTokenFromCredentials bearerTokenAuth) {
switch (credentialType) {
case P12_PRIVATE_KEY_CREDENTIALS:
return serviceAccountAuth;

View File

@ -16,18 +16,16 @@
*/
package org.jclouds.oauth.v2.config;
import org.jclouds.oauth.v2.JWSAlgorithms;
public final class OAuthProperties {
public class OAuthProperties {
/** The JSON Web Signature alg, from the {@link JWSAlgorithms#supportedAlgs() supported list}. */
/** The JSON Web Signature alg, must be {@code RS256} or {@code none}. */
public static final String JWS_ALG = "jclouds.oauth.jws-alg";
/**
* 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 <a href="http://tools.ietf.org/html/draft-jones-json-web-token-04">doc</a>
* @see <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30">doc</a>
*/
public static final String AUDIENCE = "jclouds.oauth.audience";
@ -38,4 +36,6 @@ public class OAuthProperties {
*/
public static final String CREDENTIAL_TYPE = "jclouds.oauth.credential-type";
private OAuthProperties() {
}
}

View File

@ -14,27 +14,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.functions;
package org.jclouds.oauth.v2.config;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.propagate;
import static org.jclouds.crypto.Pems.privateKeySpec;
import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
import static org.jclouds.util.Throwables2.getFirstThrowableOfType;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.jclouds.domain.Credentials;
import org.jclouds.location.Provider;
import org.jclouds.oauth.v2.JWSAlgorithms;
import org.jclouds.rest.AuthorizationException;
import com.google.common.annotations.VisibleForTesting;
@ -46,11 +44,11 @@ import com.google.common.io.ByteSource;
import com.google.common.util.concurrent.UncheckedExecutionException;
/**
* Loads {@link PrivateKey} from a pem private key using the KeyFactory obtained vi {@link
* JWSAlgorithms#keyFactory(String)}. The pem pk algorithm must match the KeyFactory algorithm.
* Loads {@link PrivateKey} from a pem private key using and RSA KeyFactory. The pem pk algorithm must match the
* KeyFactory algorithm.
*/
@Singleton // due to cache
public final class PrivateKeySupplier implements Supplier<PrivateKey> {
final class PrivateKeySupplier implements Supplier<PrivateKey> {
private final Supplier<Credentials> creds;
private final LoadingCache<Credentials, PrivateKey> keyCache;
@ -68,17 +66,14 @@ public final class PrivateKeySupplier implements Supplier<PrivateKey> {
*/
@VisibleForTesting
static final class PrivateKeyForCredentials extends CacheLoader<Credentials, PrivateKey> {
private final String jwsAlg;
@Inject PrivateKeyForCredentials(@Named(JWS_ALG) String jwsAlg) {
this.jwsAlg = jwsAlg;
}
@Override public PrivateKey load(Credentials in) {
try {
String privateKeyInPemFormat = in.credential;
KeyFactory keyFactory = JWSAlgorithms.keyFactory(jwsAlg);
String privateKeyInPemFormat = checkNotNull(in.credential, "credential in PEM format");
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(privateKeySpec(ByteSource.wrap(privateKeyInPemFormat.getBytes(UTF_8))));
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (IOException e) {
throw propagate(e);
} catch (InvalidKeySpecException e) {

View File

@ -16,19 +16,40 @@
*/
package org.jclouds.oauth.v2.domain;
import org.jclouds.json.SerializedNames;
import com.google.auto.value.AutoValue;
/**
* Description of Claims corresponding to a {@linkplain Token JWT Token}.
* Claims corresponding to a {@linkplain Token JWT Token}.
*
* @see <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4">registered list</a>
*/
public final class Claims {
@AutoValue
public abstract class Claims {
/** The issuer of this token. In google, the service account email. */
public abstract String iss();
/** A comma-separated list of scopes needed to perform the request. */
public abstract String scope();
/**
* The oauth audience, who this token is intended for. For instance in JWT and for
* google API's, this maps to: {@code https://accounts.google.com/o/oauth2/token}
*/
public abstract String aud();
/** The expiration time, in seconds since {@link #iat()}. */
public abstract long exp();
/** The time at which the JWT was issued, in seconds since the epoch. */
public static final String ISSUED_AT = "iat";
public abstract long iat();
/** The expiration time, in seconds since {@link #ISSUED_AT}. */
public static final String EXPIRATION_TIME = "exp";
@SerializedNames({ "iss", "scope", "aud", "exp", "iat" })
public static Claims create(String iss, String scope, String aud, long exp, long iat) {
return new AutoValue_Claims(iss, scope, aud, exp, iat);
}
private Claims(){
throw new AssertionError("intentionally unimplemented");
Claims() {
}
}

View File

@ -1,41 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.domain;
import org.jclouds.json.SerializedNames;
import com.google.auto.value.AutoValue;
/**
* The header for the OAuth token, contains the signer algorithm's name and the type of the token
*
* @see <a href="https://developers.google.com/accounts/docs/OAuth2ServiceAccount">doc</a>
*/
@AutoValue
public abstract class Header {
/** The name of the algorithm used to compute the signature, e.g., {@code ES256}. */
public abstract String signerAlgorithm();
/** The type of the token, e.g., {@code JWT}. */
public abstract String type();
@SerializedNames({ "alg", "typ" })
public static Header create(String signerAlgorithm, String type){
return new AutoValue_Header(signerAlgorithm, type);
}
}

View File

@ -1,31 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.domain;
import java.util.Map;
import com.google.auto.value.AutoValue;
@AutoValue
public abstract class TokenRequest {
public abstract Header header();
public abstract Map<String, Object> claimSet();
public static TokenRequest create(Header header, Map<String, Object> claims) {
return new AutoValue_TokenRequest(header, claims);
}
}

View File

@ -27,10 +27,14 @@ import org.jclouds.location.Provider;
import com.google.common.base.Supplier;
public final class BearerTokenAuthenticator implements OAuthAuthenticationFilter {
/**
* When the user supplies {@link org.jclouds.oauth.v2.config.CredentialType#BEARER_TOKEN_CREDENTIALS}, the credential
* is a literal bearer token. This filter applies that to the request.
*/
public final class BearerTokenFromCredentials implements OAuthFilter {
private final Supplier<Credentials> creds;
@Inject BearerTokenAuthenticator(@Provider Supplier<Credentials> creds) {
@Inject BearerTokenFromCredentials(@Provider Supplier<Credentials> creds) {
this.creds = creds;
}

View File

@ -0,0 +1,113 @@
/*
* 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 static java.util.concurrent.TimeUnit.SECONDS;
import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE;
import javax.inject.Inject;
import javax.inject.Named;
import org.jclouds.domain.Credentials;
import org.jclouds.http.HttpException;
import org.jclouds.http.HttpRequest;
import org.jclouds.location.Provider;
import org.jclouds.oauth.v2.AuthorizationApi;
import org.jclouds.oauth.v2.config.OAuthScopes;
import org.jclouds.oauth.v2.domain.Claims;
import org.jclouds.oauth.v2.domain.Token;
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;
/**
* Authorizes new Bearer Tokens at runtime by authorizing claims needed for the http request.
*
* <h3>Cache</h3>
* This maintains a time-based Bearer Token cache. By default expires after 59 minutes
* (the maximum time a token is valid is 60 minutes).
* This cache and expiry period is system-wide and does not attend to per-instance expiry time
* (e.g. "expires_in" from Google Compute -- which is set to the standard 3600 seconds).
*/
public class JWTBearerTokenFlow implements OAuthFilter {
private static final Joiner ON_COMMA = Joiner.on(",");
private final String audience;
private final Supplier<Credentials> credentialsSupplier;
private final OAuthScopes scopes;
private final long tokenDuration;
private final LoadingCache<Claims, Token> tokenCache;
public static class TestJWTBearerTokenFlow extends JWTBearerTokenFlow {
@Inject TestJWTBearerTokenFlow(AuthorizeToken loader, @Named(PROPERTY_SESSION_INTERVAL) long tokenDuration,
@Named(AUDIENCE) String audience, @Provider Supplier<Credentials> credentialsSupplier, OAuthScopes scopes) {
super(loader, tokenDuration, audience, credentialsSupplier, scopes);
}
/** Constant time for testing. */
long currentTimeSeconds() {
return 0;
}
}
@Inject JWTBearerTokenFlow(AuthorizeToken loader, @Named(PROPERTY_SESSION_INTERVAL) long tokenDuration,
@Named(AUDIENCE) String audience, @Provider Supplier<Credentials> credentialsSupplier, OAuthScopes scopes) {
this.audience = audience;
this.credentialsSupplier = credentialsSupplier;
this.scopes = scopes;
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(tokenDuration, SECONDS).build(loader);
}
static final class AuthorizeToken extends CacheLoader<Claims, Token> {
private final AuthorizationApi api;
@Inject AuthorizeToken(AuthorizationApi api) {
this.api = api;
}
@Override public Token load(Claims key) throws Exception {
return api.authorize(key);
}
}
@Override public HttpRequest filter(HttpRequest request) throws HttpException {
long now = currentTimeSeconds();
Claims claims = Claims.create( //
credentialsSupplier.get().identity, // iss
ON_COMMA.join(scopes.forRequest(request)), // scope
audience, // aud
now + tokenDuration, // exp
now // iat
);
Token token = tokenCache.getUnchecked(claims);
String authorization = String.format("%s %s", token.tokenType(), token.accessToken());
return request.toBuilder().addHeader("Authorization", authorization).build();
}
long currentTimeSeconds() {
return System.currentTimeMillis() / 1000;
}
}

View File

@ -1,51 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.filters;
import javax.inject.Inject;
import org.jclouds.http.HttpException;
import org.jclouds.http.HttpRequest;
import org.jclouds.oauth.v2.domain.Token;
import org.jclouds.oauth.v2.domain.TokenRequest;
import com.google.common.base.Function;
import com.google.common.cache.LoadingCache;
/**
* To be used by client applications to embed an OAuth authentication in their REST requests.
* <p/>
* TODO when we're able to use the OAuthAuthentication an this should be used automatically
*/
public final class OAuthAuthenticator implements OAuthAuthenticationFilter {
private Function<HttpRequest, TokenRequest> tokenRequestBuilder;
private Function<TokenRequest, Token> tokenFetcher;
@Inject OAuthAuthenticator(Function<HttpRequest, TokenRequest> tokenRequestBuilder,
LoadingCache<TokenRequest, Token> tokenFetcher) {
this.tokenRequestBuilder = tokenRequestBuilder;
this.tokenFetcher = tokenFetcher;
}
@Override public HttpRequest filter(HttpRequest request) throws HttpException {
TokenRequest tokenRequest = tokenRequestBuilder.apply(request);
Token token = tokenFetcher.apply(tokenRequest);
String authorization = String.format("%s %s", token.tokenType(), token.accessToken());
return request.toBuilder().addHeader("Authorization", authorization).build();
}
}

View File

@ -18,10 +18,6 @@ package org.jclouds.oauth.v2.filters;
import org.jclouds.http.HttpRequestFilter;
/**
* Marker interface to specify auth mechanism (credentials or bearer token)
*
*/
public interface OAuthAuthenticationFilter extends HttpRequestFilter {
/** Indicates use of auth mechanism according to {@link org.jclouds.oauth.v2.config.OAuthProperties#CREDENTIAL_TYPE). */
public interface OAuthFilter extends HttpRequestFilter {
}

View File

@ -1,92 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.functions;
import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
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.domain.Claims.EXPIRATION_TIME;
import static org.jclouds.oauth.v2.domain.Claims.ISSUED_AT;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import org.jclouds.domain.Credentials;
import org.jclouds.http.HttpRequest;
import org.jclouds.location.Provider;
import org.jclouds.oauth.v2.config.OAuthScopes;
import org.jclouds.oauth.v2.domain.Header;
import org.jclouds.oauth.v2.domain.TokenRequest;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Supplier;
/** Builds the default token request with the following claims: {@code iss,scope,aud,iat,exp}. */
public class BuildTokenRequest implements Function<HttpRequest, TokenRequest> {
private static final Joiner ON_COMMA = Joiner.on(",");
private final String assertionTargetDescription;
private final String signatureAlgorithm;
private final Supplier<Credentials> credentialsSupplier;
private final OAuthScopes scopes;
private final long tokenDuration;
public static class TestBuildTokenRequest extends BuildTokenRequest {
@Inject TestBuildTokenRequest(@Named(AUDIENCE) String assertionTargetDescription,
@Named(JWS_ALG) String signatureAlgorithm, @Provider Supplier<Credentials> credentialsSupplier,
OAuthScopes scopes, @Named(PROPERTY_SESSION_INTERVAL) long tokenDuration) {
super(assertionTargetDescription, signatureAlgorithm, credentialsSupplier, scopes, tokenDuration);
}
public long currentTimeSeconds() {
return 0;
}
}
@Inject BuildTokenRequest(@Named(AUDIENCE) String assertionTargetDescription,
@Named(JWS_ALG) String signatureAlgorithm, @Provider Supplier<Credentials> credentialsSupplier,
OAuthScopes scopes, @Named(PROPERTY_SESSION_INTERVAL) long tokenDuration) {
this.assertionTargetDescription = assertionTargetDescription;
this.signatureAlgorithm = signatureAlgorithm;
this.credentialsSupplier = credentialsSupplier;
this.scopes = scopes;
this.tokenDuration = tokenDuration;
}
@Override public TokenRequest apply(HttpRequest request) {
Header header = Header.create(signatureAlgorithm, "JWT");
Map<String, Object> claims = new LinkedHashMap<String, Object>();
claims.put("iss", credentialsSupplier.get().identity);
claims.put("scope", ON_COMMA.join(scopes.forRequest(request)));
claims.put("aud", assertionTargetDescription);
long now = currentTimeSeconds();
claims.put(EXPIRATION_TIME, now + tokenDuration);
claims.put(ISSUED_AT, now);
return TokenRequest.create(header, claims);
}
long currentTimeSeconds() {
return System.currentTimeMillis() / 1000;
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.functions;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Joiner.on;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.io.BaseEncoding.base64Url;
import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import org.jclouds.json.Json;
import org.jclouds.oauth.v2.config.Authorization;
import org.jclouds.rest.AuthorizationException;
import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
public final class ClaimsToAssertion implements Function<Object, String> {
private static final List<String> SUPPORTED_ALGS = ImmutableList.of("RS256", "none");
private final Supplier<PrivateKey> privateKey;
private final Json json;
private final String alg;
@Inject ClaimsToAssertion(@Named(JWS_ALG) String alg, @Authorization Supplier<PrivateKey> privateKey, Json json) {
this.alg = alg;
checkArgument(SUPPORTED_ALGS.contains(alg), "%s %s not in supported list", JWS_ALG, alg, SUPPORTED_ALGS);
this.privateKey = privateKey;
this.json = json;
}
@Override public String apply(Object input) {
String encodedHeader = String.format("{\"alg\":\"%s\",\"typ\":\"JWT\"}", alg);
String encodedClaimSet = json.toJson(input);
encodedHeader = base64Url().omitPadding().encode(encodedHeader.getBytes(UTF_8));
encodedClaimSet = base64Url().omitPadding().encode(encodedClaimSet.getBytes(UTF_8));
byte[] signature = alg.equals("none")
? null
: sha256(privateKey.get(), on(".").join(encodedHeader, encodedClaimSet).getBytes(UTF_8));
String encodedSignature = signature != null ? base64Url().omitPadding().encode(signature) : "";
// the final assertion in base 64 encoded {header}.{claimSet}.{signature} format
return on(".").join(encodedHeader, encodedClaimSet, encodedSignature);
}
static byte[] sha256(PrivateKey privateKey, byte[] input) {
try {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(input);
return signature.sign();
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (SignatureException e) {
throw new AuthorizationException(e);
} catch (InvalidKeyException e) {
throw new AuthorizationException(e);
}
}
}

View File

@ -1,38 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.functions;
import javax.inject.Inject;
import org.jclouds.oauth.v2.OAuthApi;
import org.jclouds.oauth.v2.domain.Token;
import org.jclouds.oauth.v2.domain.TokenRequest;
import com.google.common.base.Function;
public final class FetchToken implements Function<TokenRequest, Token> {
private final OAuthApi oAuthApi;
@Inject FetchToken(OAuthApi oAuthApi) {
this.oAuthApi = oAuthApi;
}
@Override public Token apply(TokenRequest input) {
return this.oAuthApi.authenticate(input);
}
}

View File

@ -1,101 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.functions;
import static com.google.common.base.Throwables.propagate;
import static org.jclouds.oauth.v2.JWSAlgorithms.macOrSignature;
import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import javax.crypto.Mac;
import javax.inject.Inject;
import javax.inject.Named;
import org.jclouds.oauth.v2.config.OAuth;
import org.jclouds.rest.AuthorizationException;
import com.google.common.base.Function;
import com.google.common.base.Supplier;
/**
* Function that signs/produces mac's for OAuth tokens, provided a {@link Signature} or a {@link Mac} algorithm and
* {@link PrivateKey}
*/
public final class SignOrProduceMacForToken implements Supplier<Function<byte[], byte[]>> {
private final String macOrSignature;
private final Supplier<PrivateKey> credentials;
@Inject SignOrProduceMacForToken(@Named(JWS_ALG) String jwsAlg, @OAuth Supplier<PrivateKey> credentials) {
this.macOrSignature = macOrSignature(jwsAlg);
this.credentials = credentials;
}
@Override public Function<byte[], byte[]> get() {
try {
if (macOrSignature.startsWith("SHA")) {
return new SignatureGenerator(macOrSignature, credentials.get());
}
return new MessageAuthenticationCodeGenerator(macOrSignature, credentials.get());
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("Invalid contents in JWSAlgorithms! " + e.getMessage());
} catch (InvalidKeyException e) {
throw new AuthorizationException("cannot parse pk. " + e.getMessage(), e);
}
}
private static class MessageAuthenticationCodeGenerator implements Function<byte[], byte[]> {
private final 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<byte[], byte[]> {
private final 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 propagate(e);
}
}
}
}

View File

@ -1,58 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.handlers;
import static javax.ws.rs.core.Response.Status;
import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream;
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;
public final class OAuthErrorHandler implements HttpErrorHandler {
@Override 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);
}
}

View File

@ -0,0 +1,94 @@
/*
* 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;
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.OAuthScopes.SingleScope;
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.OAuthModule;
import org.jclouds.oauth.v2.config.OAuthScopes;
import org.jclouds.oauth.v2.domain.Claims;
import org.jclouds.oauth.v2.domain.Token;
import org.jclouds.providers.ProviderMetadata;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
import com.google.inject.Binder;
import com.google.inject.Module;
import com.google.inject.name.Names;
@Test(groups = "live", singleThreaded = true)
public class AuthorizationApiLiveTest extends BaseApiLiveTest<AuthorizationApi> {
private final String jwsAlg = "RS256";
private String scope;
private String audience;
public AuthorizationApiLiveTest() {
provider = "oauth";
}
public void authenticateJWTToken() throws Exception {
long now = System.currentTimeMillis() / 1000;
Claims claims = Claims.create(
identity, // iss
scope, // scope
audience, // aud
now + 3600, // exp
now // iat
);
Token token = api.authorize(claims);
assertNotNull(token, "no token when authorizing " + claims);
}
/** 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();
}
@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");
return props;
}
@Override protected Iterable<Module> setupModules() {
return ImmutableList.<Module>builder() //
.add(new OAuthModule()) //
.add(new Module() {
@Override public void configure(Binder binder) {
// ContextBuilder erases oauth.endpoint, as that's the same name as the provider key.
binder.bindConstant().annotatedWith(Names.named("oauth.endpoint")).to(endpoint);
binder.bind(OAuthScopes.class).toInstance(SingleScope.create(scope));
}
}).addAll(super.setupModules()).build();
}
}

View File

@ -14,38 +14,35 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.features;
package org.jclouds.oauth.v2;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.io.BaseEncoding.base64Url;
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.OAuthProperties.AUDIENCE;
import static org.jclouds.oauth.v2.domain.Claims.EXPIRATION_TIME;
import static org.jclouds.oauth.v2.domain.Claims.ISSUED_AT;
import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE;
import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
import static org.jclouds.util.Strings2.toStringAndClose;
import static org.testng.Assert.assertEquals;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
import java.util.Properties;
import org.jclouds.ContextBuilder;
import org.jclouds.concurrent.config.ExecutorServiceModule;
import org.jclouds.oauth.v2.OAuthApi;
import org.jclouds.oauth.v2.OAuthApiMetadata;
import org.jclouds.oauth.v2.OAuthTestUtils;
import org.jclouds.oauth.v2.config.OAuthModule;
import org.jclouds.oauth.v2.config.OAuthScopes;
import org.jclouds.oauth.v2.config.OAuthScopes.SingleScope;
import org.jclouds.oauth.v2.domain.Header;
import org.jclouds.oauth.v2.domain.Claims;
import org.jclouds.oauth.v2.domain.Token;
import org.jclouds.oauth.v2.domain.TokenRequest;
import org.jclouds.rest.AnonymousHttpApiMetadata;
import org.testng.annotations.Test;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.BaseEncoding;
import com.google.inject.Binder;
@ -55,7 +52,7 @@ import com.squareup.okhttp.mockwebserver.MockWebServer;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
@Test(groups = "unit", testName = "OAuthApiMockTest")
public class OAuthApiMockTest {
public class AuthorizationApiMockTest {
private static final String SCOPE = "https://www.googleapis.com/auth/prediction";
private static final String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
@ -66,14 +63,13 @@ public class OAuthApiMockTest {
private static final Token TOKEN = Token.create("1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M", "Bearer", 3600);
private static final Map<String, Object> CLAIMS = ImmutableMap.<String, Object>builder()
.put("iss", "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com")
.put("scope", SCOPE)
.put("aud", "https://accounts.google.com/o/oauth2/token")
.put(EXPIRATION_TIME, 1328573381)
.put(ISSUED_AT, 1328569781).build();
private static final Header HEADER = Header.create("RS256", "JWT");
private static final Claims CLAIMS = Claims.create(
"761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com", // iss
SCOPE, // scope
"https://accounts.google.com/o/oauth2/token", // aud
1328573381, // exp
1328569781 // iat
);
public void testGenerateJWTRequest() throws Exception {
MockWebServer server = new MockWebServer();
@ -84,9 +80,9 @@ public class OAuthApiMockTest {
"}"));
server.play();
OAuthApi api = api(server.getUrl("/"));
AuthorizationApi api = api(server.getUrl("/"));
assertEquals(api.authenticate(TokenRequest.create(HEADER, CLAIMS)), TOKEN);
assertEquals(api.authorize(CLAIMS), TOKEN);
RecordedRequest request = server.takeRequest();
assertEquals(request.getMethod(), "POST");
@ -107,20 +103,23 @@ public class OAuthApiMockTest {
private final BaseEncoding encoding = base64Url().omitPadding();
private OAuthApi api(URL url) throws IOException {
private AuthorizationApi api(URL url) throws IOException {
Properties overrides = new Properties();
overrides.put(AUDIENCE, "https://accounts.google.com/o/oauth2/token");
overrides.put(PROPERTY_MAX_RETRIES, "1");
overrides.setProperty("oauth.endpoint", url.toString());
overrides.setProperty(JWS_ALG, "RS256");
overrides.setProperty(CREDENTIAL_TYPE, P12_PRIVATE_KEY_CREDENTIALS.toString());
overrides.setProperty(AUDIENCE, "https://accounts.google.com/o/oauth2/token");
overrides.setProperty(PROPERTY_MAX_RETRIES, "1");
return ContextBuilder.newBuilder(new OAuthApiMetadata())
return ContextBuilder.newBuilder(AnonymousHttpApiMetadata.forApi(AuthorizationApi.class))
.credentials("foo", toStringAndClose(OAuthTestUtils.class.getResourceAsStream("/testpk.pem")))
.endpoint(url.toString())
.overrides(overrides)
.modules(ImmutableSet.of(new ExecutorServiceModule(sameThreadExecutor()), new Module() {
.modules(ImmutableSet.of(new ExecutorServiceModule(sameThreadExecutor()), new OAuthModule(), new Module() {
@Override public void configure(Binder binder) {
binder.bind(OAuthScopes.class).toInstance(SingleScope.create(SCOPE));
}
}))
.buildApi(OAuthApi.class);
.buildApi(AuthorizationApi.class);
}
}

View File

@ -1,39 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2;
import org.jclouds.View;
import org.jclouds.apis.internal.BaseApiMetadataTest;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.TypeToken;
/**
* Tests that OAuthApiMetadata is properly registered in ServiceLoader
* <p/>
* <pre>
* META-INF/services/org.jclouds.apis.ApiMetadata
* </pre>
*/
@Test(groups = "unit")
public class OAuthApiMetadataTest extends BaseApiMetadataTest {
public OAuthApiMetadataTest() {
super(new OAuthApiMetadata(), ImmutableSet.<TypeToken<? extends View>>of());
}
}

View File

@ -1,85 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.binders;
import static org.jclouds.oauth.v2.domain.Claims.EXPIRATION_TIME;
import static org.jclouds.oauth.v2.domain.Claims.ISSUED_AT;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
import java.io.IOException;
import java.util.Map;
import org.jclouds.http.HttpRequest;
import org.jclouds.json.config.GsonModule;
import org.jclouds.oauth.v2.domain.Header;
import org.jclouds.oauth.v2.domain.TokenRequest;
import org.jclouds.util.Strings2;
import org.testng.annotations.Test;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.inject.Binder;
import com.google.inject.Guice;
import com.google.inject.Module;
import com.google.inject.Provides;
@Test(groups = "unit", testName = "OAuthTokenBinderTest")
public class TokenBinderTest {
public static final String STRING_THAT_GENERATES_URL_UNSAFE_BASE64_ENCODING = "§1234567890'+±!\"#$%&/()" +
"=?*qwertyuiopº´WERTYUIOPªàsdfghjklç~ASDFGHJKLÇ^<zxcvbnm,.->ZXCVBNM;:_@€";
public void testPayloadIsUrlSafe() throws IOException {
Header header = Header.create("a", "b");
Map<String, Object> claims = ImmutableMap.<String, Object>builder()
.put(ISSUED_AT, 0)
.put(EXPIRATION_TIME, 0)
.put("ist", STRING_THAT_GENERATES_URL_UNSAFE_BASE64_ENCODING).build();
TokenRequest tokenRequest = TokenRequest.create(header, claims);
HttpRequest request = tokenBinder.bindToRequest(
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<String> parts = Splitter.on(".").split(payload);
assertSame(Iterables.size(parts), 3);
assertTrue(!payload.contains("+"));
assertTrue(!payload.contains("/"));
}
private final TokenBinder tokenBinder = Guice.createInjector(new GsonModule(), new Module() {
@Override public void configure(Binder binder) {
}
@Provides Supplier<Function<byte[], byte[]>> signer() {
return (Supplier) Suppliers.ofInstance(Functions.constant(null));
}
}).getInstance(TokenBinder.class);
}

View File

@ -14,24 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.functions;
package org.jclouds.oauth.v2.config;
import static com.google.common.base.Suppliers.ofInstance;
import static org.jclouds.oauth.v2.functions.PrivateKeySupplier.PrivateKeyForCredentials;
import static org.testng.Assert.assertNotNull;
import java.io.File;
import java.io.IOException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import java.util.Properties;
import org.jclouds.domain.Credentials;
import org.jclouds.oauth.v2.OAuthTestUtils;
import org.jclouds.oauth.v2.config.PrivateKeySupplier.PrivateKeyForCredentials;
import org.jclouds.rest.AuthorizationException;
import org.testng.annotations.Test;
@ -43,8 +37,7 @@ import com.google.common.io.Files;
public class PrivateKeySupplierTest {
/** Test loading the credentials by extracting a pk from a PKCS12 keystore. */
public void testLoadPKString() throws IOException, NoSuchAlgorithmException, KeyStoreException, CertificateException,
UnrecoverableKeyException, InvalidKeySpecException {
public void testLoadPKString() throws Exception {
assertNotNull(loadPrivateKey());
}
@ -52,14 +45,7 @@ public class PrivateKeySupplierTest {
public void testAuthorizationExceptionIsThrownOnBadKeys() {
PrivateKeySupplier supplier = new PrivateKeySupplier(
Suppliers.ofInstance(new Credentials("MOMMA", "FileNotFoundCredential")),
new PrivateKeyForCredentials("RS256"));
supplier.get();
}
@Test(expectedExceptions = AuthorizationException.class)
public void testGSEChildExceptionsPropagateAsAuthorizationException() {
PrivateKeySupplier supplier = new PrivateKeySupplier(Suppliers.ofInstance(new Credentials("MOMMA", "MIA")),
new PrivateKeyForCredentials("MOMMA"));
new PrivateKeyForCredentials());
supplier.get();
}
@ -68,16 +54,14 @@ public class PrivateKeySupplierTest {
Credentials validCredentials = new Credentials(propertied.getProperty("oauth.identity"),
propertied.getProperty("oauth.credential"));
PrivateKeySupplier supplier = new PrivateKeySupplier(Suppliers.ofInstance(validCredentials),
new PrivateKeyForCredentials("RS256"));
new PrivateKeyForCredentials());
assertNotNull(supplier.get());
}
public static PrivateKey loadPrivateKey()
throws IOException, NoSuchAlgorithmException, CertificateException, InvalidKeySpecException {
public static PrivateKey loadPrivateKey() throws Exception {
PrivateKeySupplier supplier = new PrivateKeySupplier(ofInstance(new Credentials("foo",
Files.asCharSource(new File("src/test/resources/testpk.pem"), Charsets.UTF_8).read())),
new PrivateKeyForCredentials("RS256"));
new PrivateKeyForCredentials());
return supplier.get();
}
}

View File

@ -1,84 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.features;
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.JWS_ALG;
import static org.jclouds.oauth.v2.domain.Claims.EXPIRATION_TIME;
import static org.jclouds.oauth.v2.domain.Claims.ISSUED_AT;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import java.util.Map;
import java.util.Properties;
import org.jclouds.oauth.v2.JWSAlgorithms;
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.testng.annotations.Test;
import com.google.common.collect.ImmutableMap;
/**
* 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.jws-alg
*/
@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 jwsAlg = getMandatoryProperty(properties, JWS_ALG);
checkState(JWSAlgorithms.supportedAlgs().contains(jwsAlg), "Algorithm not supported: %s", jwsAlg);
Header header = Header.create(jwsAlg, "JWT");
String audience = getMandatoryProperty(properties, AUDIENCE);
long now = System.currentTimeMillis() / 1000;
Map<String, Object> claims = ImmutableMap.<String, Object>builder()
.put("iss", identity)
.put("scope", scope)
.put("aud", audience)
.put(EXPIRATION_TIME, now + 3600)
.put(ISSUED_AT, now).build();
TokenRequest tokenRequest = TokenRequest.create(header, claims);
Token token = api.authenticate(tokenRequest);
assertNotNull(token, "no token when authenticating " + tokenRequest);
}
}

View File

@ -15,22 +15,17 @@
* limitations under the License.
*/
package org.jclouds.oauth.v2.functions;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Suppliers.ofInstance;
import static com.google.common.io.BaseEncoding.base64Url;
import static org.testng.Assert.assertNotNull;
import static org.testng.AssertJUnit.assertEquals;
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.Charsets.UTF_8;
import static com.google.common.io.BaseEncoding.base64Url;
import static org.jclouds.oauth.v2.config.PrivateKeySupplierTest.loadPrivateKey;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import org.testng.annotations.Test;
@Test(groups = "unit")
public class SignerFunctionTest {
public class ClaimsToAssertionTest {
private static final String PAYLOAD = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.\n" +
"eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZ" +
@ -44,11 +39,8 @@ public class SignerFunctionTest {
"I9-nj3oUGd1fQty2k4Lsd-Zdkz6es";
public void testSignPayload() throws InvalidKeyException, IOException, NoSuchAlgorithmException,
CertificateException, InvalidKeySpecException {
SignOrProduceMacForToken signer = new SignOrProduceMacForToken("RS256",
ofInstance(PrivateKeySupplierTest.loadPrivateKey()));
byte[] payloadSignature = signer.get().apply(PAYLOAD.getBytes(UTF_8));
public void sha256() throws Exception {
byte[] payloadSignature = ClaimsToAssertion.sha256(loadPrivateKey(), PAYLOAD.getBytes(UTF_8));
assertNotNull(payloadSignature);
assertEquals(base64Url().omitPadding().encode(payloadSignature), SHA256withRSA_PAYLOAD_SIGNATURE_RESULT);

View File

@ -1,92 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.handlers;
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;
import java.net.URI;
import org.easymock.IArgumentMatcher;
import org.jclouds.http.HttpCommand;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpResponse;
import org.testng.annotations.Test;
@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<? extends Exception> 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<? extends Exception> 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<? extends Exception> 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;
}
}

View File

@ -1,40 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.internal;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.io.BaseEncoding.base64Url;
import static org.testng.Assert.assertTrue;
import org.testng.annotations.Test;
/**
* Tests that the Base64 implementations used to Base64 encode the tokens are Url safe.
*/
@Test(groups = "unit")
public class Base64UrlSafeTest {
public static final String STRING_THAT_GENERATES_URL_UNSAFE_BASE64_ENCODING = "§1234567890'+±!\"#$%&/()"
+ "=?*qwertyuiopº´WERTYUIOPªàsdfghjklç~ASDFGHJKLÇ^<zxcvbnm,.->ZXCVBNM;:_@€";
public void testUsedBase64IsUrlSafe() {
String encoded = base64Url().omitPadding().encode(STRING_THAT_GENERATES_URL_UNSAFE_BASE64_ENCODING.getBytes(UTF_8));
assertTrue(!encoded.contains("+"));
assertTrue(!encoded.contains("/"));
assertTrue(!encoded.endsWith("="));
}
}

View File

@ -1,63 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.internal;
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.OAuthScopes.SingleScope;
import java.util.Properties;
import org.jclouds.apis.BaseApiLiveTest;
import org.jclouds.oauth.v2.OAuthApi;
import org.jclouds.oauth.v2.config.OAuthScopes;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
import com.google.inject.Binder;
import com.google.inject.Module;
@Test(groups = "live")
public class BaseOAuthApiLiveTest extends BaseApiLiveTest<OAuthApi> {
protected String scope;
public BaseOAuthApiLiveTest() {
provider = "oauth";
}
@Override protected Properties setupProperties() {
Properties props = super.setupProperties();
setCredential(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");
scope = setIfTestSystemPropertyPresent(props, "jclouds.oauth.scope");
setIfTestSystemPropertyPresent(props, JWS_ALG);
return props;
}
@Override protected Iterable<Module> setupModules() {
return ImmutableList.<Module>builder().add(new Module() {
@Override public void configure(Binder binder) {
binder.bind(OAuthScopes.class).toInstance(SingleScope.create(scope));
}
}).addAll(super.setupModules()).build();
}
}

View File

@ -1,112 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.oauth.v2.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE;
import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
import static org.jclouds.oauth.v2.domain.Claims.EXPIRATION_TIME;
import static org.jclouds.oauth.v2.domain.Claims.ISSUED_AT;
import static org.testng.Assert.assertNotNull;
import java.io.Closeable;
import java.util.Map;
import java.util.Properties;
import org.jclouds.apis.BaseApiLiveTest;
import org.jclouds.config.ValueOfConfigurationKeyOrNull;
import org.jclouds.oauth.v2.JWSAlgorithms;
import org.jclouds.oauth.v2.OAuthApi;
import org.jclouds.oauth.v2.domain.Header;
import org.jclouds.oauth.v2.domain.Token;
import org.jclouds.oauth.v2.domain.TokenRequest;
import org.testng.annotations.Test;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.reflect.TypeToken;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
/**
* A base test of oauth authenticated rest providers. Providers must set the following properties:
* <p/>
* - oauth.endpoint
* - oauth.audience
* - oauth.jws-alg
* <p/>
* - oauth.scopes is provided by the subclass
* <p/>
* This test asserts that a provider can authenticate with oauth for a given scope, or more simply
* that authentication/authorization is working.
*/
@Test(groups = "live")
public abstract class BaseOAuthAuthenticatedApiLiveTest<A extends Closeable> extends BaseApiLiveTest<A> {
protected abstract String getScopes();
private OAuthApi oauthApi;
public void testAuthenticate() {
// obtain the necessary properties from the context
String jwsAlg = checkNotNull(propFunction.apply(JWS_ALG), JWS_ALG);
checkState(JWSAlgorithms.supportedAlgs().contains(jwsAlg), "Algorithm not supported: %s", jwsAlg);
String audience = checkNotNull(propFunction.apply(AUDIENCE), AUDIENCE);
// obtain the scopes from the subclass
String scopes = getScopes();
Header header = Header.create(jwsAlg, "JWT");
long now = SECONDS.convert(System.currentTimeMillis(), MILLISECONDS);
Map<String, Object> claims = ImmutableMap.<String, Object>builder()
.put("iss", identity)
.put("scope", scopes)
.put("aud", audience)
.put(EXPIRATION_TIME, now + 3600)
.put(ISSUED_AT, now).build();
TokenRequest tokenRequest = TokenRequest.create(header, claims);
Token token = oauthApi.authenticate(tokenRequest);
assertNotNull(token, "no token when authenticating " + tokenRequest);
}
@SuppressWarnings({ "unchecked", "serial" })
protected A create(Properties props, Iterable<Module> modules) {
Injector injector = newBuilder().modules(modules).overrides(props).buildInjector();
propFunction = injector.getInstance(ValueOfConfigurationKeyOrNull.class);
try {
oauthApi = injector.getInstance(OAuthApi.class);
} catch (Exception e) {
throw new IllegalStateException("Provider has no OAuthApi bound. Was the OAuthAuthenticationModule added?");
}
return (A) injector.getInstance(Key.get(new TypeToken<A>(getClass()) {
}.getType()));
}
private Function<String, String> propFunction;
}

View File

@ -1,5 +0,0 @@
{
"access_token" : "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M",
"token_type" : "Bearer",
"expires_in" : 3600
}