client credentials JWT support
Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/f46b38dd Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/f46b38dd Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/f46b38dd Branch: refs/heads/master Commit: f46b38dd89626bc3d32d8260dd816d44255a8420 Parents: ccd1ef2 Author: Jim Spring <[email protected]> Authored: Sat May 14 19:35:39 2016 -0700 Committer: Ignasi Barrera <[email protected]> Committed: Fri May 20 00:10:37 2016 +0200 ---------------------------------------------------------------------- apis/oauth/README | 13 +- .../org/jclouds/oauth/v2/AuthorizationApi.java | 14 +++ .../config/CertificateFingerprintSupplier.java | 113 +++++++++++++++++ .../jclouds/oauth/v2/config/CredentialType.java | 5 +- .../jclouds/oauth/v2/config/OAuthModule.java | 13 +- .../oauth/v2/config/OAuthProperties.java | 9 +- .../oauth/v2/domain/CertificateFingerprint.java | 43 +++++++ .../v2/domain/ClientCredentialsAuthArgs.java | 48 ++++++++ .../v2/domain/ClientCredentialsClaims.java | 61 ++++++++++ .../ClientCredentialsJWTBearerTokenFlow.java | 120 +++++++++++++++++++ .../ClientCredentialsClaimsToAssertion.java | 93 ++++++++++++++ .../oauth/v2/AuthorizationApiLiveTest.java | 34 +++++- .../oauth/v2/AuthorizationApiMockTest.java | 85 +++++++++++-- apis/oauth/src/test/resources/testcert.pem | 22 ++++ 14 files changed, 652 insertions(+), 21 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/README ---------------------------------------------------------------------- diff --git a/apis/oauth/README b/apis/oauth/README index 7d039b1..73e6e5b 100644 --- a/apis/oauth/README +++ b/apis/oauth/README @@ -23,7 +23,7 @@ mvn clean install -Plive \ -Dtest.jclouds.oauth.scope=https://www.googleapis.com/auth/prediction \ -To Run the live test against Azure Active Directory which uses the client_credentials grant type: +To Run the live test against Azure Active Directory which uses the client_credentials grant type when using a password: mvn clean install -Plive \ -Dtest.oauth.identity=<azure app id> \ @@ -32,3 +32,14 @@ mvn clean install -Plive \ -Dtest.jclouds.oauth.resource=https://management.azure.com/ \ -Dtest.jclouds.oauth.credential-type=clientCredentialsSecret +To run the live test against Azure Active directory using the client_credentials grant type with a certificate and private key: + +mvn clean install -Plive \ +-Dtest.jclouds.oauth.credential-type=clientCredentialsP12 \ +-Dtest.jclouds.oauth.resource=https://management.azure.com/ \ +-Dtest.oauth.endpoint=https://login.microsoftonline.com/<tenant id>/oauth2/token \ +-Dtest.jclouds.oauth.audience=https://login.microsoftonline.com/<tenant id>/oauth2/token +-Dtest.oauth.identity=<azure app id> \ +-Dtest.oauth.credential=<path to unencrypted private key PEM file> \ +-Dtest.jclouds.oauth.certificate=<path to certificate PEM file> + http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java ---------------------------------------------------------------------- diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java index b77a8de..50c6668 100644 --- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java @@ -29,8 +29,10 @@ import org.jclouds.javax.annotation.Nullable; import org.jclouds.oauth.v2.OAuthFallbacks.AuthorizationExceptionOn4xx; import org.jclouds.oauth.v2.config.Authorization; import org.jclouds.oauth.v2.domain.Claims; +import org.jclouds.oauth.v2.domain.ClientCredentialsClaims; import org.jclouds.oauth.v2.domain.Token; import org.jclouds.oauth.v2.functions.ClaimsToAssertion; +import org.jclouds.oauth.v2.functions.ClientCredentialsClaimsToAssertion; import org.jclouds.rest.annotations.Endpoint; import org.jclouds.rest.annotations.Fallback; import org.jclouds.rest.annotations.FormParams; @@ -59,4 +61,16 @@ public interface AuthorizationApi extends Closeable { @FormParam("resource") String resource, @FormParam("scope") @Nullable String scope ); + + @Named("oauth2:authorize_client_p12") + @POST + @FormParams(keys = {"grant_type", "client_assertion_type"}, values = {"client_credentials", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}) + @Consumes(APPLICATION_JSON) + @Fallback(AuthorizationExceptionOn4xx.class) + Token authorize( + @FormParam("client_id") String client_id, + @FormParam("client_assertion") @ParamParser(ClientCredentialsClaimsToAssertion.class) ClientCredentialsClaims claim, + @FormParam("resource") String resource, + @FormParam("scope") @Nullable String scope + ); } http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CertificateFingerprintSupplier.java ---------------------------------------------------------------------- diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CertificateFingerprintSupplier.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CertificateFingerprintSupplier.java new file mode 100644 index 0000000..b59064c --- /dev/null +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CertificateFingerprintSupplier.java @@ -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.config; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Throwables.propagate; +import static org.jclouds.crypto.Pems.x509Certificate; +import static org.jclouds.oauth.v2.config.OAuthProperties.CERTIFICATE; +import static org.jclouds.util.Throwables2.getFirstThrowableOfType; + +import java.io.IOException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import javax.inject.Named; + +import org.jclouds.domain.Credentials; +import org.jclouds.location.Provider; +import org.jclouds.oauth.v2.domain.CertificateFingerprint; +import org.jclouds.rest.AuthorizationException; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Supplier; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.common.hash.Hashing; +import static com.google.common.io.BaseEncoding.base64; +import com.google.common.hash.HashCode; + +/** + * Loads the fingerprint of a certificate associated with the {@link PrivateKey} from a pem X509Certificate. + */ +@Singleton // due to cache +final class CertificateFingerprintSupplier implements Supplier<CertificateFingerprint> { + + private final Supplier<Credentials> creds; + private final LoadingCache<Credentials, CertificateFingerprint> certCache; + + @Inject CertificateFingerprintSupplier(@Provider Supplier<Credentials> creds, CertificateFingerprintForCredentials loader) { + this.creds = creds; + // throw out the certificate fingerprint related to old credentials + this.certCache = CacheBuilder.newBuilder().maximumSize(2).build(checkNotNull(loader, "loader")); + } + + /** + * it is relatively expensive to extract a certificate from a PEM and calculate it's fingerprint. + * cache the relationship between current credentials so that the fingerprint is only recalculated once. + */ + @VisibleForTesting + static final class CertificateFingerprintForCredentials extends CacheLoader<Credentials, CertificateFingerprint> { + @Inject(optional = true) @Named(CERTIFICATE) String certInPemFormat; + + @Override public CertificateFingerprint load(Credentials in) { + try { + /** + * CERTIFICATE made optional on injection so that it's not required when other OAuth methods + * are used. + */ + if (certInPemFormat == null) { + throw new IllegalArgumentException("certificate not specified."); + } + X509Certificate cert = null; + cert = x509Certificate(certInPemFormat); + + /** Get the fingerprint in Base64 format */ + byte[] encodedCert = cert.getEncoded(); + HashCode hash = Hashing.sha1().hashBytes(encodedCert); + String fingerprint = base64().encode(hash.asBytes()); + + return CertificateFingerprint.create(fingerprint, cert); + } catch (CertificateException e) { + throw new AssertionError(e); + } catch (IOException e) { + throw propagate(e); + } catch (IllegalArgumentException e) { + throw new AuthorizationException("cannot parse cert. " + e.getMessage(), e); + } + } + } + + @Override public CertificateFingerprint get() { + try { + // loader always throws UncheckedExecutionException so no point in using get() + return certCache.getUnchecked(checkNotNull(creds.get(), "credential supplier returned null")); + } catch (UncheckedExecutionException e) { + AuthorizationException authorizationException = getFirstThrowableOfType(e, AuthorizationException.class); + if (authorizationException != null) { + throw authorizationException; + } + throw e; + } + } +} + http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java ---------------------------------------------------------------------- diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java index b80f4c4..1719cb4 100644 --- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java @@ -29,7 +29,10 @@ public enum CredentialType { P12_PRIVATE_KEY_CREDENTIALS, /** Contents are an ID and Secret */ - CLIENT_CREDENTIALS_SECRET; + CLIENT_CREDENTIALS_SECRET, + + /** Contents are an ID and PEM-encoded Private Key. The certificate is specified as it's own property. */ + CLIENT_CREDENTIALS_P12_AND_CERTIFICATE; @Override public String toString() { return UPPER_UNDERSCORE.to(LOWER_CAMEL, name()); http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java ---------------------------------------------------------------------- diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java index 97d58b7..ffc03cd 100644 --- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java @@ -16,10 +16,11 @@ */ package org.jclouds.oauth.v2.config; +import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE; import static org.jclouds.oauth.v2.config.CredentialType.BEARER_TOKEN_CREDENTIALS; -import static org.jclouds.oauth.v2.config.CredentialType.CLIENT_CREDENTIALS_SECRET; 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.CredentialType.CLIENT_CREDENTIALS_SECRET; +import static org.jclouds.oauth.v2.config.CredentialType.CLIENT_CREDENTIALS_P12_AND_CERTIFICATE; import static org.jclouds.rest.config.BinderUtils.bindHttpApi; import java.net.URI; @@ -30,9 +31,11 @@ import javax.inject.Named; import javax.inject.Singleton; import org.jclouds.oauth.v2.AuthorizationApi; +import org.jclouds.oauth.v2.domain.CertificateFingerprint; +import org.jclouds.oauth.v2.filters.JWTBearerTokenFlow; import org.jclouds.oauth.v2.filters.BearerTokenFromCredentials; +import org.jclouds.oauth.v2.filters.ClientCredentialsJWTBearerTokenFlow; import org.jclouds.oauth.v2.filters.ClientCredentialsSecretFlow; -import org.jclouds.oauth.v2.filters.JWTBearerTokenFlow; import org.jclouds.oauth.v2.filters.OAuthFilter; import com.google.common.base.Supplier; @@ -51,6 +54,7 @@ public final class OAuthModule extends AbstractModule { bindHttpApi(binder(), AuthorizationApi.class); bind(CredentialType.class).toProvider(CredentialTypeFromPropertyOrDefault.class); bind(new TypeLiteral<Supplier<PrivateKey>>() {}).annotatedWith(Authorization.class).to(PrivateKeySupplier.class); + bind(new TypeLiteral<Supplier<CertificateFingerprint>>() {}).annotatedWith(Authorization.class).to(CertificateFingerprintSupplier.class); } @Provides @@ -76,7 +80,8 @@ public final class OAuthModule extends AbstractModule { protected Map<CredentialType, Class<? extends OAuthFilter>> authenticationFlowMap() { return ImmutableMap.of(P12_PRIVATE_KEY_CREDENTIALS, JWTBearerTokenFlow.class, BEARER_TOKEN_CREDENTIALS, BearerTokenFromCredentials.class, - CLIENT_CREDENTIALS_SECRET, ClientCredentialsSecretFlow.class); + CLIENT_CREDENTIALS_SECRET, ClientCredentialsSecretFlow.class, + CLIENT_CREDENTIALS_P12_AND_CERTIFICATE, ClientCredentialsJWTBearerTokenFlow.class); } @Provides http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java ---------------------------------------------------------------------- diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java index 87b5ca9..29498f1 100644 --- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java @@ -37,13 +37,20 @@ public final class OAuthProperties { public static final String CREDENTIAL_TYPE = "jclouds.oauth.credential-type"; /** - * When using oauth with Azure Active Direction and Client Credentials, a "resource" must + * When using oauth with Azure Active Directory and Client Credentials, a "resource" must * be specified as part of the request. * * @see <a href="https://msdn.microsoft.com/en-us/library/azure/dn645543.aspx">doc</a> */ public static final String RESOURCE = "jclouds.oauth.resource"; + /** + * When using oauth with Azure Active Directory, Client Credentials, and using JWT + * authentication, the certificate associated with the Private Key must be provided. + * The fingerprint of the certificate is included in the JWT headers. + */ + public static final String CERTIFICATE = "jclouds.oauth.certificate"; + private OAuthProperties() { } } http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/CertificateFingerprint.java ---------------------------------------------------------------------- diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/CertificateFingerprint.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/CertificateFingerprint.java new file mode 100644 index 0000000..a2f797b --- /dev/null +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/CertificateFingerprint.java @@ -0,0 +1,43 @@ +/* + * 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; + +import java.security.cert.X509Certificate; + +/** + * Details corresponding the a client_credential Azure AD Oauth request + */ +@AutoValue +public abstract class CertificateFingerprint { + /** The fingerprint of the certificate **/ + public abstract String fingerprint(); + + /** The certificate */ + public abstract X509Certificate certificate(); + + @SerializedNames({ "fingerprint", "certificate" }) + public static CertificateFingerprint create(String fingerprint, X509Certificate certificate) { + return new AutoValue_CertificateFingerprint(fingerprint, certificate); + } + + CertificateFingerprint() { + } +} http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsAuthArgs.java ---------------------------------------------------------------------- diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsAuthArgs.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsAuthArgs.java new file mode 100644 index 0000000..1dcad42 --- /dev/null +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsAuthArgs.java @@ -0,0 +1,48 @@ +/* + * 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.javax.annotation.Nullable; +import org.jclouds.json.SerializedNames; + +import com.google.auto.value.AutoValue; + +/** + * Details corresponding the a client_credential Azure AD Oauth request + */ +@AutoValue +public abstract class ClientCredentialsAuthArgs { + /** The ID of the client. **/ + public abstract String clientId(); + + /** The claims for the JWT. */ + public abstract ClientCredentialsClaims claims(); + + /** The resource to authorize against. **/ + public abstract String resource(); + + /** The scope(s) to authorize against. **/ + @Nullable public abstract String scope(); + + @SerializedNames({ "client_id", "claims", "resource", "scope" }) + public static ClientCredentialsAuthArgs create(String clientId, ClientCredentialsClaims claims, String resource, String scope) { + return new AutoValue_ClientCredentialsAuthArgs(clientId, claims, resource, scope); + } + + ClientCredentialsAuthArgs() { + } +} http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsClaims.java ---------------------------------------------------------------------- diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsClaims.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsClaims.java new file mode 100644 index 0000000..3c37bef --- /dev/null +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsClaims.java @@ -0,0 +1,61 @@ +/* + * 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; + +/** + * Claims corresponding to a {@linkplain Token JWT Token} for use when making a client_credentials grant request. + * + * @see <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4">registered list</a> + */ +@AutoValue +public abstract class ClientCredentialsClaims { + /** + * The issuer of this token. In Azure, it is either the email address for the Active Directory account + * or the ID of the application set up as a Service Principal. + */ + public abstract String iss(); + + /** The subject of the JWT. For Azure, "sub" is typically equal to "iss". */ + public abstract String sub(); + + /** + * The oauth audience, who this token is intended for. For instance in JWT and for Azure + * Resource Manager APIs, this maps to https://login.microsoftonline.com/TENANT_ID/oauth2/token. + */ + public abstract String aud(); + + /** The expiration time, in seconds since the epoch after which the JWT must not be accepted for processing. */ + public abstract long exp(); + + /** The time before which the JWT must not be accepted for processing, in seconds since the epoch. */ + public abstract long nbf(); + + /** "JWT ID", a unique identifier for the JWT. */ + public abstract String jti(); + + @SerializedNames({ "iss", "sub", "aud", "exp", "nbf", "jti" }) + public static ClientCredentialsClaims create(String iss, String sub, String aud, long exp, long nbf, String jti) { + return new AutoValue_ClientCredentialsClaims(iss, sub, aud, exp, nbf, jti); + } + + ClientCredentialsClaims() { + } +} http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/ClientCredentialsJWTBearerTokenFlow.java ---------------------------------------------------------------------- diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/ClientCredentialsJWTBearerTokenFlow.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/ClientCredentialsJWTBearerTokenFlow.java new file mode 100644 index 0000000..dd11940 --- /dev/null +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/ClientCredentialsJWTBearerTokenFlow.java @@ -0,0 +1,120 @@ +/* + * 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 static org.jclouds.oauth.v2.config.OAuthProperties.RESOURCE; + +import javax.inject.Inject; +import javax.inject.Named; +import java.util.List; +import java.util.UUID; + +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.ClientCredentialsAuthArgs; +import org.jclouds.oauth.v2.domain.ClientCredentialsClaims; +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 ClientCredentialsJWTBearerTokenFlow implements OAuthFilter { + private static final Joiner ON_SPACE = Joiner.on(" "); + + private final String resource; + private final String audience; + private final Supplier<Credentials> credentialsSupplier; + private final OAuthScopes scopes; + private final long tokenDuration; + private final LoadingCache<ClientCredentialsAuthArgs, Token> tokenCache; + + @Inject + ClientCredentialsJWTBearerTokenFlow(AuthorizeToken loader, @Named(PROPERTY_SESSION_INTERVAL) long tokenDuration, + @Provider Supplier<Credentials> credentialsSupplier, + OAuthScopes scopes, + @Named(AUDIENCE) String audience, + @Named(RESOURCE) String resource) { + this.credentialsSupplier = credentialsSupplier; + this.scopes = scopes; + this.audience = audience; + this.resource = resource; + this.tokenDuration = tokenDuration; + // since the session interval is also the token expiration time requested to the server make the token expire a + // bit before the deadline to make sure there aren't session expiration exceptions + long cacheExpirationSeconds = tokenDuration > 30 ? tokenDuration - 30 : tokenDuration; + this.tokenCache = CacheBuilder.newBuilder().expireAfterWrite(cacheExpirationSeconds, SECONDS).build(loader); + } + + static final class AuthorizeToken extends CacheLoader<ClientCredentialsAuthArgs, Token> { + private final AuthorizationApi api; + + @Inject AuthorizeToken(AuthorizationApi api) { + this.api = api; + } + + @Override public Token load(ClientCredentialsAuthArgs key) throws Exception { + return api.authorize(key.clientId(), key.claims(), key.resource(), key.scope()); + } + } + + @Override public HttpRequest filter(HttpRequest request) throws HttpException { + long now = currentTimeSeconds(); + List<String> configuredScopes = scopes.forRequest(request); + ClientCredentialsClaims claims = ClientCredentialsClaims.create( // + credentialsSupplier.get().identity, // iss + credentialsSupplier.get().identity, // sub + audience, // aud + now + tokenDuration, // exp + now, // nbf + UUID.randomUUID().toString() // jti + ); + ClientCredentialsAuthArgs authArgs = ClientCredentialsAuthArgs.create( + credentialsSupplier.get().identity, + claims, + resource == null ? "" : resource, + configuredScopes.isEmpty() ? null : ON_SPACE.join(configuredScopes) + ); + + Token token = tokenCache.getUnchecked(authArgs); + String authorization = String.format("%s %s", token.tokenType(), token.accessToken()); + return request.toBuilder().addHeader("Authorization", authorization).build(); + } + + long currentTimeSeconds() { + return System.currentTimeMillis() / 1000; + } +} + http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/functions/ClientCredentialsClaimsToAssertion.java ---------------------------------------------------------------------- diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/functions/ClientCredentialsClaimsToAssertion.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/functions/ClientCredentialsClaimsToAssertion.java new file mode 100644 index 0000000..b43f579 --- /dev/null +++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/functions/ClientCredentialsClaimsToAssertion.java @@ -0,0 +1,93 @@ +/* + * 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.oauth.v2.domain.CertificateFingerprint; +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 ClientCredentialsClaimsToAssertion implements Function<Object, String> { + private static final List<String> SUPPORTED_ALGS = ImmutableList.of("RS256", "none"); + + private final Supplier<PrivateKey> privateKey; + private final Supplier<CertificateFingerprint> certFingerprint; + private final Json json; + private final String alg; + + @Inject ClientCredentialsClaimsToAssertion(@Named(JWS_ALG) String alg, + @Authorization Supplier<PrivateKey> privateKey, + @Authorization Supplier<CertificateFingerprint> certFingerprint, + 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.certFingerprint = certFingerprint; + this.json = json; + } + + @Override public String apply(Object input) { + String encodedHeader = String.format("{\"alg\":\"%s\",\"typ\":\"JWT\",\"x5t\":\"%s\"}", alg, certFingerprint.get().fingerprint()); + 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); + } + } +} http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java ---------------------------------------------------------------------- diff --git a/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java b/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java index 1ded5dc..5d0d7cf 100644 --- a/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java +++ b/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java @@ -21,11 +21,13 @@ import static org.jclouds.oauth.v2.OAuthTestUtils.setCredential; import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG; import static org.jclouds.oauth.v2.config.OAuthProperties.RESOURCE; import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE; +import static org.jclouds.oauth.v2.config.OAuthProperties.CERTIFICATE; import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE; import static org.jclouds.providers.AnonymousProviderMetadata.forApiOnEndpoint; import static org.testng.Assert.assertNotNull; import java.util.Properties; +import java.util.UUID; import org.jclouds.apis.BaseApiLiveTest; import org.jclouds.oauth.v2.config.CredentialType; @@ -33,6 +35,7 @@ import org.jclouds.oauth.v2.config.OAuthModule; import org.jclouds.oauth.v2.config.OAuthScopes; import org.jclouds.oauth.v2.config.OAuthScopes.SingleScope; import org.jclouds.oauth.v2.domain.Claims; +import org.jclouds.oauth.v2.domain.ClientCredentialsClaims; import org.jclouds.oauth.v2.domain.Token; import org.jclouds.providers.ProviderMetadata; import org.testng.annotations.DataProvider; @@ -51,6 +54,7 @@ public class AuthorizationApiLiveTest extends BaseApiLiveTest<AuthorizationApi> private String audience; private String credentialType; private String resource; + private String certificate; public AuthorizationApiLiveTest() { provider = "oauth"; @@ -65,7 +69,13 @@ public class AuthorizationApiLiveTest extends BaseApiLiveTest<AuthorizationApi> @DataProvider public Object[][] onlyRunForClientCredentialsSecret() { return (CredentialType.fromValue(credentialType) == CredentialType.CLIENT_CREDENTIALS_SECRET) ? - OAuthTestUtils.SINGLE_NO_ARG_INVOCATION : OAuthTestUtils.NO_INVOCATIONS; + OAuthTestUtils.SINGLE_NO_ARG_INVOCATION : OAuthTestUtils.NO_INVOCATIONS; + } + + @DataProvider + public Object[][] onlyRunForClientCredentialsP12() { + return (CredentialType.fromValue(credentialType) == CredentialType.CLIENT_CREDENTIALS_P12_AND_CERTIFICATE) ? + OAuthTestUtils.SINGLE_NO_ARG_INVOCATION : OAuthTestUtils.NO_INVOCATIONS; } @Test(dataProvider = "onlyRunForP12PrivateKeyCredentials") @@ -98,6 +108,23 @@ public class AuthorizationApiLiveTest extends BaseApiLiveTest<AuthorizationApi> assertNotNull(token, "no token when authorizing " + identity); } + @Test(dataProvider = "onlyRunForClientCredentialsP12") + public void authenticateClientCredentialsP12Test() throws Exception { + long now = System.currentTimeMillis() / 1000; + ClientCredentialsClaims claims = ClientCredentialsClaims.create( + identity, // iss + identity, // sub + audience, // aud + now + 3600, // exp + now, // iat + UUID.randomUUID().toString() + ); + + Token token = api.authorize(identity, claims, resource, null); + + 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(); @@ -121,6 +148,11 @@ public class AuthorizationApiLiveTest extends BaseApiLiveTest<AuthorizationApi> // Set the credential specific properties. if (CredentialType.fromValue(credentialType) == CredentialType.CLIENT_CREDENTIALS_SECRET) { resource = checkNotNull(setIfTestSystemPropertyPresent(props, RESOURCE), "test." + RESOURCE); + } else if (CredentialType.fromValue(credentialType) == CredentialType.CLIENT_CREDENTIALS_P12_AND_CERTIFICATE) { + audience = checkNotNull(setIfTestSystemPropertyPresent(props, AUDIENCE), "test.jclouds.oauth.audience"); + resource = checkNotNull(setIfTestSystemPropertyPresent(props, RESOURCE), "test." + RESOURCE); + certificate = setCredential(props, CERTIFICATE); + credential = setCredential(props, "oauth.credential"); } else if (CredentialType.fromValue(credentialType) == CredentialType.P12_PRIVATE_KEY_CREDENTIALS) { audience = checkNotNull(setIfTestSystemPropertyPresent(props, AUDIENCE), "test.jclouds.oauth.audience"); credential = setCredential(props, "oauth.credential"); http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java ---------------------------------------------------------------------- diff --git a/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java b/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java index adc7585..89fe953 100644 --- a/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java +++ b/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java @@ -23,10 +23,12 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static org.jclouds.Constants.PROPERTY_MAX_RETRIES; import static org.jclouds.oauth.v2.config.CredentialType.P12_PRIVATE_KEY_CREDENTIALS; import static org.jclouds.oauth.v2.config.CredentialType.CLIENT_CREDENTIALS_SECRET; +import static org.jclouds.oauth.v2.config.CredentialType.CLIENT_CREDENTIALS_P12_AND_CERTIFICATE; +import static org.jclouds.oauth.v2.config.OAuthProperties.CERTIFICATE; import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE; -import static org.jclouds.oauth.v2.config.OAuthProperties.RESOURCE; import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE; import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG; +import static org.jclouds.oauth.v2.config.OAuthProperties.RESOURCE; import static org.jclouds.util.Strings2.toStringAndClose; import static org.testng.Assert.assertEquals; import static org.testng.Assert.fail; @@ -42,6 +44,7 @@ import org.jclouds.oauth.v2.config.OAuthModule; import org.jclouds.oauth.v2.config.OAuthScopes; import org.jclouds.oauth.v2.config.OAuthScopes.SingleScope; import org.jclouds.oauth.v2.domain.Claims; +import org.jclouds.oauth.v2.domain.ClientCredentialsClaims; import org.jclouds.oauth.v2.domain.Token; import org.jclouds.rest.AnonymousHttpApiMetadata; import org.jclouds.rest.AuthorizationException; @@ -76,13 +79,28 @@ public class AuthorizationApiMockTest { 1328569781 // iat ); + private static final String clientCredentialsHeader = "{\"alg\":\"RS256\",\"typ\":\"JWT\",\"x5t\":\"RZk6mx4gGECvF6XWZWkK9qaGdHk=\"}"; + private static final String clientCredentialsClaims = "{\"iss\":\"a242b44e-2c2a-3bdd-b094-6152da263c54\"," + + "\"sub\":\"a242b44e-2c2a-3bdd-b094-6152da263c54\",\"aud\":" + + "\"https://login.microsoftonline.com/a242ccee-1a1a-3bdd-b094-6152da263c54/oauth2/token\"" + + ",\"exp\":1328573381,\"nbf\":1328569781,\"jti\":\"abcdefgh\"}"; + + private static final ClientCredentialsClaims CLIENT_CREDENTIALS_CLAIMS = ClientCredentialsClaims.create( + "a242b44e-2c2a-3bdd-b094-6152da263c54", // iss + "a242b44e-2c2a-3bdd-b094-6152da263c54", // sub + "https://login.microsoftonline.com/a242ccee-1a1a-3bdd-b094-6152da263c54/oauth2/token", //aud + 1328573381, // exp + 1328569781, // nbf + "abcdefgh" // jti + ); + public void testGenerateJWTRequest() throws Exception { MockWebServer server = new MockWebServer(); try { server.enqueue(new MockResponse().setBody("{\n" - + " \"access_token\" : \"1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M\",\n" - + " \"token_type\" : \"Bearer\",\n" + " \"expires_in\" : 3600\n" + "}")); + + " \"access_token\" : \"1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M\",\n" + + " \"token_type\" : \"Bearer\",\n" + " \"expires_in\" : 3600\n" + "}")); server.play(); AuthorizationApi api = api(server.getUrl("/"), P12_PRIVATE_KEY_CREDENTIALS); @@ -95,16 +113,16 @@ public class AuthorizationApiMockTest { assertEquals(request.getHeader("Content-Type"), "application/x-www-form-urlencoded"); assertEquals( - new String(request.getBody(), UTF_8), // - "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&" - + - // Base64 Encoded Header - "assertion=" - + Joiner.on('.').join(encoding.encode(header.getBytes(UTF_8)), - encoding.encode(claims.getBytes(UTF_8)), - // Base64 encoded {header}.{claims} signature (using - // SHA256) - "W2Lesr_98AzVYiMbzxFqmwcOjpIWlwqkC6pNn1fXND9oSDNNnFhy-AAR6DKH-x9ZmxbY80" + new String(request.getBody(), UTF_8), // + "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&" + + + // Base64 Encoded Header + "assertion=" + + Joiner.on('.').join(encoding.encode(header.getBytes(UTF_8)), + encoding.encode(claims.getBytes(UTF_8)), + // Base64 encoded {header}.{claims} signature (using + // SHA256) + "W2Lesr_98AzVYiMbzxFqmwcOjpIWlwqkC6pNn1fXND9oSDNNnFhy-AAR6DKH-x9ZmxbY80" + "R5fH-OCeWumXlVgceKN8Z2SmgQsu8ElTpypQA54j_5j8vUImJ5hsOUYPeyF1U2BUzZ3L5g" + "03PXBA0YWwRU9E1ChH28dQBYuGiUmYw")); } finally { @@ -161,6 +179,46 @@ public class AuthorizationApiMockTest { } } + public void testGenerateClientCredentialsJWTRequest() throws Exception { + MockWebServer server = new MockWebServer(); + + String identity = "user"; + String resource = "http://management.azure.com/"; + String encoded_resource = "http%3A//management.azure.com/"; + + try { + server.enqueue(new MockResponse().setBody("{\n" + + " \"access_token\" : \"1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M\",\n" + + " \"token_type\" : \"Bearer\",\n" + " \"expires_in\" : 3600\n" + "}")); + server.play(); + + AuthorizationApi api = api(server.getUrl("/"), CLIENT_CREDENTIALS_P12_AND_CERTIFICATE); + assertEquals(api.authorize(identity, CLIENT_CREDENTIALS_CLAIMS, resource, null), TOKEN); + + RecordedRequest request = server.takeRequest(); + assertEquals(request.getMethod(), "POST"); + assertEquals(request.getHeader("Accept"), APPLICATION_JSON); + assertEquals(request.getHeader("Content-Type"), "application/x-www-form-urlencoded"); + + assertEquals( + new String(request.getBody(), UTF_8), + "grant_type=client_credentials&" + + "client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&" + + "client_id=" + identity + "&" + + "client_assertion=" + + Joiner.on(".").join(encoding.encode(clientCredentialsHeader.getBytes(UTF_8)), + encoding.encode(clientCredentialsClaims.getBytes(UTF_8)), + "ip3i0YLlunb4iq8sUMlpYDKnEuzmvlLpF4NQvn_aiysO5cuT5QHuGREq" + + "gyEa-UMhfZoW49ggUWjS7YBT00r64cFE3dovaNMiZYZuVWu_" + + "FpqO2QlwV7uXqhaRIE0cyabbKG44YJwA-NE4rtFZedFMo94F" + + "6aOz2FN3en8zS9UVqmM" + ) + "&" + + "resource=" + encoded_resource); + } finally { + server.shutdown(); + } + } + private final BaseEncoding encoding = base64Url().omitPadding(); private AuthorizationApi api(URL url, CredentialType credentialType) throws IOException { @@ -171,6 +229,7 @@ public class AuthorizationApiMockTest { overrides.setProperty(AUDIENCE, "https://accounts.google.com/o/oauth2/token"); overrides.setProperty(RESOURCE, "https://management.azure.com/"); overrides.setProperty(PROPERTY_MAX_RETRIES, "1"); + overrides.setProperty(CERTIFICATE, toStringAndClose(OAuthTestUtils.class.getResourceAsStream("/testcert.pem"))); return ContextBuilder.newBuilder(AnonymousHttpApiMetadata.forApi(AuthorizationApi.class)) .credentials("foo", toStringAndClose(OAuthTestUtils.class.getResourceAsStream("/testpk.pem"))) http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/test/resources/testcert.pem ---------------------------------------------------------------------- diff --git a/apis/oauth/src/test/resources/testcert.pem b/apis/oauth/src/test/resources/testcert.pem new file mode 100644 index 0000000..19e744d --- /dev/null +++ b/apis/oauth/src/test/resources/testcert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDkTCCAnmgAwIBAgIJAISFMZeicwQVMA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRMwEQYDVQQHDApTYW50YSBDcnV6 +MQ8wDQYDVQQKDAZKQ1Rlc3QxFTATBgNVBAMMDGpjdGVzdHNwY2VydDAeFw0xNjA1 +MTQxNzUzMzFaFw0yNjA1MTIxNzUzMzFaMF8xCzAJBgNVBAYTAlVTMRMwEQYDVQQI +DApDYWxpZm9ybmlhMRMwEQYDVQQHDApTYW50YSBDcnV6MQ8wDQYDVQQKDAZKQ1Rl +c3QxFTATBgNVBAMMDGpjdGVzdHNwY2VydDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALKkugQxFZq224O2Hb1Q+J8VyXs+fjbwGSErhyY0ynENvCaq2trG +Mia0NAqtRZmG5Fn3KEYo9yCjjm0N34mer5u8X8aErvBa1LTkwrK2dQQ1oXGtbFn1 +dTqho00YwxzMxT3raDw9xXhexloDQbr/EAm4f1zu+05BSC3xSDVvvzARBPSseVfw +gxWpS4M4aQp9M6Tv2ENYnecfl6StkaPdxaguJeVdpSoBe7piEEz1f2LEoC3Fw6De +JIUgzQyVoffCWCA+RCf3o8GOqce0+INW50rcEv1JrGrDSUeEUYDCg+FniT9vKBsm +sV8u3o1YxvtWmAY3KtXC7akwqHSdifecLtECAwEAAaNQME4wHQYDVR0OBBYEFL2W +1+sMin4FbE9RFQr6FqEh9NFUMB8GA1UdIwQYMBaAFL2W1+sMin4FbE9RFQr6FqEh +9NFUMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADaHB0PbSooF1PkD +2KwwYck7cm9C0jmnUVdcmJ6GrG8OdXEP14E4rUzrstFK0XSPHYoBH0p0xMISutHD +RVLgsxXbnPhXVKaTuDspgedSAaHPFQJNEtMHHCNaSS2Vyh+2Ha9HLD7dsy+9QBKf +Et4MMLXT/n4WVzKJcweWI/T8oIIbfHzPTo+jWYfTxxKJcL+C/GY2QREkKs+5mR2t +N4q69ydcjeajB9F8XPbJlTDen+6ofwtQ45tS5w5EjV3SvOh4QOEVz1su9F5Ojw7Q +oPuUKMynxeo2E6FsiNj5m+8NPLKiX2phnKMogbJyOhligj1QRR+zBc/62aPCP6Z4 +2pWp7Lc= +-----END CERTIFICATE-----
