mirror of https://github.com/apache/jclouds.git
* 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:
parent
a35d73c6d0
commit
46a7351a8a
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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("="));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"access_token" : "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M",
|
||||
"token_type" : "Bearer",
|
||||
"expires_in" : 3600
|
||||
}
|
Loading…
Reference in New Issue