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

[Identity] Improvements to MSAL support #11407

Merged
merged 12 commits into from
Sep 29, 2020
Merged

Conversation

sophiajt
Copy link
Contributor

This is another batch of improvements on our MSAL interop. In this PR:

  • Will will do silent flow authentication if the user provides a cache and authentication record for lookup
  • The HTTP pipeline is configurable, and we'll adapt calls for the MSAL pipeline
  • We now parse out and use the port from the redirectUri

We'll continue improving, I just wanted to land updates more incrementally.

Copy link
Member

@xirzec xirzec left a comment

Choose a reason for hiding this comment

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

Left some thoughts / code cleanup suggestions

@@ -82,14 +82,14 @@
"@azure/core-http": "^1.1.6",
"@azure/core-tracing": "1.0.0-preview.9",
"@azure/logger": "^1.0.0",
"@azure/msal-node": "^1.0.0-alpha.5",
"@azure/msal-node": "^1.0.0-alpha.6",
Copy link
Member

Choose a reason for hiding this comment

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

should we pin this? or are they not making breaking changes? ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They're still making changes, yeah.

sdk/identity/identity/review/identity.api.md Outdated Show resolved Hide resolved
sdk/identity/identity/src/client/identityClient.ts Outdated Show resolved Hide resolved
sdk/identity/identity/src/client/identityClient.ts Outdated Show resolved Hide resolved
sdk/identity/identity/src/client/identityClient.ts Outdated Show resolved Hide resolved
try {
return this.acquireTokenFromCache();
} catch (e) {
if (e instanceof AuthenticationRequired) {
Copy link
Member

Choose a reason for hiding this comment

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

this is a bit brittle, since errors from different constructors/contexts are not instanceof, hence a name property check is sometimes better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you have a link to a preferred pattern, by chance?

Copy link
Member

Choose a reason for hiding this comment

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

If you set this.name = "YourErrorType" in your custom error constructor, you can check if e.name === "YourErrorType". It's not necessarily better, but duck typing on a discriminant seems a little better than relying on two packages using the same constructor function for an error.

Copy link
Member

Choose a reason for hiding this comment

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

It'll also print your custom error's class name when toString is called, along with message.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Luckily, we have full control here, since we both define and are the only ones throwing this error. I think I'll leave it as-is for now, but something we might want to throw into the next JS/TS team discussion around preferred coding patterns.

cacheOptions?: {
cachePlugin?: {
readFromStorage: () => Promise<string>;
writeToStorage: (getMergedState: (oldState: string) => string) => Promise<void>;
Copy link
Member

Choose a reason for hiding this comment

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

feels kinda bad that write isn't the same shape as read... what does getMergedState() do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This comes from having to merge our changes into the cache rather than just writing out (other applications and/or other languages may have written to the shared cache)

Copy link
Member

Choose a reason for hiding this comment

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

Right, so you always hand back oldState being the last string you read in with readFromStorage and then write that? Or something else?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Generally, the user won't even fulfill this contract. We'll soon be providing helper functions that will create the storage/persistence plugin for you.

I'm tempted to chalk this up as some of the underlying implementation leaking through a little bit, though I'm not sure how to resolve as we still currently allow users to implement their own persistence plugins.

@schaabs - any thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

We've been back and forth on whether we'll be adding APIs to allow people to write the cache themselves. If we can hide these for now it would be ideal. But given it's a preview if we can't easily hide these in the short term we could include them, but we should be clear it's subject to change.

status: response.status
};
});
}
Copy link
Contributor

@sadasant sadasant Sep 24, 2020

Choose a reason for hiding this comment

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

Sorry to ask, but I'd like to know: Don't we need tracing here?

this.port = parseInt(url.port);
if (isNaN(this.port)) {
this.port = 80;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

In the odd scenario where somebody has the processor when this method is called, could it be possible for something actively reading this property to read it in a NaN state?

If that isn't plausible, then ignore this! 🌞

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure what you're asking here. Though maybe this will help clarify the above, from parseInt docs:

Return value
An integer parsed from the given string.

Or NaN when

the radix is smaller than 2 or bigger than 36, or
the first non-whitespace character cannot be converted to a number.

There may be clearer ways to do the above. I'm just detecting the NaN case and defaulting the port.

@@ -31,6 +31,15 @@ export class AuthenticationError extends Error {
// @public
export const AuthenticationErrorName = "AuthenticationError";

// @public
export interface AuthenticationRecord {
Copy link
Member

Choose a reason for hiding this comment

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

AuthenticationRecord should have a clientId property as well as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It comes from https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/src/account/AccountInfo.ts#L14-L20

Should we take in clientId and repackage it for MSAL as one of their fields?

Copy link
Member

Choose a reason for hiding this comment

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

This data should be returned in the response from acquireToken***.

Copy link
Member

@schaabs schaabs Sep 28, 2020

Choose a reason for hiding this comment

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

Actually, I take that back. In .NET we take that data into the constructor of the InteractiveBrowserCredential and store it as a property, ClientId, on the instance:

https://github.com/Azure/azure-sdk-for-net/blob/a8a0e8cdf5528017fa3dfd22cae81a879e7f0d39/sdk/identity/Azure.Identity/src/InteractiveBrowserCredential.cs#L75-L77

Then when we interactively authenticate we update the AuthenticationRecord also passing in the stored ClientId:

https://github.com/Azure/azure-sdk-for-net/blob/a8a0e8cdf5528017fa3dfd22cae81a879e7f0d39/sdk/identity/Azure.Identity/src/InteractiveBrowserCredential.cs#L207-L214

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not quite sure I follow. Are you saying you do repack the value for MSAL and rename the field? Or just that .NET works differently than msal-node

expiresOnTimestamp: response.expiresOn.getTime(),
token: response.accessToken
};
} catch (e) {
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure that handling ALL exceptions thrown from acquireTokenSilent is the right thing to do here. Does MSAL document an particular error they will raise in the case that interaction is needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From the MSAL docs:

"In case the refresh_token is expired or not found, an error is thrown and the guidance is for the user to call any interactive token acquisition API (eg: acquireTokenByCode())."

We could ask them for more details, and see if they're comfortable locking down the exception side of the API or exposing more information.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, we should follow up with them on this. It would be nice to make the error contract for these cases a bit tighter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This sounds like it's out of scope for the time being. But they can look into it for a follow-up. I think we can take their change and modify what we do. It doesn't feel like necessarily a breaking change for us.

@schaabs schaabs self-requested a review September 29, 2020 19:46
Copy link
Member

@schaabs schaabs left a comment

Choose a reason for hiding this comment

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

I think the current design is ok for a preview so we can get feedback here. We're still finalizing the design across languages so I don't think we should block getting this into this preview. Looks good from my perspective.

Copy link
Member

@xirzec xirzec left a comment

Choose a reason for hiding this comment

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

I think this is fine, with the caveat that as-is this package will eventually break due to MSAL breaking changes.

Left some comments around async functions and custom errors. We really need a better custom error story for our SDKs.

@@ -184,6 +190,36 @@ export class IdentityClient extends ServiceClient {
}
}

sendGetRequestAsync<T>(
Copy link
Member

Choose a reason for hiding this comment

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

why isn't this an async function so we can use await inside instead?

});
}

