Skip to content

Commit aa7857a

Browse files
Merge pull request #661 from AzureAD/nebharg/MsiInitial
Add initial code for MSI
2 parents 4dc7a5e + 2a264bb commit aa7857a

36 files changed

+1775
-22
lines changed

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ private AuthenticationResultSupplier getAuthenticationResultSupplier(MsalRequest
292292
supplier = new AcquireTokenByOnBehalfOfSupplier(
293293
(ConfidentialClientApplication) this,
294294
(OnBehalfOfRequest) msalRequest);
295+
} else if (msalRequest instanceof ManagedIdentityRequest) {
296+
supplier = new AcquireTokenByManagedIdentitySupplier(
297+
(ManagedIdentityApplication) this,
298+
(ManagedIdentityRequest) msalRequest);
295299
} else {
296300
supplier = new AcquireTokenByAuthorizationGrantSupplier(
297301
this,
@@ -329,7 +333,7 @@ public abstract static class Builder<T extends Builder<T>> {
329333
private String azureRegion;
330334
private Integer connectTimeoutForDefaultHttpClient;
331335
private Integer readTimeoutForDefaultHttpClient;
332-
private boolean instanceDiscovery = true;
336+
protected boolean isInstanceDiscoveryEnabled = true;
333337

334338
/**
335339
* Constructor to create instance of Builder of client application
@@ -673,7 +677,7 @@ public T azureRegion(String val) {
673677
yet still want MSAL to accept any authority that you will provide,
674678
you can use a ``False`` to unconditionally disable Instance Discovery. */
675679
public T instanceDiscovery(boolean val) {
676-
instanceDiscovery = val;
680+
isInstanceDiscoveryEnabled = val;
677681
return self();
678682
}
679683

@@ -705,7 +709,7 @@ public T instanceDiscovery(boolean val) {
705709
clientCapabilities = builder.clientCapabilities;
706710
autoDetectRegion = builder.autoDetectRegion;
707711
azureRegion = builder.azureRegion;
708-
instanceDiscovery = builder.instanceDiscovery;
712+
instanceDiscovery = builder.isInstanceDiscoveryEnabled;
709713

710714
if (aadAadInstanceDiscoveryResponse != null) {
711715
AadInstanceDiscoveryProvider.cacheInstanceDiscoveryMetadata(
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
import com.nimbusds.oauth2.sdk.ParseException;
7+
import com.nimbusds.oauth2.sdk.SerializeException;
8+
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
9+
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
10+
import lombok.Getter;
11+
import lombok.Setter;
12+
import org.slf4j.Logger;
13+
import org.slf4j.LoggerFactory;
14+
15+
import java.beans.Encoder;
16+
import java.io.IOException;
17+
import java.net.HttpURLConnection;
18+
import java.net.MalformedURLException;
19+
import java.net.SocketException;
20+
import java.net.URISyntaxException;
21+
22+
//base class for all sources that support managed identity
23+
abstract class AbstractManagedIdentitySource {
24+
25+
protected static final String TIMEOUT_ERROR = "[Managed Identity] Authentication unavailable. The request to the managed identity endpoint timed out.";
26+
private static final Logger LOG = LoggerFactory.getLogger(AbstractManagedIdentitySource.class);
27+
private static final String MANAGED_IDENTITY_NO_RESPONSE_RECEIVED = "[Managed Identity] Authentication unavailable. No response received from the managed identity endpoint.";
28+
29+
protected final ManagedIdentityRequest managedIdentityRequest;
30+
private ServiceBundle serviceBundle;
31+
private ManagedIdentitySourceType managedIdentitySourceType;
32+
33+
@Getter
34+
@Setter
35+
private boolean isUserAssignedManagedIdentity;
36+
@Getter
37+
@Setter
38+
private String managedIdentityUserAssignedClientId;
39+
@Getter
40+
@Setter
41+
private String managedIdentityUserAssignedResourceId;
42+
43+
public AbstractManagedIdentitySource(MsalRequest msalRequest, ServiceBundle serviceBundle,
44+
ManagedIdentitySourceType sourceType) {
45+
this.managedIdentityRequest = (ManagedIdentityRequest) msalRequest;
46+
this.managedIdentitySourceType = sourceType;
47+
this.serviceBundle = serviceBundle;
48+
}
49+
50+
public ManagedIdentityResponse getManagedIdentityResponse(
51+
ManagedIdentityParameters parameters) {
52+
53+
createManagedIdentityRequest(parameters.resource);
54+
IHttpResponse response;
55+
56+
try {
57+
HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, managedIdentityRequest.computeURI().toString(), managedIdentityRequest.headers);
58+
response = HttpHelper.executeHttpRequest(httpRequest, managedIdentityRequest.requestContext(), serviceBundle);
59+
} catch (URISyntaxException e) {
60+
throw new RuntimeException(e);
61+
} catch (MsalClientException e) {
62+
if (e.getCause() instanceof SocketException) {
63+
throw new MsalManagedIdentityException(MsalError.MANAGED_IDENTITY_UNREACHABLE_NETWORK, e.getMessage(), managedIdentitySourceType);
64+
}
65+
66+
throw e;
67+
}
68+
69+
return handleResponse(parameters, response);
70+
}
71+
72+
public ManagedIdentityResponse handleResponse(
73+
ManagedIdentityParameters parameters,
74+
IHttpResponse response) {
75+
76+
String message;
77+
78+
try {
79+
if (response.statusCode() == HttpURLConnection.HTTP_OK) {
80+
LOG.info("[Managed Identity] Successful response received.");
81+
return getSuccessfulResponse(response);
82+
} else {
83+
message = getMessageFromErrorResponse(response);
84+
LOG.error(
85+
String.format("[Managed Identity] request failed, HttpStatusCode: %s Error message: %s",
86+
response.statusCode(), message));
87+
throw new MsalManagedIdentityException(AuthenticationErrorCode.MANAGED_IDENTITY_REQUEST_FAILED, message, managedIdentitySourceType);
88+
}
89+
} catch (Exception e) {
90+
if (!(e instanceof MsalServiceException)) {
91+
LOG.error(
92+
String.format("[Managed Identity] Exception: %s Http status code: %s", e.getMessage(),
93+
response != null ? response.statusCode() : ""));
94+
message = MsalErrorMessage.MANAGED_IDENTITY_UNEXPECTED_RESPONSE;
95+
} else {
96+
throw e;
97+
}
98+
throw new MsalManagedIdentityException(AuthenticationErrorCode.MANAGED_IDENTITY_REQUEST_FAILED, message, managedIdentitySourceType);
99+
}
100+
}
101+
102+
public abstract void createManagedIdentityRequest(String resource);
103+
104+
protected ManagedIdentityResponse getSuccessfulResponse(IHttpResponse response) {
105+
106+
ManagedIdentityResponse managedIdentityResponse = JsonHelper
107+
.convertJsonToObject(response.body(), ManagedIdentityResponse.class);
108+
109+
if (managedIdentityResponse == null || managedIdentityResponse.getAccessToken() == null
110+
|| managedIdentityResponse.getAccessToken().isEmpty() || managedIdentityResponse.getExpiresOn() == null
111+
|| managedIdentityResponse.getExpiresOn().isEmpty()) {
112+
LOG.error("[Managed Identity] Response is either null or insufficient for authentication.");
113+
throw new MsalManagedIdentityException(MsalError.MANAGED_IDENTITY_REQUEST_FAILED, MsalErrorMessage.MANAGED_IDENTITY_UNEXPECTED_RESPONSE, managedIdentitySourceType);
114+
}
115+
116+
return managedIdentityResponse;
117+
}
118+
119+
protected String getMessageFromErrorResponse(IHttpResponse response) {
120+
ManagedIdentityErrorResponse managedIdentityErrorResponse =
121+
JsonHelper.convertJsonToObject(response.body(), ManagedIdentityErrorResponse.class);
122+
123+
if (managedIdentityErrorResponse == null) {
124+
return MANAGED_IDENTITY_NO_RESPONSE_RECEIVED;
125+
}
126+
127+
if (managedIdentityErrorResponse.getMessage() != null && !managedIdentityErrorResponse.getMessage().isEmpty()) {
128+
return String.format("[Managed Identity] Error Message: %s Managed Identity Correlation ID: %s Use this Correlation ID for further investigation.",
129+
managedIdentityErrorResponse.getMessage(), managedIdentityErrorResponse.getCorrelationId());
130+
}
131+
132+
return String.format("[Managed Identity] Error Code: %s Error Message: %s",
133+
managedIdentityErrorResponse.getError(), managedIdentityErrorResponse.getErrorDescription());
134+
}
135+
136+
protected static IEnvironmentVariables getEnvironmentVariables(ManagedIdentityParameters parameters) {
137+
return parameters.environmentVariables == null ? new EnvironmentVariables() : parameters.environmentVariables;
138+
}
139+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
import java.util.HashSet;
10+
import java.util.Set;
11+
12+
class AcquireTokenByManagedIdentitySupplier extends AuthenticationResultSupplier {
13+
14+
private static final Logger LOG = LoggerFactory.getLogger(AcquireTokenByManagedIdentitySupplier.class);
15+
16+
private ManagedIdentityParameters managedIdentityParameters;
17+
18+
AcquireTokenByManagedIdentitySupplier(ManagedIdentityApplication managedIdentityApplication, MsalRequest msalRequest) {
19+
super(managedIdentityApplication, msalRequest);
20+
this.managedIdentityParameters = (ManagedIdentityParameters) msalRequest.requestContext().apiParameters();
21+
}
22+
23+
@Override
24+
AuthenticationResult execute() throws Exception {
25+
26+
if (StringHelper.isNullOrBlank(managedIdentityParameters.resource)) {
27+
throw new MsalClientException(
28+
MsalError.RESOURCE_REQUIRED_MANAGED_IDENTITY,
29+
MsalErrorMessage.SCOPES_REQUIRED);
30+
}
31+
32+
TokenRequestExecutor tokenRequestExecutor = new TokenRequestExecutor(
33+
clientApplication.authenticationAuthority,
34+
msalRequest,
35+
clientApplication.getServiceBundle()
36+
);
37+
38+
if (!managedIdentityParameters.forceRefresh) {
39+
LOG.debug("ForceRefresh set to false. Attempting cache lookup");
40+
41+
try {
42+
Set<String> scopes = new HashSet<>();
43+
scopes.add(this.managedIdentityParameters.resource);
44+
SilentParameters parameters = SilentParameters
45+
.builder(scopes)
46+
.build();
47+
48+
RequestContext context = new RequestContext(
49+
this.clientApplication,
50+
PublicApi.ACQUIRE_TOKEN_SILENTLY,
51+
parameters);
52+
53+
SilentRequest silentRequest = new SilentRequest(
54+
parameters,
55+
this.clientApplication,
56+
context,
57+
null);
58+
59+
AcquireTokenSilentSupplier supplier = new AcquireTokenSilentSupplier(
60+
this.clientApplication,
61+
silentRequest);
62+
63+
return supplier.execute();
64+
} catch (MsalClientException ex) {
65+
if (ex.errorCode().equals(AuthenticationErrorCode.CACHE_MISS)) {
66+
LOG.debug(String.format("Cache lookup failed: %s", ex.getMessage()));
67+
return fetchNewAccessTokenAndSaveToCache(tokenRequestExecutor, clientApplication.authenticationAuthority.host);
68+
} else {
69+
LOG.error("Error occurred while cache lookup. " + ex.getMessage());
70+
throw ex;
71+
}
72+
}
73+
}
74+
75+
LOG.info("Skipped looking for an Access Token in the cache because forceRefresh or Claims were set. ");
76+
return fetchNewAccessTokenAndSaveToCache(tokenRequestExecutor, clientApplication.authenticationAuthority.host);
77+
}
78+
79+
private AuthenticationResult fetchNewAccessTokenAndSaveToCache(TokenRequestExecutor tokenRequestExecutor, String host) throws Exception {
80+
81+
ManagedIdentityClient managedIdentityClient = new ManagedIdentityClient(msalRequest, tokenRequestExecutor.getServiceBundle());
82+
83+
ManagedIdentityResponse managedIdentityResponse = managedIdentityClient
84+
.getManagedIdentityResponse(managedIdentityParameters);
85+
86+
AuthenticationResult authenticationResult = createFromManagedIdentityResponse(managedIdentityResponse);
87+
clientApplication.tokenCache.saveTokens(tokenRequestExecutor, authenticationResult, clientApplication.authenticationAuthority.host);
88+
return authenticationResult;
89+
}
90+
91+
private AuthenticationResult createFromManagedIdentityResponse(ManagedIdentityResponse managedIdentityResponse) {
92+
long expiresOn = Long.valueOf(managedIdentityResponse.expiresOn);
93+
long refreshOn = expiresOn > 2 * 3600 ? (expiresOn / 2) : 0L;
94+
95+
return AuthenticationResult.builder()
96+
.accessToken(managedIdentityResponse.getAccessToken())
97+
.scopes(managedIdentityParameters.getResource())
98+
.expiresOn(expiresOn)
99+
.extExpiresOn(0)
100+
.refreshOn(refreshOn)
101+
.build();
102+
}
103+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
import java.net.URI;
10+
import java.net.URISyntaxException;
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
14+
class AppServiceManagedIdentitySource extends AbstractManagedIdentitySource{
15+
16+
private static final Logger LOG = LoggerFactory.getLogger(AppServiceManagedIdentitySource.class);
17+
18+
// MSI Constants. Docs for MSI are available here https://docs.microsoft.com/azure/app-service/overview-managed-identity
19+
private static final String APP_SERVICE_MSI_API_VERSION = "2019-08-01";
20+
private static final String SECRET_HEADER_NAME = "X-IDENTITY-HEADER";
21+
private static URI endpointUri;
22+
23+
private URI endpoint;
24+
private String secret;
25+
26+
@Override
27+
public void createManagedIdentityRequest(String resource) {
28+
managedIdentityRequest.baseEndpoint = endpoint;
29+
managedIdentityRequest.method = HttpMethod.GET;
30+
31+
Map<String, String> headers = new HashMap<>();
32+
headers.put(SECRET_HEADER_NAME, secret);
33+
managedIdentityRequest.headers = headers;
34+
35+
Map<String, String> queryParameters = new HashMap<>();
36+
queryParameters.put("api-version", APP_SERVICE_MSI_API_VERSION );
37+
queryParameters.put("resource", resource);
38+
39+
if (!StringHelper.isNullOrBlank(getManagedIdentityUserAssignedClientId()))
40+
{
41+
LOG.info("[Managed Identity] Adding user assigned client id to the request.");
42+
queryParameters.put(Constants.MANAGED_IDENTITY_CLIENT_ID, getManagedIdentityUserAssignedClientId());
43+
}
44+
45+
if (!StringHelper.isNullOrBlank(getManagedIdentityUserAssignedResourceId()))
46+
{
47+
LOG.info("[Managed Identity] Adding user assigned resource id to the request.");
48+
queryParameters.put(Constants.MANAGED_IDENTITY_RESOURCE_ID, getManagedIdentityUserAssignedResourceId());
49+
}
50+
51+
managedIdentityRequest.queryParameters = queryParameters;
52+
}
53+
54+
private AppServiceManagedIdentitySource(MsalRequest msalRequest, ServiceBundle serviceBundle, URI endpoint, String secret)
55+
{
56+
super(msalRequest, serviceBundle, ManagedIdentitySourceType.AppService);
57+
this.endpoint = endpoint;
58+
this.secret = secret;
59+
}
60+
61+
protected static AbstractManagedIdentitySource create(MsalRequest msalRequest, ServiceBundle serviceBundle) {
62+
63+
IEnvironmentVariables environmentVariables = getEnvironmentVariables((ManagedIdentityParameters) msalRequest.requestContext().apiParameters());
64+
String msiSecret = environmentVariables.getEnvironmentVariable(Constants.IDENTITY_HEADER);
65+
String msiEndpoint = environmentVariables.getEnvironmentVariable(Constants.IDENTITY_ENDPOINT);
66+
67+
return validateEnvironmentVariables(msiEndpoint, msiSecret)
68+
? new AppServiceManagedIdentitySource(msalRequest, serviceBundle, endpointUri, msiSecret)
69+
: null;
70+
}
71+
72+
private static boolean validateEnvironmentVariables(String msiEndpoint, String secret)
73+
{
74+
endpointUri = null;
75+
76+
// if BOTH the env vars endpoint and secret values are null, this MSI provider is unavailable.
77+
if (StringHelper.isNullOrBlank(msiEndpoint) || StringHelper.isNullOrBlank(secret))
78+
{
79+
LOG.info("[Managed Identity] App service managed identity is unavailable.");
80+
return false;
81+
}
82+
83+
try
84+
{
85+
endpointUri = new URI(msiEndpoint);
86+
}
87+
catch (URISyntaxException ex)
88+
{
89+
throw new MsalManagedIdentityException(MsalError.INVALID_MANAGED_IDENTITY_ENDPOINT, String.format(
90+
MsalErrorMessage.MANAGED_IDENTITY_ENDPOINT_INVALID_URI_ERROR, "IDENTITY_ENDPOINT", msiEndpoint, "App Service"),
91+
ManagedIdentitySourceType.AppService);
92+
}
93+
94+
LOG.info("[Managed Identity] Environment variables validation passed for app service managed identity. Endpoint URI: {endpointUri}. Creating App Service managed identity.");
95+
return true;
96+
}
97+
98+
}

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,10 @@ public class AuthenticationErrorCode {
130130
* slow response, and this may be resolvable by increasing timeouts. For more details, see https://aka.ms/msal4j-http-client
131131
*/
132132
public final static String HTTP_TIMEOUT = "http_timeout";
133+
134+
/**
135+
* Indicates that a request to managed identity endpoint failed, see error message for detailed reason and correlation id.
136+
* For more information on managed identity see https://aka.ms/msal4j-managed-identity.
137+
*/
138+
public static final String MANAGED_IDENTITY_REQUEST_FAILED = "managed_identity_request_failed";
133139
}

0 commit comments

Comments
 (0)