-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Conversation
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.
Left some thoughts / code cleanup suggestions
sdk/identity/identity/package.json
Outdated
@@ -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", |
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 we pin this? or are they not making breaking changes? ;)
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.
They're still making changes, yeah.
try { | ||
return this.acquireTokenFromCache(); | ||
} catch (e) { | ||
if (e instanceof AuthenticationRequired) { |
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 a bit brittle, since errors from different constructors/contexts are not instanceof
, hence a name property check is sometimes better.
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.
Do you have a link to a preferred pattern, by chance?
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 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.
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.
It'll also print your custom error's class name when toString is called, along with 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.
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.
sdk/identity/identity/src/credentials/interactiveBrowserCredential.ts
Outdated
Show resolved
Hide resolved
sdk/identity/identity/src/credentials/interactiveBrowserCredential.ts
Outdated
Show resolved
Hide resolved
sdk/identity/identity/src/credentials/interactiveBrowserCredential.ts
Outdated
Show resolved
Hide resolved
cacheOptions?: { | ||
cachePlugin?: { | ||
readFromStorage: () => Promise<string>; | ||
writeToStorage: (getMergedState: (oldState: string) => string) => Promise<void>; |
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.
feels kinda bad that write isn't the same shape as read... what does getMergedState() do?
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 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)
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.
Right, so you always hand back oldState
being the last string you read in with readFromStorage
and then write that? Or something 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.
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?
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'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 | ||
}; | ||
}); | ||
} |
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.
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; | ||
} |
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.
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! 🌞
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.
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 { |
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.
AuthenticationRecord
should have a clientId
property as well as well.
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 we take in clientId
and repackage it for MSAL as one of their fields?
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 data should be returned in the response from acquireToken***
.
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.
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:
Then when we interactively authenticate we update the AuthenticationRecord
also passing in the stored ClientId
:
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.
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) { |
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'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?
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.
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.
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, we should follow up with them on this. It would be nice to make the error contract for these cases a bit tighter.
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 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.
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 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.
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 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>( |
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 isn't this an async function so we can use await inside instead?
}); | ||
} | ||
|
||
sendPostRequestAsync<T>( |
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 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 {} |
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.
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?
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 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); |
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 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) => { |
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.
not sure why this method isn't async so you can try/catch here instead of using .catch
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. |
[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.
This is another batch of improvements on our MSAL interop. In this PR:
We'll continue improving, I just wanted to land updates more incrementally.