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.identity>FIX_ME</test.oauth.identity>
|
||||||
<test.oauth.credential>FIX_ME</test.oauth.credential>
|
<test.oauth.credential>FIX_ME</test.oauth.credential>
|
||||||
<test.oauth.endpoint>FIX_ME</test.oauth.endpoint>
|
<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.audience>FIX_ME</test.jclouds.oauth.audience>
|
||||||
<test.jclouds.oauth.scope>FIX_ME</test.jclouds.oauth.scope>
|
<test.jclouds.oauth.scope>FIX_ME</test.jclouds.oauth.scope>
|
||||||
<test.oauth.api-version>2</test.oauth.api-version>
|
<test.oauth.api-version>2</test.oauth.api-version>
|
||||||
|
|
|
@ -14,27 +14,33 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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 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.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.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> {
|
* Binds to an OAuth2 <a href="http://tools.ietf.org/html/rfc6749#section-3.1">authorization endpoint</a>.
|
||||||
|
*/
|
||||||
@Override
|
@Endpoint(Authorization.class)
|
||||||
public String resource() {
|
public interface AuthorizationApi extends Closeable {
|
||||||
return "/tokenResponse.json";
|
@Named("oauth2:authorize")
|
||||||
}
|
@POST
|
||||||
|
@FormParams(keys = "grant_type", values = "urn:ietf:params:oauth:grant-type:jwt-bearer")
|
||||||
@Override
|
|
||||||
@Consumes(APPLICATION_JSON)
|
@Consumes(APPLICATION_JSON)
|
||||||
public Token expected() {
|
Token authorize(@FormParam("assertion") @ParamParser(ClaimsToAssertion.class) Claims claims);
|
||||||
return Token.create("1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M", "Bearer", 3600);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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;
|
import javax.inject.Qualifier;
|
||||||
|
|
||||||
/**
|
|
||||||
* Qualifies OAuth related resources, such as Endpoint.
|
|
||||||
*
|
|
||||||
* @see org.jclouds.oauth.v2.OAuthApi
|
|
||||||
*/
|
|
||||||
@Retention(value = RetentionPolicy.RUNTIME)
|
@Retention(value = RetentionPolicy.RUNTIME)
|
||||||
@Target(value = {ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
|
@Target(value = {ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
|
||||||
@Qualifier
|
@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;
|
package org.jclouds.oauth.v2.config;
|
||||||
|
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static org.jclouds.oauth.v2.config.CredentialType.P12_PRIVATE_KEY_CREDENTIALS;
|
||||||
import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
|
import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE;
|
||||||
import static org.jclouds.oauth.v2.JWSAlgorithms.NONE;
|
import static org.jclouds.rest.config.BinderUtils.bindHttpApi;
|
||||||
import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
|
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
|
|
||||||
import org.jclouds.http.HttpRequest;
|
import javax.inject.Named;
|
||||||
import org.jclouds.oauth.v2.domain.Token;
|
import javax.inject.Singleton;
|
||||||
import org.jclouds.oauth.v2.domain.TokenRequest;
|
|
||||||
import org.jclouds.oauth.v2.filters.BearerTokenAuthenticator;
|
import org.jclouds.oauth.v2.AuthorizationApi;
|
||||||
import org.jclouds.oauth.v2.filters.OAuthAuthenticationFilter;
|
import org.jclouds.oauth.v2.filters.BearerTokenFromCredentials;
|
||||||
import org.jclouds.oauth.v2.filters.OAuthAuthenticator;
|
import org.jclouds.oauth.v2.filters.JWTBearerTokenFlow;
|
||||||
import org.jclouds.oauth.v2.functions.BuildTokenRequest;
|
import org.jclouds.oauth.v2.filters.OAuthFilter;
|
||||||
import org.jclouds.oauth.v2.functions.FetchToken;
|
|
||||||
import org.jclouds.oauth.v2.functions.PrivateKeySupplier;
|
|
||||||
import org.jclouds.oauth.v2.functions.SignOrProduceMacForToken;
|
|
||||||
|
|
||||||
import com.google.common.base.Function;
|
|
||||||
import com.google.common.base.Functions;
|
|
||||||
import com.google.common.base.Supplier;
|
import com.google.common.base.Supplier;
|
||||||
import com.google.common.base.Suppliers;
|
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.AbstractModule;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.Provides;
|
import com.google.inject.Provides;
|
||||||
import com.google.inject.Singleton;
|
|
||||||
import com.google.inject.TypeLiteral;
|
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() {
|
@Override protected void configure() {
|
||||||
|
bindHttpApi(binder(), AuthorizationApi.class);
|
||||||
bind(CredentialType.class).toProvider(CredentialTypeFromPropertyOrDefault.class);
|
bind(CredentialType.class).toProvider(CredentialTypeFromPropertyOrDefault.class);
|
||||||
bind(new TypeLiteral<Function<HttpRequest, TokenRequest>>() {}).to(BuildTokenRequest.class);
|
bind(new TypeLiteral<Supplier<PrivateKey>>() {}).annotatedWith(Authorization.class).to(PrivateKeySupplier.class);
|
||||||
bind(new TypeLiteral<Function<TokenRequest, Token>>() {}).to(FetchToken.class);
|
|
||||||
bind(new TypeLiteral<Supplier<PrivateKey>>() {}).annotatedWith(OAuth.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
|
@Provides
|
||||||
@Singleton
|
@Authorization
|
||||||
public LoadingCache<TokenRequest, Token> provideAccessCache(Function<TokenRequest, Token> getAccess,
|
protected Supplier<URI> oauthEndpoint(@javax.inject.Named("oauth.endpoint") String endpoint) {
|
||||||
@Named(PROPERTY_SESSION_INTERVAL) long expirationSeconds) {
|
return Suppliers.ofInstance(URI.create(endpoint));
|
||||||
// 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public static class CredentialTypeFromPropertyOrDefault implements Provider<CredentialType> {
|
public static class CredentialTypeFromPropertyOrDefault implements Provider<CredentialType> {
|
||||||
@Inject(optional = true)
|
@Inject(optional = true)
|
||||||
@Named(OAuthProperties.CREDENTIAL_TYPE)
|
@Named(CREDENTIAL_TYPE)
|
||||||
String credentialType = CredentialType.P12_PRIVATE_KEY_CREDENTIALS.toString();
|
String credentialType = P12_PRIVATE_KEY_CREDENTIALS.toString();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CredentialType get() {
|
public CredentialType get() {
|
||||||
|
@ -101,9 +67,9 @@ public class OAuthModule extends AbstractModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
protected OAuthAuthenticationFilter authenticationFilterForCredentialType(CredentialType credentialType,
|
protected OAuthFilter authenticationFilterForCredentialType(CredentialType credentialType,
|
||||||
OAuthAuthenticator serviceAccountAuth,
|
JWTBearerTokenFlow serviceAccountAuth,
|
||||||
BearerTokenAuthenticator bearerTokenAuth) {
|
BearerTokenFromCredentials bearerTokenAuth) {
|
||||||
switch (credentialType) {
|
switch (credentialType) {
|
||||||
case P12_PRIVATE_KEY_CREDENTIALS:
|
case P12_PRIVATE_KEY_CREDENTIALS:
|
||||||
return serviceAccountAuth;
|
return serviceAccountAuth;
|
||||||
|
|
|
@ -16,18 +16,16 @@
|
||||||
*/
|
*/
|
||||||
package org.jclouds.oauth.v2.config;
|
package org.jclouds.oauth.v2.config;
|
||||||
|
|
||||||
import org.jclouds.oauth.v2.JWSAlgorithms;
|
public final class OAuthProperties {
|
||||||
|
|
||||||
public class OAuthProperties {
|
/** The JSON Web Signature alg, must be {@code RS256} or {@code none}. */
|
||||||
|
|
||||||
/** The JSON Web Signature alg, from the {@link JWSAlgorithms#supportedAlgs() supported list}. */
|
|
||||||
public static final String JWS_ALG = "jclouds.oauth.jws-alg";
|
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
|
* 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"}
|
* 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";
|
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";
|
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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.Charsets.UTF_8;
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
import static com.google.common.base.Throwables.propagate;
|
import static com.google.common.base.Throwables.propagate;
|
||||||
import static org.jclouds.crypto.Pems.privateKeySpec;
|
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 static org.jclouds.util.Throwables2.getFirstThrowableOfType;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.spec.InvalidKeySpecException;
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import org.jclouds.domain.Credentials;
|
import org.jclouds.domain.Credentials;
|
||||||
import org.jclouds.location.Provider;
|
import org.jclouds.location.Provider;
|
||||||
import org.jclouds.oauth.v2.JWSAlgorithms;
|
|
||||||
import org.jclouds.rest.AuthorizationException;
|
import org.jclouds.rest.AuthorizationException;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
@ -46,11 +44,11 @@ import com.google.common.io.ByteSource;
|
||||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads {@link PrivateKey} from a pem private key using the KeyFactory obtained vi {@link
|
* Loads {@link PrivateKey} from a pem private key using and RSA KeyFactory. The pem pk algorithm must match the
|
||||||
* JWSAlgorithms#keyFactory(String)}. The pem pk algorithm must match the KeyFactory algorithm.
|
* KeyFactory algorithm.
|
||||||
*/
|
*/
|
||||||
@Singleton // due to cache
|
@Singleton // due to cache
|
||||||
public final class PrivateKeySupplier implements Supplier<PrivateKey> {
|
final class PrivateKeySupplier implements Supplier<PrivateKey> {
|
||||||
|
|
||||||
private final Supplier<Credentials> creds;
|
private final Supplier<Credentials> creds;
|
||||||
private final LoadingCache<Credentials, PrivateKey> keyCache;
|
private final LoadingCache<Credentials, PrivateKey> keyCache;
|
||||||
|
@ -68,17 +66,14 @@ public final class PrivateKeySupplier implements Supplier<PrivateKey> {
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final class PrivateKeyForCredentials extends CacheLoader<Credentials, PrivateKey> {
|
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) {
|
@Override public PrivateKey load(Credentials in) {
|
||||||
try {
|
try {
|
||||||
String privateKeyInPemFormat = in.credential;
|
String privateKeyInPemFormat = checkNotNull(in.credential, "credential in PEM format");
|
||||||
KeyFactory keyFactory = JWSAlgorithms.keyFactory(jwsAlg);
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||||
return keyFactory.generatePrivate(privateKeySpec(ByteSource.wrap(privateKeyInPemFormat.getBytes(UTF_8))));
|
return keyFactory.generatePrivate(privateKeySpec(ByteSource.wrap(privateKeyInPemFormat.getBytes(UTF_8))));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw propagate(e);
|
throw propagate(e);
|
||||||
} catch (InvalidKeySpecException e) {
|
} catch (InvalidKeySpecException e) {
|
|
@ -16,19 +16,40 @@
|
||||||
*/
|
*/
|
||||||
package org.jclouds.oauth.v2.domain;
|
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>
|
* @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. */
|
/** 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}. */
|
@SerializedNames({ "iss", "scope", "aud", "exp", "iat" })
|
||||||
public static final String EXPIRATION_TIME = "exp";
|
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(){
|
Claims() {
|
||||||
throw new AssertionError("intentionally unimplemented");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
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;
|
private final Supplier<Credentials> creds;
|
||||||
|
|
||||||
@Inject BearerTokenAuthenticator(@Provider Supplier<Credentials> creds) {
|
@Inject BearerTokenFromCredentials(@Provider Supplier<Credentials> creds) {
|
||||||
this.creds = 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;
|
import org.jclouds.http.HttpRequestFilter;
|
||||||
|
|
||||||
/**
|
/** Indicates use of auth mechanism according to {@link org.jclouds.oauth.v2.config.OAuthProperties#CREDENTIAL_TYPE). */
|
||||||
* Marker interface to specify auth mechanism (credentials or bearer token)
|
public interface OAuthFilter extends HttpRequestFilter {
|
||||||
*
|
|
||||||
*/
|
|
||||||
public interface OAuthAuthenticationFilter 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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.base.Charsets.UTF_8;
|
||||||
import static com.google.common.io.BaseEncoding.base64Url;
|
import static com.google.common.io.BaseEncoding.base64Url;
|
||||||
import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor;
|
import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor;
|
||||||
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
|
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
|
||||||
import static org.jclouds.Constants.PROPERTY_MAX_RETRIES;
|
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.config.OAuthProperties.AUDIENCE;
|
||||||
import static org.jclouds.oauth.v2.domain.Claims.EXPIRATION_TIME;
|
import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE;
|
||||||
import static org.jclouds.oauth.v2.domain.Claims.ISSUED_AT;
|
import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
|
||||||
import static org.jclouds.util.Strings2.toStringAndClose;
|
import static org.jclouds.util.Strings2.toStringAndClose;
|
||||||
import static org.testng.Assert.assertEquals;
|
import static org.testng.Assert.assertEquals;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
import org.jclouds.ContextBuilder;
|
import org.jclouds.ContextBuilder;
|
||||||
import org.jclouds.concurrent.config.ExecutorServiceModule;
|
import org.jclouds.concurrent.config.ExecutorServiceModule;
|
||||||
import org.jclouds.oauth.v2.OAuthApi;
|
import org.jclouds.oauth.v2.config.OAuthModule;
|
||||||
import org.jclouds.oauth.v2.OAuthApiMetadata;
|
|
||||||
import org.jclouds.oauth.v2.OAuthTestUtils;
|
|
||||||
import org.jclouds.oauth.v2.config.OAuthScopes;
|
import org.jclouds.oauth.v2.config.OAuthScopes;
|
||||||
import org.jclouds.oauth.v2.config.OAuthScopes.SingleScope;
|
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.Token;
|
||||||
import org.jclouds.oauth.v2.domain.TokenRequest;
|
import org.jclouds.rest.AnonymousHttpApiMetadata;
|
||||||
import org.testng.annotations.Test;
|
import org.testng.annotations.Test;
|
||||||
|
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.collect.ImmutableMap;
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
import com.google.inject.Binder;
|
import com.google.inject.Binder;
|
||||||
|
@ -55,7 +52,7 @@ import com.squareup.okhttp.mockwebserver.MockWebServer;
|
||||||
import com.squareup.okhttp.mockwebserver.RecordedRequest;
|
import com.squareup.okhttp.mockwebserver.RecordedRequest;
|
||||||
|
|
||||||
@Test(groups = "unit", testName = "OAuthApiMockTest")
|
@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 SCOPE = "https://www.googleapis.com/auth/prediction";
|
||||||
|
|
||||||
private static final String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
|
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 Token TOKEN = Token.create("1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M", "Bearer", 3600);
|
||||||
|
|
||||||
private static final Map<String, Object> CLAIMS = ImmutableMap.<String, Object>builder()
|
private static final Claims CLAIMS = Claims.create(
|
||||||
.put("iss", "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com")
|
"761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com", // iss
|
||||||
.put("scope", SCOPE)
|
SCOPE, // scope
|
||||||
.put("aud", "https://accounts.google.com/o/oauth2/token")
|
"https://accounts.google.com/o/oauth2/token", // aud
|
||||||
.put(EXPIRATION_TIME, 1328573381)
|
1328573381, // exp
|
||||||
.put(ISSUED_AT, 1328569781).build();
|
1328569781 // iat
|
||||||
|
);
|
||||||
private static final Header HEADER = Header.create("RS256", "JWT");
|
|
||||||
|
|
||||||
public void testGenerateJWTRequest() throws Exception {
|
public void testGenerateJWTRequest() throws Exception {
|
||||||
MockWebServer server = new MockWebServer();
|
MockWebServer server = new MockWebServer();
|
||||||
|
@ -84,9 +80,9 @@ public class OAuthApiMockTest {
|
||||||
"}"));
|
"}"));
|
||||||
server.play();
|
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();
|
RecordedRequest request = server.takeRequest();
|
||||||
assertEquals(request.getMethod(), "POST");
|
assertEquals(request.getMethod(), "POST");
|
||||||
|
@ -107,20 +103,23 @@ public class OAuthApiMockTest {
|
||||||
|
|
||||||
private final BaseEncoding encoding = base64Url().omitPadding();
|
private final BaseEncoding encoding = base64Url().omitPadding();
|
||||||
|
|
||||||
private OAuthApi api(URL url) throws IOException {
|
private AuthorizationApi api(URL url) throws IOException {
|
||||||
Properties overrides = new Properties();
|
Properties overrides = new Properties();
|
||||||
overrides.put(AUDIENCE, "https://accounts.google.com/o/oauth2/token");
|
overrides.setProperty("oauth.endpoint", url.toString());
|
||||||
overrides.put(PROPERTY_MAX_RETRIES, "1");
|
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")))
|
.credentials("foo", toStringAndClose(OAuthTestUtils.class.getResourceAsStream("/testpk.pem")))
|
||||||
.endpoint(url.toString())
|
.endpoint(url.toString())
|
||||||
.overrides(overrides)
|
.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) {
|
@Override public void configure(Binder binder) {
|
||||||
binder.bind(OAuthScopes.class).toInstance(SingleScope.create(SCOPE));
|
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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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 com.google.common.base.Suppliers.ofInstance;
|
||||||
import static org.jclouds.oauth.v2.functions.PrivateKeySupplier.PrivateKeyForCredentials;
|
|
||||||
import static org.testng.Assert.assertNotNull;
|
import static org.testng.Assert.assertNotNull;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.UnrecoverableKeyException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
import org.jclouds.domain.Credentials;
|
import org.jclouds.domain.Credentials;
|
||||||
import org.jclouds.oauth.v2.OAuthTestUtils;
|
import org.jclouds.oauth.v2.OAuthTestUtils;
|
||||||
|
import org.jclouds.oauth.v2.config.PrivateKeySupplier.PrivateKeyForCredentials;
|
||||||
import org.jclouds.rest.AuthorizationException;
|
import org.jclouds.rest.AuthorizationException;
|
||||||
import org.testng.annotations.Test;
|
import org.testng.annotations.Test;
|
||||||
|
|
||||||
|
@ -43,8 +37,7 @@ import com.google.common.io.Files;
|
||||||
public class PrivateKeySupplierTest {
|
public class PrivateKeySupplierTest {
|
||||||
|
|
||||||
/** Test loading the credentials by extracting a pk from a PKCS12 keystore. */
|
/** Test loading the credentials by extracting a pk from a PKCS12 keystore. */
|
||||||
public void testLoadPKString() throws IOException, NoSuchAlgorithmException, KeyStoreException, CertificateException,
|
public void testLoadPKString() throws Exception {
|
||||||
UnrecoverableKeyException, InvalidKeySpecException {
|
|
||||||
assertNotNull(loadPrivateKey());
|
assertNotNull(loadPrivateKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,14 +45,7 @@ public class PrivateKeySupplierTest {
|
||||||
public void testAuthorizationExceptionIsThrownOnBadKeys() {
|
public void testAuthorizationExceptionIsThrownOnBadKeys() {
|
||||||
PrivateKeySupplier supplier = new PrivateKeySupplier(
|
PrivateKeySupplier supplier = new PrivateKeySupplier(
|
||||||
Suppliers.ofInstance(new Credentials("MOMMA", "FileNotFoundCredential")),
|
Suppliers.ofInstance(new Credentials("MOMMA", "FileNotFoundCredential")),
|
||||||
new PrivateKeyForCredentials("RS256"));
|
new PrivateKeyForCredentials());
|
||||||
supplier.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expectedExceptions = AuthorizationException.class)
|
|
||||||
public void testGSEChildExceptionsPropagateAsAuthorizationException() {
|
|
||||||
PrivateKeySupplier supplier = new PrivateKeySupplier(Suppliers.ofInstance(new Credentials("MOMMA", "MIA")),
|
|
||||||
new PrivateKeyForCredentials("MOMMA"));
|
|
||||||
supplier.get();
|
supplier.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,16 +54,14 @@ public class PrivateKeySupplierTest {
|
||||||
Credentials validCredentials = new Credentials(propertied.getProperty("oauth.identity"),
|
Credentials validCredentials = new Credentials(propertied.getProperty("oauth.identity"),
|
||||||
propertied.getProperty("oauth.credential"));
|
propertied.getProperty("oauth.credential"));
|
||||||
PrivateKeySupplier supplier = new PrivateKeySupplier(Suppliers.ofInstance(validCredentials),
|
PrivateKeySupplier supplier = new PrivateKeySupplier(Suppliers.ofInstance(validCredentials),
|
||||||
new PrivateKeyForCredentials("RS256"));
|
new PrivateKeyForCredentials());
|
||||||
assertNotNull(supplier.get());
|
assertNotNull(supplier.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PrivateKey loadPrivateKey()
|
public static PrivateKey loadPrivateKey() throws Exception {
|
||||||
throws IOException, NoSuchAlgorithmException, CertificateException, InvalidKeySpecException {
|
|
||||||
PrivateKeySupplier supplier = new PrivateKeySupplier(ofInstance(new Credentials("foo",
|
PrivateKeySupplier supplier = new PrivateKeySupplier(ofInstance(new Credentials("foo",
|
||||||
Files.asCharSource(new File("src/test/resources/testpk.pem"), Charsets.UTF_8).read())),
|
Files.asCharSource(new File("src/test/resources/testpk.pem"), Charsets.UTF_8).read())),
|
||||||
new PrivateKeyForCredentials("RS256"));
|
new PrivateKeyForCredentials());
|
||||||
return supplier.get();
|
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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.jclouds.oauth.v2.functions;
|
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 static com.google.common.base.Charsets.UTF_8;
|
||||||
import java.security.InvalidKeyException;
|
import static com.google.common.io.BaseEncoding.base64Url;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import static org.jclouds.oauth.v2.config.PrivateKeySupplierTest.loadPrivateKey;
|
||||||
import java.security.cert.CertificateException;
|
import static org.testng.Assert.assertEquals;
|
||||||
import java.security.spec.InvalidKeySpecException;
|
import static org.testng.Assert.assertNotNull;
|
||||||
|
|
||||||
import org.testng.annotations.Test;
|
import org.testng.annotations.Test;
|
||||||
|
|
||||||
@Test(groups = "unit")
|
@Test(groups = "unit")
|
||||||
public class SignerFunctionTest {
|
public class ClaimsToAssertionTest {
|
||||||
|
|
||||||
private static final String PAYLOAD = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.\n" +
|
private static final String PAYLOAD = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.\n" +
|
||||||
"eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZ" +
|
"eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZ" +
|
||||||
|
@ -44,11 +39,8 @@ public class SignerFunctionTest {
|
||||||
"I9-nj3oUGd1fQty2k4Lsd-Zdkz6es";
|
"I9-nj3oUGd1fQty2k4Lsd-Zdkz6es";
|
||||||
|
|
||||||
|
|
||||||
public void testSignPayload() throws InvalidKeyException, IOException, NoSuchAlgorithmException,
|
public void sha256() throws Exception {
|
||||||
CertificateException, InvalidKeySpecException {
|
byte[] payloadSignature = ClaimsToAssertion.sha256(loadPrivateKey(), PAYLOAD.getBytes(UTF_8));
|
||||||
SignOrProduceMacForToken signer = new SignOrProduceMacForToken("RS256",
|
|
||||||
ofInstance(PrivateKeySupplierTest.loadPrivateKey()));
|
|
||||||
byte[] payloadSignature = signer.get().apply(PAYLOAD.getBytes(UTF_8));
|
|
||||||
assertNotNull(payloadSignature);
|
assertNotNull(payloadSignature);
|
||||||
|
|
||||||
assertEquals(base64Url().omitPadding().encode(payloadSignature), SHA256withRSA_PAYLOAD_SIGNATURE_RESULT);
|
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