-
Notifications
You must be signed in to change notification settings - Fork 218
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
Conversation
@@ -88,5 +88,16 @@ public interface ITokenAcquisition | |||
void ReplyForbiddenWithWwwAuthenticateHeader( | |||
IEnumerable<string> scopes, | |||
MsalUiRequiredException msalSeviceException); | |||
|
|||
/// <summary> |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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
f85a5f5
to
3ecc9f9
Compare
AcquireTokenForApp(scopes)
to ITokenAcquisition
AcquireTokenForApp(scopes)
to ITokenAcquisition
|
||
if (microsoftIdentityOptions.IsB2C) | ||
if (string.IsNullOrEmpty(applicationOptions.Instance) && | ||
!string.IsNullOrEmpty(microsoftIdentityOptions.Instance)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IConfidentialClientApplication app = null; | ||
|
||
if (microsoftIdentityOptions.IsB2C) | ||
if (string.IsNullOrEmpty(applicationOptions.Instance) && |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.Build(); | ||
if (!applicationOptions.Instance.EndsWith("/")) | ||
applicationOptions.Instance += "/"; | ||
instance = applicationOptions.Instance; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.WithB2CAuthority(authority) | ||
.Build(); | ||
} | ||
else |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
|
||
// Initialize token cache providers | ||
_tokenCacheProvider?.InitializeAsync(app.AppTokenCache); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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("/")) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async Task result() => | ||
await _tokenAcquisition.AcquireTokenForAppAsync(TestConstants.s_scopesForUser).ConfigureAwait(false); | ||
|
||
Exception ex = await Assert.ThrowsAsync<MsalServiceException>(result); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this 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.
addressed your comments and/or made issues for pending items for future PRs. In reply to: 366302244 [](ancestors = 366302244) |
There was a problem hiding this 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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AcquireTokenForApp(scopes)
to ITokenAcquisition
AcquireTokenForApp(scopes)
to ITokenAcquisition
- add await on async methods - use nsubstitute for httpcontext mock
1f9a5dc
to
0b4ee95
Compare
@@ -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)); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
|
||
// Initialize token cache providers | ||
await _tokenCacheProvider.InitializeAsync(app.AppTokenCache).ConfigureAwait(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 #Resolved
.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. "); |
There was a problem hiding this comment.
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() | |||
{ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
PR also includes integration tests and lab infrastructure tests
#26