sendPostRequestAsync<T>(
Copy link
Member

Choose a reason for hiding this comment

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

this can also be an async function. The Async suffix on these methods feels very .NET though, we don't follow that convention anywhere else.

tenantId: string;
username: string;
}
class AuthenticationRequired extends CredentialUnavailable {}
Copy link
Member

Choose a reason for hiding this comment

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

might be worth setting a custom name on the error class. It also feels a little bad to not have the suffix Error here, as all the built-in JS errors are suffixed with Error.

Oh and if you're going to make a custom error type, shouldn't you export it somewhere?

Copy link
Member

Choose a reason for hiding this comment

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

You can see how we set the prototype and name in RestError here: https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/core/core-http/src/restError.ts

@@ -66,6 +65,12 @@ export class InteractiveBrowserCredential implements TokenCredential {
this.redirectUri = "http://localhost";
}

const url = new URL(this.redirectUri);
Copy link
Member

Choose a reason for hiding this comment

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

I believe this won't work on older Nodes. You can see how I do this in core-https: https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/core/core-https/src/util/url.ts

@@ -104,7 +109,37 @@ export class InteractiveBrowserCredential implements TokenCredential {
): Promise<AccessToken | null> {
const scopeArray = typeof scopes === "object" ? scopes : [scopes];

return this.acquireTokenFromBrowser(scopeArray);
if (this.authenticationRecord && this.persistenceEnabled) {
return this.acquireTokenFromCache().catch((e) => {
Copy link
Member

Choose a reason for hiding this comment

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

not sure why this method isn't async so you can try/catch here instead of using .catch

@sophiajt
Copy link
Contributor Author

Caveats noted. I'm working on a follow up PR that should address many of these, but it may not quite make it by the deadline for beta 2. Landing this just in case, but I hope to improve upon the shortcomings of this PR soon.

@sophiajt sophiajt merged commit b9eb1c5 into Azure:master Sep 29, 2020
@sophiajt sophiajt deleted the msal_wip4 branch September 29, 2020 20:17
openapi-sdkautomation bot pushed a commit to AzureSDKAutomation/azure-sdk-for-js that referenced this pull request Dec 25, 2020
[Hub Generated] Review request for Microsoft.StorageCache to add version stable/2020-10-01 (Azure#11407)

* Init the next version so diffs can work better.

* Updates readme

* Updates API version in new specs and examples

* Updates to StorageTargetProperties and added examples of cmk, mtu

Add 202 to the storage cache and storage target create/update and update examples

Fix ST no junction example's 202 response.

add properties for nfs extended groups

fix issues with extended groups properties found with autorest

add properties and objects for Active Directory username download

undo unintended change

changes from comments in pull request

AccessPolicy support.

Example fixes for Access Policies.

Fix attribute names to match latest RP.

update to credential properties for LDAP and Active Directory

marking password properties with x-ms-secret tag

minor changes on extended groups and add examples

Added blob NFS and some other validation fixes.

Update required property for domainName from dnsName

Updated blobNfs examples and some kpi fixes.

Correct validation errors in examples.

Added systemdata to resources.

Remove x-ms-secret in keyvault reference due to linter error and common types not using it.

Remove blobNfs from this version.

Remove blobNfs from spec file.

Remove x-ms-secret due to linter errors.

Fix certificate spelling.

Updating prettier and spell check errors.

Used prettier on main spec file.

Readded x-ms-secret that open api hub failed on but the PR pipeline allows.

* Add prettier fix after rebase

* Remove 202 bodies and add systemData to examples.

* Update spec with prettier.

* Address comments on spec for descriptions, readmes, and extendedGroupsEnabled.

* Updating to address addition ldap and pattern comments.

* Update version tag to include 01

* Restore changes that would be considered breaking changes matching 2020-03-01

* Address a few other comments.

* Update netbios field names and some descriptions.

* Fix s360 for missing debugInfo operation.

* Fix credscan error in example file.

* Update required fields, new validator failure on debugInfo return codes, and bindPassword example value.

* Update debug info example with new return codes.

* Update other credscan password errors.
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