Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial code for MSI #661

Merged
merged 10 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ private AuthenticationResultSupplier getAuthenticationResultSupplier(MsalRequest
supplier = new AcquireTokenByOnBehalfOfSupplier(
(ConfidentialClientApplication) this,
(OnBehalfOfRequest) msalRequest);
} else if (msalRequest instanceof ManagedIdentityRequest) {
supplier = new AcquireTokenByManagedIdentitySupplier(
(ManagedIdentityApplication) this,
(ManagedIdentityRequest) msalRequest);
} else {
supplier = new AcquireTokenByAuthorizationGrantSupplier(
this,
Expand Down Expand Up @@ -329,7 +333,7 @@ public abstract static class Builder<T extends Builder<T>> {
private String azureRegion;
private Integer connectTimeoutForDefaultHttpClient;
private Integer readTimeoutForDefaultHttpClient;
private boolean instanceDiscovery = true;
protected boolean isInstanceDiscoveryEnabled = true;

/**
* Constructor to create instance of Builder of client application
Expand Down Expand Up @@ -673,7 +677,7 @@ public T azureRegion(String val) {
yet still want MSAL to accept any authority that you will provide,
you can use a ``False`` to unconditionally disable Instance Discovery. */
public T instanceDiscovery(boolean val) {
neha-bhargava marked this conversation as resolved.
Show resolved Hide resolved
instanceDiscovery = val;
isInstanceDiscoveryEnabled = val;
return self();
}

Expand Down Expand Up @@ -705,7 +709,7 @@ public T instanceDiscovery(boolean val) {
clientCapabilities = builder.clientCapabilities;
autoDetectRegion = builder.autoDetectRegion;
azureRegion = builder.azureRegion;
instanceDiscovery = builder.instanceDiscovery;
instanceDiscovery = builder.isInstanceDiscoveryEnabled;

if (aadAadInstanceDiscoveryResponse != null) {
AadInstanceDiscoveryProvider.cacheInstanceDiscoveryMetadata(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.microsoft.aad.msal4j;

import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.SerializeException;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.beans.Encoder;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.URISyntaxException;

//base class for all sources that support managed identity
abstract class AbstractManagedIdentitySource {

protected static final String TIMEOUT_ERROR = "[Managed Identity] Authentication unavailable. The request to the managed identity endpoint timed out.";
bgavrilMS marked this conversation as resolved.
Show resolved Hide resolved
private static final Logger LOG = LoggerFactory.getLogger(AbstractManagedIdentitySource.class);
private static final String MANAGED_IDENTITY_NO_RESPONSE_RECEIVED = "[Managed Identity] Authentication unavailable. No response received from the managed identity endpoint.";

protected final ManagedIdentityRequest managedIdentityRequest;
private ServiceBundle serviceBundle;
private ManagedIdentitySourceType managedIdentitySourceType;

@Getter
@Setter
private boolean isUserAssignedManagedIdentity;
@Getter
@Setter
private String managedIdentityUserAssignedClientId;
@Getter
@Setter
private String managedIdentityUserAssignedResourceId;

public AbstractManagedIdentitySource(MsalRequest msalRequest, ServiceBundle serviceBundle,
ManagedIdentitySourceType sourceType) {
this.managedIdentityRequest = (ManagedIdentityRequest) msalRequest;
this.managedIdentitySourceType = sourceType;
this.serviceBundle = serviceBundle;
}

public ManagedIdentityResponse getManagedIdentityResponse(
ManagedIdentityParameters parameters) {

createManagedIdentityRequest(parameters.resource);
IHttpResponse response;

try {
HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, managedIdentityRequest.computeURI().toString(), managedIdentityRequest.headers);
response = HttpHelper.executeHttpRequest(httpRequest, managedIdentityRequest.requestContext(), serviceBundle);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
} catch (MsalClientException e) {
if (e.getCause() instanceof SocketException) {
throw new MsalManagedIdentityException(MsalError.MANAGED_IDENTITY_UNREACHABLE_NETWORK, e.getMessage(), managedIdentitySourceType);
}

throw e;
}

return handleResponse(parameters, response);
}

public ManagedIdentityResponse handleResponse(
ManagedIdentityParameters parameters,
IHttpResponse response) {

String message;

try {
if (response.statusCode() == HttpURLConnection.HTTP_OK) {
neha-bhargava marked this conversation as resolved.
Show resolved Hide resolved
LOG.info("[Managed Identity] Successful response received.");
return getSuccessfulResponse(response);
} else {
message = getMessageFromErrorResponse(response);
LOG.error(
String.format("[Managed Identity] request failed, HttpStatusCode: %s Error message: %s",
response.statusCode(), message));
throw new MsalManagedIdentityException(AuthenticationErrorCode.MANAGED_IDENTITY_REQUEST_FAILED, message, managedIdentitySourceType);
}
} catch (Exception e) {
if (!(e instanceof MsalServiceException)) {
LOG.error(
String.format("[Managed Identity] Exception: %s Http status code: %s", e.getMessage(),
response != null ? response.statusCode() : ""));
message = MsalErrorMessage.MANAGED_IDENTITY_UNEXPECTED_RESPONSE;
} else {
throw e;
}
throw new MsalManagedIdentityException(AuthenticationErrorCode.MANAGED_IDENTITY_REQUEST_FAILED, message, managedIdentitySourceType);
}
}

public abstract void createManagedIdentityRequest(String resource);

protected ManagedIdentityResponse getSuccessfulResponse(IHttpResponse response) {

ManagedIdentityResponse managedIdentityResponse = JsonHelper
.convertJsonToObject(response.body(), ManagedIdentityResponse.class);

if (managedIdentityResponse == null || managedIdentityResponse.getAccessToken() == null
|| managedIdentityResponse.getAccessToken().isEmpty() || managedIdentityResponse.getExpiresOn() == null
|| managedIdentityResponse.getExpiresOn().isEmpty()) {
LOG.error("[Managed Identity] Response is either null or insufficient for authentication.");
neha-bhargava marked this conversation as resolved.
Show resolved Hide resolved
throw new MsalManagedIdentityException(MsalError.MANAGED_IDENTITY_REQUEST_FAILED, MsalErrorMessage.MANAGED_IDENTITY_UNEXPECTED_RESPONSE, managedIdentitySourceType);
}

return managedIdentityResponse;
}

protected String getMessageFromErrorResponse(IHttpResponse response) {
ManagedIdentityErrorResponse managedIdentityErrorResponse =
JsonHelper.convertJsonToObject(response.body(), ManagedIdentityErrorResponse.class);

if (managedIdentityErrorResponse == null) {
return MANAGED_IDENTITY_NO_RESPONSE_RECEIVED;
}

if (managedIdentityErrorResponse.getMessage() != null && !managedIdentityErrorResponse.getMessage().isEmpty()) {
return String.format("[Managed Identity] Error Message: %s Managed Identity Correlation ID: %s Use this Correlation ID for further investigation.",
managedIdentityErrorResponse.getMessage(), managedIdentityErrorResponse.getCorrelationId());
}

return String.format("[Managed Identity] Error Code: %s Error Message: %s",
managedIdentityErrorResponse.getError(), managedIdentityErrorResponse.getErrorDescription());
}

protected static IEnvironmentVariables getEnvironmentVariables(ManagedIdentityParameters parameters) {
return parameters.environmentVariables == null ? new EnvironmentVariables() : parameters.environmentVariables;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.microsoft.aad.msal4j;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashSet;
import java.util.Set;

class AcquireTokenByManagedIdentitySupplier extends AuthenticationResultSupplier {

private static final Logger LOG = LoggerFactory.getLogger(AcquireTokenByManagedIdentitySupplier.class);

private ManagedIdentityParameters managedIdentityParameters;

AcquireTokenByManagedIdentitySupplier(ManagedIdentityApplication managedIdentityApplication, MsalRequest msalRequest) {
super(managedIdentityApplication, msalRequest);
this.managedIdentityParameters = (ManagedIdentityParameters) msalRequest.requestContext().apiParameters();
}

@Override
AuthenticationResult execute() throws Exception {

if (StringHelper.isNullOrBlank(managedIdentityParameters.resource)) {
throw new MsalClientException(
MsalError.SCOPES_REQUIRED,
MsalErrorMessage.SCOPES_REQUIRED);
}

TokenRequestExecutor tokenRequestExecutor = new TokenRequestExecutor(
clientApplication.authenticationAuthority,
msalRequest,
clientApplication.getServiceBundle()
);

if (!managedIdentityParameters.forceRefresh) {
bgavrilMS marked this conversation as resolved.
Show resolved Hide resolved
LOG.debug("ForceRefresh set to false. Attempting cache lookup");

try {
Set<String> scopes = new HashSet<>();
scopes.add(this.managedIdentityParameters.resource);
SilentParameters parameters = SilentParameters
.builder(scopes)
.build();

RequestContext context = new RequestContext(
this.clientApplication,
PublicApi.ACQUIRE_TOKEN_SILENTLY,
parameters);

SilentRequest silentRequest = new SilentRequest(
parameters,
this.clientApplication,
context,
null);

AcquireTokenSilentSupplier supplier = new AcquireTokenSilentSupplier(
this.clientApplication,
silentRequest);

return supplier.execute();
} catch (MsalClientException ex) {
bgavrilMS marked this conversation as resolved.
Show resolved Hide resolved
if (ex.errorCode().equals(AuthenticationErrorCode.CACHE_MISS)) {
LOG.debug(String.format("Cache lookup failed: %s", ex.getMessage()));
return fetchNewAccessTokenAndSaveToCache(tokenRequestExecutor, clientApplication.authenticationAuthority.host);
} else {
LOG.error("Error occurred while cache lookup. " + ex.getMessage());
throw ex;
}
}
}

LOG.info("Skipped looking for an Access Token in the cache because forceRefresh or Claims were set. ");
return fetchNewAccessTokenAndSaveToCache(tokenRequestExecutor, clientApplication.authenticationAuthority.host);
}

private AuthenticationResult fetchNewAccessTokenAndSaveToCache(TokenRequestExecutor tokenRequestExecutor, String host) throws Exception {

ManagedIdentityClient managedIdentityClient = new ManagedIdentityClient(msalRequest, tokenRequestExecutor.getServiceBundle());

ManagedIdentityResponse managedIdentityResponse = managedIdentityClient
.getManagedIdentityResponse(managedIdentityParameters);

AuthenticationResult authenticationResult = createFromManagedIdentityResponse(managedIdentityResponse);
clientApplication.tokenCache.saveTokens(tokenRequestExecutor, authenticationResult, clientApplication.authenticationAuthority.host);
return authenticationResult;
}

private AuthenticationResult createFromManagedIdentityResponse(ManagedIdentityResponse managedIdentityResponse) {
long expiresOn = Long.valueOf(managedIdentityResponse.expiresOn);
long refreshOn = expiresOn > 2 * 3600 ? (expiresOn / 2) : 0L;
neha-bhargava marked this conversation as resolved.
Show resolved Hide resolved

return AuthenticationResult.builder()
.accessToken(managedIdentityResponse.getAccessToken())
.scopes(managedIdentityParameters.getResource())
.expiresOn(expiresOn)
.extExpiresOn(0)
.refreshOn(refreshOn)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.microsoft.aad.msal4j;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

class AppServiceManagedIdentitySource extends AbstractManagedIdentitySource{

private static final Logger LOG = LoggerFactory.getLogger(AppServiceManagedIdentitySource.class);

// MSI Constants. Docs for MSI are available here https://docs.microsoft.com/azure/app-service/overview-managed-identity
private static final String APP_SERVICE_MSI_API_VERSION = "2019-08-01";
private static final String SECRET_HEADER_NAME = "X-IDENTITY-HEADER";
private static URI endpointUri;

private URI endpoint;
private String secret;

@Override
public void createManagedIdentityRequest(String resource) {
managedIdentityRequest.baseEndpoint = endpoint;
managedIdentityRequest.method = HttpMethod.GET;

Map<String, String> headers = new HashMap<>();
headers.put(SECRET_HEADER_NAME, secret);
managedIdentityRequest.headers = headers;

Map<String, String> queryParameters = new HashMap<>();
queryParameters.put("api-version", APP_SERVICE_MSI_API_VERSION );
queryParameters.put("resource", resource);

if (!StringHelper.isNullOrBlank(getManagedIdentityUserAssignedClientId()))
{
LOG.info("[Managed Identity] Adding user assigned client id to the request.");
queryParameters.put(Constants.MANAGED_IDENTITY_CLIENT_ID, getManagedIdentityUserAssignedClientId());
}

if (!StringHelper.isNullOrBlank(getManagedIdentityUserAssignedResourceId()))
{
LOG.info("[Managed Identity] Adding user assigned resource id to the request.");
queryParameters.put(Constants.MANAGED_IDENTITY_RESOURCE_ID, getManagedIdentityUserAssignedResourceId());
}

managedIdentityRequest.queryParameters = queryParameters;
}

private AppServiceManagedIdentitySource(MsalRequest msalRequest, ServiceBundle serviceBundle, URI endpoint, String secret)
{
super(msalRequest, serviceBundle, ManagedIdentitySourceType.AppService);
this.endpoint = endpoint;
this.secret = secret;
}

protected static AbstractManagedIdentitySource create(MsalRequest msalRequest, ServiceBundle serviceBundle) {

IEnvironmentVariables environmentVariables = getEnvironmentVariables((ManagedIdentityParameters) msalRequest.requestContext().apiParameters());
String msiSecret = environmentVariables.getEnvironmentVariable(Constants.IDENTITY_HEADER);
String msiEndpoint = environmentVariables.getEnvironmentVariable(Constants.IDENTITY_ENDPOINT);

return validateEnvironmentVariables(msiEndpoint, msiSecret)
? new AppServiceManagedIdentitySource(msalRequest, serviceBundle, endpointUri, msiSecret)
: null;
}

private static boolean validateEnvironmentVariables(String msiEndpoint, String secret)
{
endpointUri = null;

// if BOTH the env vars endpoint and secret values are null, this MSI provider is unavailable.
if (StringHelper.isNullOrBlank(msiEndpoint) || StringHelper.isNullOrBlank(secret))
{
LOG.info("[Managed Identity] App service managed identity is unavailable.");
return false;
}

try
{
endpointUri = new URI(msiEndpoint);
}
catch (URISyntaxException ex)
{
throw new MsalManagedIdentityException(MsalError.INVALID_MANAGED_IDENTITY_ENDPOINT, String.format(
MsalErrorMessage.MANAGED_IDENTITY_ENDPOINT_INVALID_URI_ERROR, "IDENTITY_ENDPOINT", msiEndpoint, "App Service"),
ManagedIdentitySourceType.AppService);
}

LOG.info("[Managed Identity] Environment variables validation passed for app service managed identity. Endpoint URI: {endpointUri}. Creating App Service managed identity.");
return true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,10 @@ public class AuthenticationErrorCode {
* slow response, and this may be resolvable by increasing timeouts. For more details, see https://aka.ms/msal4j-http-client
*/
public final static String HTTP_TIMEOUT = "http_timeout";

/**
* Indicates that a request to managed identity endpoint failed, see error message for detailed reason and correlation id.
* For more information on managed identity see https://aka.ms/msal4j-managed-identity.
*/
public static final String MANAGED_IDENTITY_REQUEST_FAILED = "managed_identity_request_failed";
}
Loading