All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.sqlserver.jdbc.KeyVaultTokenCredential Maven / Gradle / Ivy

There is a newer version: 12.7.0.jre11-preview
Show newest version
/*
 * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
 * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
 */

package com.microsoft.sqlserver.jdbc;

import com.azure.core.annotation.Immutable;
import com.azure.core.credential.AccessToken;
import com.azure.core.credential.TokenCredential;
import com.azure.core.credential.TokenRequestContext;
import com.microsoft.aad.msal4j.ClientCredentialFactory;
import com.microsoft.aad.msal4j.ClientCredentialParameters;
import com.microsoft.aad.msal4j.ConfidentialClientApplication;
import com.microsoft.aad.msal4j.IAuthenticationResult;
import com.microsoft.aad.msal4j.IClientCredential;
import com.microsoft.aad.msal4j.SilentParameters;
import java.net.MalformedURLException;
import java.text.MessageFormat;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet;
import java.util.concurrent.CompletableFuture;
import reactor.core.publisher.Mono;


/**
 * An AAD credential that acquires a token with a client secret for an AAD application.
 */
@Immutable
class KeyVaultTokenCredential implements TokenCredential {
    private static final String NULL_VALUE = "R_NullValue";

    private final String clientId;
    private final String clientSecret;
    private final SQLServerKeyVaultAuthenticationCallback authenticationCallback;
    private String authorization;
    private ConfidentialClientApplication confidentialClientApplication;
    private String resource;
    private String scope;

    /**
     * Creates a KeyVaultTokenCredential with the given identity client options.
     *
     * @param clientId
     *        the client ID of the application
     * @param clientSecret
     *        the secret value of the AAD application
     * @throws SQLServerException
     */
    KeyVaultTokenCredential(String clientId, String clientSecret) throws SQLServerException {
        if (null == clientId || clientId.isEmpty()) {
            MessageFormat form = new MessageFormat(SQLServerException.getErrString(NULL_VALUE));
            Object[] msgArgs1 = {"Client ID"};
            throw new SQLServerException(form.format(msgArgs1), null);
        }

        if (null == clientSecret || clientSecret.isEmpty()) {
            MessageFormat form = new MessageFormat(SQLServerException.getErrString(NULL_VALUE));
            Object[] msgArgs1 = {"Client Secret"};
            throw new SQLServerException(form.format(msgArgs1), null);
        }

        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.authenticationCallback = null;
    }

    /**
     * Creates a KeyVaultTokenCredential with the given identity client options.
     *
     * @param authenticationCallback
     *        The authentication callback that gets invoked when an access token is requested.
     */
    KeyVaultTokenCredential(SQLServerKeyVaultAuthenticationCallback authenticationCallback) {
        this.authenticationCallback = authenticationCallback;
        this.clientId = null;
        this.clientSecret = null;
    }

    @Override
    public Mono getToken(TokenRequestContext request) {
        if (null != authenticationCallback) {
            /*
             * If the callback is not null, invoke the callback to get the token. This gets invoked each time this
             * method is called and will not cache the token. It's the callback's responsibility to return a valid token
             * each time it's invoked.
             */
            String accessToken = authenticationCallback.getAccessToken(this.authorization, this.resource, this.scope);
            return Mono.just(new AccessToken(accessToken, OffsetDateTime.MIN));
        }

        // gets the token from MSAL
        return authenticateWithConfidentialClientCache(request).onErrorResume(t -> Mono.empty())
                .switchIfEmpty(Mono.defer(() -> authenticateWithConfidentialClient(request)));
    }

    /**
     * Sets the authority that will be used for authentication.
     *
     * @param authorization
     *        The name of the authorization.
     * @return The updated {@link KeyVaultTokenCredential} instance.
     */
    KeyVaultTokenCredential setAuthorization(String authorization) {
        if (null != this.authorization && this.authorization.equals(authorization)) {
            return this;
        }
        this.authorization = authorization;
        confidentialClientApplication = getConfidentialClientApplication();
        return this;
    }

    /**
     * Creates an instance of {@link ConfidentialClientApplication} using the provided client id and secret.
     *
     * @return An instance of {@link ConfidentialClientApplication}.
     */
    private ConfidentialClientApplication getConfidentialClientApplication() {
        if (null == clientId) {
            MessageFormat form = new MessageFormat(SQLServerException.getErrString(NULL_VALUE));
            Object[] msgArgs1 = {"Client ID"};
            throw new IllegalArgumentException(form.format(msgArgs1), null);
        }

        if (null == authorization) {
            MessageFormat form = new MessageFormat(SQLServerException.getErrString(NULL_VALUE));
            Object[] msgArgs1 = {"Authorization"};
            throw new IllegalArgumentException(form.format(msgArgs1), null);
        }

        if (null == clientSecret) {
            MessageFormat form = new MessageFormat(SQLServerException.getErrString(NULL_VALUE));
            Object[] msgArgs1 = {"Client Secret"};
            throw new IllegalArgumentException(form.format(msgArgs1), null);
        }

        // Create the credential using the MSAL factory method.
        IClientCredential credential;
        credential = ClientCredentialFactory.createFromSecret(clientSecret);
        ConfidentialClientApplication.Builder applicationBuilder = ConfidentialClientApplication.builder(clientId,
                credential);
        try {
            applicationBuilder = applicationBuilder.authority(authorization);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
        return applicationBuilder.build();
    }

    /**
     * Attempts to get the access token from the client cache if it's not expired. If it's expired this returns an empty
     * response.
     * 
     * @param request
     *        The context for requesting the token including the scope.
     * @return The cached access token if it's not expired.
     */
    private Mono authenticateWithConfidentialClientCache(TokenRequestContext request) {
        return Mono.fromFuture(() -> {
            SilentParameters.SilentParametersBuilder parametersBuilder = SilentParameters
                    .builder(new HashSet<>(request.getScopes()));
            try {
                return confidentialClientApplication.acquireTokenSilently(parametersBuilder.build());
            } catch (MalformedURLException e) {
                return getFailedCompletableFuture(new RuntimeException(e));
            }
        }).map(ar -> new AccessToken(ar.accessToken(),
                OffsetDateTime.ofInstant(ar.expiresOnDate().toInstant(), ZoneOffset.UTC))).filter(t -> !t.isExpired());
    }

    /**
     * If fetching the token resulted in an error, this method returns the error wrapped in a completable future.
     * 
     * @param e
     *        The original exception.
     * @return A {@link CompletableFuture} that completes with an error.
     */
    private CompletableFuture getFailedCompletableFuture(Exception e) {
        CompletableFuture completableFuture = new CompletableFuture<>();
        completableFuture.completeExceptionally(e);
        return completableFuture;
    }

    /**
     * Attempts to get the access token from the {@link ConfidentialClientApplication} for the requested scope.
     * 
     * @param request
     *        The context for requesting the token that includes the scope.
     * @return The access token.
     */
    private Mono authenticateWithConfidentialClient(TokenRequestContext request) {
        return Mono
                .fromFuture(() -> confidentialClientApplication
                        .acquireToken(ClientCredentialParameters.builder(new HashSet<>(request.getScopes())).build()))
                .map(ar -> new AccessToken(ar.accessToken(),
                        OffsetDateTime.ofInstant(ar.expiresOnDate().toInstant(), ZoneOffset.UTC)));
    }

    /**
     * Sets the resource name.
     * 
     * @param resource
     *        The resource name.
     */
    void setResource(String resource) {
        this.resource = resource;
    }

    /**
     * Sets the scope for the access token.
     * 
     * @param scope
     *        The scope for the access token.
     */
    void setScope(String scope) {
        this.scope = scope;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy