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 AcquireTokenForApp(scopes) to ITokenAcquisition #39

Merged
merged 7 commits into from
Mar 1, 2020

Conversation

jennyf19
Copy link
Collaborator

@jennyf19 jennyf19 commented Feb 27, 2020

PR also includes integration tests and lab infrastructure tests

  • first commit has the API changes, @jmprieur please review the XML, as well as the integration test set-up
  • second commit has the lab infrastructure added. tests will probably fail unless ran locally.

#26

@@ -88,5 +88,16 @@ public interface ITokenAcquisition
void ReplyForbiddenWithWwwAuthenticateHeader(
IEnumerable<string> scopes,
MsalUiRequiredException msalSeviceException);

/// <summary>
Copy link
Collaborator Author

@jennyf19 jennyf19 Feb 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmprieur no idea what you want said here. please review. :) #Resolved

@@ -238,6 +237,32 @@ public class TokenAcquisition : ITokenAcquisition
return accessToken;
}

/// <summary>
Copy link
Collaborator Author

@jennyf19 jennyf19 Feb 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmprieur please review comment #Resolved


namespace Microsoft.Identity.Web.Test.Common.TestHelpers
{
public class InMemoryTokenCache
Copy link
Collaborator Author

@jennyf19 jennyf19 Feb 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is for the lab testing #Resolved

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
Copy link
Collaborator Author

@jennyf19 jennyf19 Feb 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all the stuff in Microsoft.Identity.Web.Test.LabInfrastructure is copied over from the project in MSAL.NET. only a few small things were changed (mainly visual), but overall, code is the same. #Resolved

@jennyf19 jennyf19 changed the title Add AcquireTokenForApp(scopes) to ITokenAcquisition [WIP] Add AcquireTokenForApp(scopes) to ITokenAcquisition Feb 28, 2020

if (microsoftIdentityOptions.IsB2C)
if (string.IsNullOrEmpty(applicationOptions.Instance) &&
!string.IsNullOrEmpty(microsoftIdentityOptions.Instance))
Copy link
Collaborator Author

@jennyf19 jennyf19 Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be investigated. I found that the applicationOptions did not inherit from the microsoftIdentityOptions in this case, so I had to add values for both in the integration test. Issue tracked here: #37 #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate PR and by fixing the test set-up, i no longer have the issue, but we should still look into merging the class. will update the issue.


In reply to: 385444897 [](ancestors = 385444897)


app = ConfidentialClientApplicationBuilder
.CreateWithApplicationOptions(applicationOptions)
//.WithRedirectUri(currentUri)
Copy link
Collaborator Author

@jennyf19 jennyf19 Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

//.WithRedirectUri(currentUri) [](start = 24, length = 30)

This is another issue. As we calculate the redirectUri here and then use it, if it's not registered, we fail with a null ref. Issue tracked here: #38 #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

working around this for now by creating the httpcontext, but needs to be looked into (hence the issue)


In reply to: 385444973 [](ancestors = 385444973)


// Use MSAL to get the right token to call the API
var application = GetOrBuildConfidentialClientApplication();
string accessToken;
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: define and assign accessToken on the same line. #Resolved

@@ -303,6 +328,7 @@ private IConfidentialClientApplication GetOrBuildConfidentialClientApplication()
/// <returns></returns>
private IConfidentialClientApplication BuildConfidentialClientApplication()
{
string instance;
var request = CurrentHttpContext.Request;
var microsoftIdentityOptions = _microsoftIdentityOptions;
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these variables needed? Why not just work with the class memebers (_XXX) #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed throughout the class


In reply to: 385591146 [](ancestors = 385591146)

IConfidentialClientApplication app = null;

if (microsoftIdentityOptions.IsB2C)
if (string.IsNullOrEmpty(applicationOptions.Instance) &&
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have the context around what applicationOptions and microsoftIdentityOptions are, but it looks to me that it violates the Single Responsability Principle. This class knows too much about options - this is a code smell that you are missing an object whose responsability is to know how to combine these options. #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree. will do this in a separate PR


In reply to: 385592741 [](ancestors = 385592741)

.Build();
if (!applicationOptions.Instance.EndsWith("/"))
applicationOptions.Instance += "/";
instance = applicationOptions.Instance;
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If both applcationOptions and microsoft Options are defined, your instance will be NULL, probably leading to a null ref. #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n/a


In reply to: 385593451 [](ancestors = 385593451)

.WithB2CAuthority(authority)
.Build();
}
else
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about ADFS? ADFS does not have tenant ID #WontFix

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i made a separate issue for this.


In reply to: 385593891 [](ancestors = 385593891)

}

// Initialize token cache providers
_tokenCacheProvider?.InitializeAsync(app.AppTokenCache);
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't you getting warnings about these lines? As a general rule of thumb, you should always (well 99% of time) await async operations. Otherwise you have a race condition here - you return an app and can use it to fetch tokens, but the token cache provider might not have finished initialzing.

Also, because you don't await, you will ignore possible exceptions in the InitializeAsync. #Resolved

Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will of course mean that this private method also becomes Async. #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wasn't getting warnings but have fixed all of these. good catch.


In reply to: 385595862 [](ancestors = 385595862)

@@ -446,6 +492,19 @@ private IConfidentialClientApplication BuildConfidentialClientApplication()
}
}

private async Task<string> GetAccessTokenFromApplicationCacheAsync(
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method name is misleading - in case of a cache miss this goes to Evo. nit: I don't think it's even needed, it's just one line of code. #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have removed. i was hoping to isolate stuff to test, but doesn't make sense at this point.


In reply to: 385597157 [](ancestors = 385597157)


namespace Microsoft.Identity.Web.Test.Common.Mocks
{
public class MockHttpContextAccessor : IHttpContextAccessor
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use NSubstitute to create mocks on the fly? You will have better control over them. #Resolved

{
public class MsalTestTokenCacheProvider : MsalAbstractTokenCacheProvider
{
public IMemoryCache MemoryCache { get; set; }
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review visibility of all these members. I don't think a setter is needed. #Resolved

}

// Use MSAL to get the right token to call the API
var application = GetOrBuildConfidentialClientApplication();
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a class member named application. Please rename this class member to _application to avoid confusion. #Resolved

private string _ccaSecret;
private readonly ITestOutputHelper _output;

public AcquireTokenForAppIntegrationTests(ITestOutputHelper output) //test set-up
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a look at NUnit's [SetUp] and [OneTimeSetup] attributes. Some of the setup should be performed once, and some of it for each test:

AcquireTokenForAppIntegrationTests -> once - I guess ctor is ok, but try to use [OneTimeSetup] as this is the recommended pattern. https://github.com/nunit/docs/wiki/SetUp-and-TearDown
InitializeTokenAcquisitionObjects -> once per test ([SetUp]) #WontFix

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i did look at that, but since we are already using xunit, i just stayed with their recommendations. we can change things over though.


In reply to: 385617575 [](ancestors = 385617575)

Copy link
Contributor

@pmaytak pmaytak Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XUnit allows the same behavior. I'll take care of it. #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @pmaytak


In reply to: 385919792 [](ancestors = 385919792)

Copy link
Member

@bgavrilMS bgavrilMS Feb 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, appologies, I mistakenly thought you were using NUnit instead of XUnit. #Resolved

return app;
try
{
if (microsoftIdentityOptions.IsB2C)
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This piece of logic is not tested. #WontFix

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are missing a lot of tests. they will come in future PRs.


In reply to: 385618752 [](ancestors = 385618752)

.WithRedirectUri(currentUri)
.WithB2CAuthority(authority)
.Build();
if (!microsoftIdentityOptions.Instance.EndsWith("/"))
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This piece of logic is not tested. #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is removed. not applicable anymore


In reply to: 385618845 [](ancestors = 385618845)

async Task result() =>
await _tokenAcquisition.AcquireTokenForAppAsync(TestConstants.s_scopesForUser).ConfigureAwait(false);

Exception ex = await Assert.ThrowsAsync<MsalServiceException>(result);
Copy link
Member

@bgavrilMS bgavrilMS Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can assert the error code as well if you cast directly to the type of exception:

MsalServiceExceptionex = await Assert.ThrowsAsync<MsalServiceException>(result); #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes..thanks.


In reply to: 385619799 [](ancestors = 385619799)

Copy link
Member

@bgavrilMS bgavrilMS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks good, main reason for requesting changes is not awaiting on InitializeAsync .
Could would be much cleaner and easier to test if another object would be responsible for merging microsoftOptions and appOptions.

@jennyf19
Copy link
Collaborator Author

addressed your comments and/or made issues for pending items for future PRs.


In reply to: 366302244 [](ancestors = 366302244)

Copy link
Collaborator

@jmprieur jmprieur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.
I've asked a couple of questions and provided a suggestion

@@ -88,5 +88,16 @@ public interface ITokenAcquisition
void ReplyForbiddenWithWwwAuthenticateHeader(
IEnumerable<string> scopes,
MsalUiRequiredException msalSeviceException);

/// <summary>
/// Acquires a token from the authority configured in the app, for the confidential client itself (in the name of no user)
Copy link
Collaborator

@jmprieur jmprieur Feb 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "in the name of no user" clear? should we write "not on behalf of a user" ? #Resolved

@@ -238,6 +237,34 @@ public class TokenAcquisition : ITokenAcquisition
return accessToken;
}

/// <summary>
/// Acquires a token from the authority configured in the app, for the confidential client itself (in the name of no user)
Copy link
Collaborator

@jmprieur jmprieur Feb 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question as for the interface. #Resolved

{
public class MsalTestTokenCacheProvider : MsalAbstractTokenCacheProvider
{
public IMemoryCache MemoryCache { get; }
Copy link
Collaborator

@jmprieur jmprieur Feb 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be one IMemoryCache per cache key?
Meaning should MemoryCache be a Dictionary<string, IMemoryCache>() ? #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

synced off line.


In reply to: 385982535 [](ancestors = 385982535)

@jennyf19 jennyf19 changed the title [WIP] Add AcquireTokenForApp(scopes) to ITokenAcquisition Add AcquireTokenForApp(scopes) to ITokenAcquisition Feb 29, 2020
- Add AcquireTokenForApp(scopes)
- Add integration tests
- add await on async methods
- use nsubstitute for httpcontext mock
@@ -29,7 +29,7 @@ public static ClientInfo CreateFromJson(string clientInfo)
{
return DeserializeFromJson<ClientInfo>(Base64UrlHelpers.DecodeToBytes(clientInfo));
}
catch (Exception exc)
catch (Exception)
{
throw new ArgumentException($"Failed to parse the returned client info. ", nameof(clientInfo));
Copy link
Member

@bgavrilMS bgavrilMS Feb 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this isn't your code, but ArgumentException is really not the appropriate exception here. ArgumentException tells the developer that they did smth wrong :). Parsing client_info, a piece of JSON returned by EVO, has nothing to do with this.

Personally I would just let the JSON exception flow out of the deserialize (i.e. remove the try / catch).

In general, it is a good practice to have your own dedicated exceptions for this project, similar to how MSAL has. I suggest that you review the exception strategy before releasing, as changing exceptions is breaking. CC @jmprieur #Resolved

Copy link
Collaborator Author

@jennyf19 jennyf19 Mar 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will discuss. thanks for putting this on our radar. i made the changes in this class that you recommend, but we will discuss how to handle the other exceptions.


In reply to: 386016125 [](ancestors = 386016125)

using (var stream = new MemoryStream(jsonByteArray))
using (var reader = new StreamReader(stream, Encoding.UTF8))
return (T)JsonSerializer.Create().Deserialize(reader, typeof(T));
using var stream = new MemoryStream(jsonByteArray);
Copy link
Member

@bgavrilMS bgavrilMS Feb 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fancy new C# 8 feature you're using there :) #Resolved

.WithRedirectUri(currentUri)
.WithAuthority(authority)
.Build();
_logger.LogInformation(ex.Message);
Copy link
Member

@bgavrilMS bgavrilMS Feb 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_logger has a LogError with an exception argument. Seems more appropriate. #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed. thanks.


In reply to: 386016331 [](ancestors = 386016331)

}

// Initialize token cache providers
await _tokenCacheProvider.InitializeAsync(app.AppTokenCache).ConfigureAwait(false);
Copy link
Member

@bgavrilMS bgavrilMS Feb 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 #Resolved

@jennyf19 jennyf19 merged commit 277cdd1 into master Mar 1, 2020
@jennyf19 jennyf19 deleted the jennyf/ATForApp branch March 1, 2020 01:37
.AcquireTokenByAuthorizationCode(scopes.Except(_scopesRequestedByMsal), context.ProtocolMessage.Code)
.ExecuteAsync()
.ConfigureAwait(false);
context.HandleCodeRedemption(null, result.IdToken);
}
catch (MsalException ex)
{
_logger.LogInformation(ex.Message);
_logger.LogInformation(ex, "Exception occured while adding an account to the cache from the auth code. ");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should discuss putting all error strings into a separate class.

@@ -287,61 +314,67 @@ public async Task RemoveAccountAsync(RedirectContext context)
/// </summary>
/// <param name="claimsPrincipal"></param>
/// <returns></returns>
private IConfidentialClientApplication GetOrBuildConfidentialClientApplication()
private async Task<IConfidentialClientApplication> GetOrBuildConfidentialClientApplicationAsync()
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should discuss renaming this method (and any ones like it). Having 'or' or 'and' in the method name suggests that a method has more than one responsibility. In this case I would probably rename it to just 'GetConfidential...' since the caller only cares about getting an app instance; whether it's built or not is internal/abstracted detail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetOrCreate is a pretty standard method name. In any case, this is private method, so it does not matter that much. IMO the more explicit the better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants