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

Update the framename to reflect authority and scopes #1267

Merged
merged 8 commits into from
Apr 24, 2020
Merged

Conversation

sameerag
Copy link
Member

@sameerag sameerag commented Feb 6, 2020

This PR fixes #1225 by making the frame name unique for scopes+authority when making silent calls

@coveralls
Copy link

coveralls commented Feb 6, 2020

Coverage Status

Coverage increased (+0.01%) to 79.503% when pulling 7277a7f on unique-framename into 9274fac on dev.

@keystroke
Copy link

@jasonnutter @sameerag thanks for looking into this! I wonder, should the frame name / identifier also include a moniker for "silent" or "popup" so otherwise-duplicate token requests don't collide for these different operations? Or silent vs. popup is disambiguated via some other mechanism?

@keystroke
Copy link

@sameerag I attempted to hack your changes into the MSAL source (code shown below) using the non-minified version.

I see that the target function is only invoked a single time during the concurrent requests.

/**
 * @hidden
 * Acquires access token using a hidden iframe.
 * @ignore
 */
UserAgentApplication.prototype.renewToken = function (scopes, resolve, reject, account, serverAuthenticationRequest) {
    var scope = scopes.join(" ").toLowerCase();
    this.logger.verbose("renewToken is called for scope:" + scope);
    var frameName = "msalRenewFrame" + scope + serverAuthenticationRequest.authority;
    console.log("Using frame name: " + frameName, serverAuthenticationRequest);
    var frameHandle = WindowUtils_1.WindowUtils.addHiddenIFrame(frameName, this.logger);
    this.updateCacheEntries(serverAuthenticationRequest, account);
    this.logger.verbose("Renew token Expected state: " + serverAuthenticationRequest.state);
    // Build urlNavigate with "prompt=none" and navigate to URL in hidden iFrame
    var urlNavigate = UrlUtils_1.UrlUtils.urlRemoveQueryStringParameter(UrlUtils_1.UrlUtils.createNavigateUrl(serverAuthenticationRequest), Constants_1.Constants.prompt) + Constants_1.Constants.prompt_none + Constants_1.Constants.response_mode_fragment;
    window.renewStates.push(serverAuthenticationRequest.state);
    window.requestType = Constants_1.Constants.renewToken;
    this.registerCallback(serverAuthenticationRequest.state, scope, resolve, reject);
    this.logger.infoPii("Navigate to:" + urlNavigate);
    frameHandle.src = "about:blank";
    this.loadIframeTimeout(urlNavigate, frameName, scope).catch(function (error) { return reject(error); });
};

I made the similair change to the other function ("renewIdToken") as shown above. When I run my sample again using the concurrent discovery flow, I see that above function is only called once, despite calling acquire token silent 10 times with different authority values:

image

However, when I included my "disambiguation" code from the sample to force unique scope strings for each token request, I do see that this function is called for each set of scopes:

function beginTenantDiscoveryConcurrent(loginToken) {
    console.log('Beginning tenant discovery concurrent:', loginToken);
    for (let i = 1; i <= 9; i++) {
        let tenant = 'azurestackci0' + i + '.onmicrosoft.com';
        let disambiguation = '';
        for (let j = 0; j < i; j++) {
            disambiguation += ' openid'
        }
        let scopes = ['openid', disambiguation];
        msal.acquireTokenSilent({
            forceRefresh: true,
            scopes: scopes, //['openid'],
            authority: 'https://login.microsoftonline.com/' + tenant
        }).then(function (token) {
            console.log('Tenant ' + tenant + '(fromCache: ' + token.fromCache + '):', getAccessTokenClaims(token).tid);
        }).catch(function (error) {
            console.log('Tenant ' + tenant + ':', error.message);
        });
    }
}

image

If I dig into the code of acquire token silent a bit more, I see this code which looks like it is caching windows requests only by scope here, and not including authority (or popup vs silent related to what I was discussing with @jasonnutter):

image

Please note I'm not entirely certain in above investigation, but it does seem to be the issue. Please let me know if I've missed something.

Thanks again for looking into this!

@keystroke
Copy link

@sameerag I further hacked the code to replace all references of:

window.activeRenewals[scope]

to:

window.activeRenewals[scope + authority]

I can verify that this change works with my specific sample, however there are some further design issues.

First, the signature and context of this function does not have any authority information provided to it, so there was no way for me to even "hack" the code to include the correct authority; instead the function signature would need to be modified to have authority context:

/**
 * @hidden
 * Used to add the developer requested callback to the array of callbacks for the specified scopes. The updated array is stored on the window object
 * @param {string} expectedState - Unique state identifier (guid).
 * @param {string} scope - Developer requested permissions. Not all scopes are guaranteed to be included in the access token returned.
 * @param {Function} resolve - The resolve function of the promise object.
 * @param {Function} reject - The reject function of the promise object.
 * @ignore
 */
UserAgentApplication.prototype.registerCallback = function (expectedState, scope, resolve, reject) {
    var _this = this;
    // track active renewals
    window.activeRenewals[scope] = expectedState;

Second, I found this function that did have enough context for me to hack the code to include an authority reference, though of course a better design might always pass scope and authority around everywhere. Honestly, scopes are meaningless without the associated authority in general, so probably through the entire code base you want to associate the two concepts.

/**
 * @hidden
 * Calling _loadFrame but with a timeout to signal failure in loadframeStatus. Callbacks are left.
 * registered when network errors occur and subsequent token requests for same resource are registered to the pending request.
 * @ignore
 */
UserAgentApplication.prototype.loadIframeTimeout = function (urlNavigate, frameName, scope) {
    return tslib_1.__awaiter(this, void 0, Promise, function () {
        var expectedState, iframe, hash, error_2;
        return tslib_1.__generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    let hackedAuthority = frameName.substr(frameName.indexOf('https:'));
                    expectedState = window.activeRenewals[scope + hackedAuthority];

Those were the only references in the code I found to "window.activeRenewals" so it is promising. In fact, the problematic method above (registerCallback) is called 4 times in the code base, and in each call, it has a handle to "serverAuthenticationRequest" which has the authority parameter! So instead of just passing the expected state to that function, it could pass the authority as well, though I think for a cleaner code you might consider just passing the entire "serverAuthenticationRequest" object to this function?

IDK, I'll let you folks take a look. But pretty sure these are the necessary fixes.

Copy link

@keystroke keystroke left a comment

Choose a reason for hiding this comment

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

I have submitted comments in the conversation tab after testing and investigating code changes. Thanks!

@sameerag
Copy link
Member Author

sameerag commented Feb 8, 2020

@keystroke you could be right as our renewal code blocks concurrency if the request is made for the same scopes, and that could be executed before creating an iframe. I will check this more closely and make changes accordingly.

@jasonnutter
Copy link
Contributor

If just changing the frame name to include authority works and provides value on its own, I would like to scope this PR to just include that change. Any further improvements can be made incrementally.

@sameerag
Copy link
Member Author

I am scoping this PR to name change only and will bring up a new PR to address the concurrency check accounting for authority. @jasonnutter and @keystroke, can you help with the reviews, I have addressed the feedback.

@sameerag sameerag self-assigned this Feb 12, 2020
Copy link
Contributor

@jasonnutter jasonnutter left a comment

Choose a reason for hiding this comment

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

Looks good, thanks for the test!

@keystroke
Copy link

@jasonnutter @sameerag I understand you are trying to scope this PR to the first half of a potential solution, however I'm not sure of the value of that, can you take a look at the changes in #1279 to consider a more complete / direct approach?

Copy link
Contributor

@jmckennon jmckennon left a comment

Choose a reason for hiding this comment

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

Looks good to me!

Copy link

@keystroke keystroke left a comment

Choose a reason for hiding this comment

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

This change does not fix the issue without additional changes, and those additional changes may "invalidate" this change, please see PR #1279 to consider additional modifications to some function signatures, including the introduction of a "request fingerprint" that ought to be used in a few places where only scopes are passed or "frame name" is passed.

@jasonnutter
Copy link
Contributor

jasonnutter commented Feb 20, 2020

@keystroke Gotya. @sameerag and I will chat and follow up. Thanks!

@jasonnutter jasonnutter added this to the msal@1.3.0 - Release milestone Apr 20, 2020
@jasonnutter
Copy link
Contributor

Updates look good!

@jasonnutter
Copy link
Contributor

@sameerag Is this ready to be merged?

@jasonnutter jasonnutter mentioned this pull request Apr 24, 2020
4 tasks
@sameerag sameerag merged commit ac663a8 into dev Apr 24, 2020
@sameerag sameerag deleted the unique-framename branch May 4, 2020 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants