mirror of https://github.com/apache/jclouds.git
client credentials JWT support
This commit is contained in:
parent
ccd1ef2b4d
commit
f46b38dd89
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
@ -16,10 +16,11 @@
|
|||
*/
|
||||
package org.jclouds.oauth.v2.config;
|
||||
|
||||
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.BEARER_TOKEN_CREDENTIALS;
|
||||
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.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.filters.BearerTokenFromCredentials;
|
||||
import org.jclouds.oauth.v2.filters.ClientCredentialsSecretFlow;
|
||||
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.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
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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")))
|
||||
|
|
|
@ -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-----
|
Loading…
Reference in New Issue