diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/OnBehalfOfRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/OnBehalfOfRequest.cs index 2b19278547..9192c65dbe 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/OnBehalfOfRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/OnBehalfOfRequest.cs @@ -90,6 +90,13 @@ protected override async Task ExecuteAsync(CancellationTok private async Task FetchNewAccessTokenAsync(CancellationToken cancellationToken) { var msalTokenResponse = await SendTokenRequestAsync(GetBodyParameters(), cancellationToken).ConfigureAwait(false); + if (msalTokenResponse.ClientInfo is null && + AuthenticationRequestParameters.AuthorityInfo.AuthorityType != AuthorityType.Adfs) + { + var logger = AuthenticationRequestParameters.RequestContext.Logger; + logger.Info("This is an on behalf of request for a service principal as no client info returned in the token response."); + } + return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse).ConfigureAwait(false); } diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs index b9cae238b0..7ff90d3c48 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs @@ -190,7 +190,8 @@ protected async Task CacheTokenResponseAndCreateAuthentica if (!AuthenticationRequestParameters.IsClientCredentialRequest && AuthenticationRequestParameters.ApiId != ApiEvent.ApiIds.AcquireTokenByRefreshToken && - AuthenticationRequestParameters.AuthorityInfo.AuthorityType != AuthorityType.Adfs) + AuthenticationRequestParameters.AuthorityInfo.AuthorityType != AuthorityType.Adfs && + !(msalTokenResponse.ClientInfo is null)) { //client_info is not returned from client credential flows because there is no user present. fromServer = ClientInfo.CreateFromJson(msalTokenResponse.ClientInfo); diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/ConfidentialClientIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/ConfidentialClientIntegrationTests.cs index 56c5f6827d..562d9ff6ef 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/ConfidentialClientIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/ConfidentialClientIntegrationTests.cs @@ -21,6 +21,7 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Instance; using Microsoft.Identity.Client.Internal; +using Microsoft.Identity.Json.Utilities; using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Integration.Infrastructure; @@ -50,6 +51,11 @@ public class ConfidentialClientIntegrationTests private const string ArlingtonConfidentialClientIDOBO = "c0555d2d-02f2-4838-802e-3463422e571d"; private const string ArlingtonPublicClientIDOBO = "cb7faed4-b8c0-49ee-b421-f5ed16894c83"; private const string ArlingtonAuthority = "https://login.microsoftonline.us/45ff0c17-f8b5-489b-b7fd-2fedebbec0c4"; + //The following client ids are for applications that are within PPE + private const string OBOClientPpeClientID = "9793041b-9078-4942-b1d2-babdc472cc0c"; + private const string OBOServicePpeClientID = "c84e9c32-0bc9-4a73-af05-9efe9982a322"; + private const string OBOServiceDownStreamApiPpeClientID = "23d08a1e-1249-4f7c-b5a5-cb11f29b6923"; + private const string PPEAuthenticationAuthority = "https://login.windows-ppe.net/f686d426-8d16-42db-81b7-ab578e110ccd"; private const string PublicCloudHost = "https://login.microsoftonline.com/"; private const string ArlingtonCloudHost = "https://login.microsoftonline.us/"; @@ -693,6 +699,54 @@ public async Task ClientCreds_ClientAssertion_AAD_NoWilson_Async() } } + [TestMethod] + public async Task ClientCreds_ServicePrincipal_OBO_PPE_Async() + { + //An explination of the OBO for service principal scenario can be found here https://aadwiki.windows-int.net/index.php?title=App_OBO_aka._Service_Principal_OBO + X509Certificate2 cert = GetCertificate(); + IReadOnlyList scopes = new List() { OBOServicePpeClientID + "/.default" }; + IReadOnlyList scopes2 = new List() { OBOServiceDownStreamApiPpeClientID + "/.default" }; + + var confidentialSPApp = ConfidentialClientApplicationBuilder + .Create(OBOClientPpeClientID) + .WithAuthority(PPEAuthenticationAuthority) + .WithCertificate(cert) + .WithTestLogging() + .Build(); + + var authenticationResult = await confidentialSPApp.AcquireTokenForClient(scopes).ExecuteAsync().ConfigureAwait(false); + + string appToken = authenticationResult.AccessToken; + var userAssertion = new UserAssertion(appToken); + string atHash = userAssertion.AssertionHash; + + var _confidentialApp = ConfidentialClientApplicationBuilder + .Create(OBOServicePpeClientID) + .WithCertificate(cert) + .Build(); + + var userCacheRecorder = _confidentialApp.UserTokenCache.RecordAccess(); + + authenticationResult = await _confidentialApp.AcquireTokenOnBehalfOf(scopes2, userAssertion) + .WithAuthority(PPEAuthenticationAuthority) + .ExecuteAsync().ConfigureAwait(false); + + Assert.IsNotNull(authenticationResult); + Assert.IsNotNull(authenticationResult.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, authenticationResult.AuthenticationResultMetadata.TokenSource); + + authenticationResult = await _confidentialApp.AcquireTokenOnBehalfOf(scopes2, userAssertion) + .WithAuthority(PPEAuthenticationAuthority) + .ExecuteAsync().ConfigureAwait(false); + + Assert.IsNotNull(authenticationResult); + Assert.IsNotNull(authenticationResult.AccessToken); + Assert.IsTrue(!userCacheRecorder.LastAfterAccessNotificationArgs.IsApplicationCache); + Assert.IsTrue(userCacheRecorder.LastAfterAccessNotificationArgs.HasTokens); + Assert.AreEqual(atHash, userCacheRecorder.LastAfterAccessNotificationArgs.SuggestedCacheKey); + Assert.AreEqual(TokenSource.Cache, authenticationResult.AuthenticationResultMetadata.TokenSource); + } + private string GetSignedClientAssertionDirectly( string issuer, // client ID string audience, // ${authority}/oauth2/v2.0/token for AAD or ${authority}/oauth2/token for ADFS