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

Make token credentials compatible with @azure/core-auth's TokenCredential #66

Closed
wants to merge 12 commits into from

Conversation

daviwil
Copy link
Contributor

@daviwil daviwil commented Jul 19, 2019

This change makes ms-rest-nodeauth token credential implementations compatible with @azure/core-auth's TokenCredential, enabling them to be used with Track 2 SDK libraries. This is part of the resolution for Azure/azure-sdk-for-js#4108.

I've verified these changes with DeviceTokenCredentials, AzureCliCredentials, and ApplicationTokenCredentials using a version of @azure/arm-resources that does not accept ServiceClientCredentials in its ServiceClient constructor. I also removed the use of signingPolicy in @azure/core-auth in my local build just to make sure these credentials are being used with BearerTokenAuthenticationPolicy.

One implication of this change is that the fix we made to isTokenCredential in Azure/azure-sdk-for-js#4335 now has to be rolled back because all of these credentials now expose both getToken and signRequest due to implementing both ServiceClientCredentials and TokenCredential. I'll make a PR to @azure/core-http if we decide we like this compatibility approach.

Questions for Reviewers

  • I've made this a major version bump, but do we think the API changes would actually be considered breaking changes since they are only additive?
  • What do you think about the name prepareToken? Doesn't seem explicit enough to me so I'm open to ideas for a better name.

@daviwil
Copy link
Contributor Author

daviwil commented Jul 19, 2019

Worth noting that the CI build will fail because we haven't shipped @azure/core-auth yet. Once we decide whether the fix in Azure/azure-sdk-for-js#4335 should be rolled back, I'll get that change in and then have the engineering systems team ship that library for us.

@daviwil
Copy link
Contributor Author

daviwil commented Jul 19, 2019

PR Azure/azure-sdk-for-js#4367 restricts the ServiceClient interface to only accept TokenCredential implementations.

export function prepareToken<T extends TokenResponseLike>(
token: T,
scopes: string | string[] | undefined): T | AccessToken {
if (scopes) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What if scopes is an empty string ? This check will fail for that. If the intention is to check specifically for null or undefined then using the "double equal to" or "not with a single equal to" will be a good check as that will take care of null and undefined.
if (scopes != undefined)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! It shouldn't be an empty string, but you never know what a user might do. I'll make the check stricter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On second thought, the idea is check for whether a parameter was passed to scopes, not to scrutinize whether the value of scopes is valid. I think in this case I'm only concerned about whether it's explicitly undefined since that's what the parameter would contain if nothing was passed.

BearerTokenAuthenticationPolicy in core-http will always pass a string or an array to the scopes parameter so I think it's pretty safe to only check for undefined since we don't actually use the real value of scopes in these forward-compatible implementations anyway.

package.json Outdated
@@ -28,6 +28,7 @@
"tsconfig.json"
],
"dependencies": {
"@azure/core-auth": "1.0.0-preview.1",
"@azure/ms-rest-azure-env": "^1.1.2",
"@azure/ms-rest-js": "^1.8.7",
Copy link
Contributor

@amarzavery amarzavery Jul 19, 2019

Choose a reason for hiding this comment

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

Should this be replaced by @azure/core-http? By not moving to core-http do we risk having type conflicts for the customer in his/her application?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nah, we pulled the TokenCredential interface and related types into @azure/core-auth so that they can be reused in other places. @azure/core-http depends on @azure/core-auth in the unshipped code in master.

@amarzavery
Copy link
Contributor

For safety, I would let this be a major version bump (3.0.0). It keeps a clear boundary between new stuff and the old stuff. Makes things easier for us to maintain.

return {
token: token.accessToken,
expiresOnTimestamp:
typeof token.expiresOn === "string"
Copy link
Contributor

Choose a reason for hiding this comment

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

This assumes that expiresOn exists on all the credential types in ms-rest-nodeauth.
Are we sure about this assumption?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I can't find expiresOn being assigned anywhere in a few of the credential implementations. @amarzavery, do you remember how users are supposed to deal with token expiration in this library? I only see AzureCliCredentials implementing any kind of token refresh logic.

Copy link
Contributor

Choose a reason for hiding this comment

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

adal-node has an in memory token cache. It gets a new token if it is about to get expired or gives the token from the token cache.

For AzureCliCredentials we are calling the cli to authenticate and give us the new token that will be valid for the next one hour. Cli knows how to refresh the token if it is using it internally. We can't be calling the Cli for every request, as that will terribly slow down the sdk (spawning a new process and asking the Cli for creds).

Thus we try to do the same thing that adal-python would do anyways (if we are within the token refresh window (last 5 mins before the token expiry) then ask the Cli for token, it will ask adal-python which will then get the new token that will be valid for the next one hour).

Thus we need to check the expiresOn property.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've loosened the expectation of an expiresOn property on the token and put a default expiration timestamp of an hour if one is not provided. What do you think?

@@ -0,0 +1,28 @@
import { AccessToken } from "@azure/core-auth";

export interface TokenResponseLike {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this have to be exported? I don't see it being referenced anywhere other than this file

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Possibly not, I'll give it a shot.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like removing the export worked fine!

@@ -74,7 +78,7 @@ export class ApplicationTokenCertificateCredentials extends ApplicationTokenCred
if (tokenResponse.error || tokenResponse.errorDescription) {
return reject(tokenResponse);
}
return resolve(tokenResponse as TokenResponse);
return resolve(prepareToken(tokenResponse as TokenResponse, scopes));
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Where-ever applicable, please consider having a const for the prepared token instead of inlining the call to prepareToken in this manner. Having the call in a separate line would make it easier to add breakpoints when debugging

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, I'll make that change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

import { assert } from "chai";
import { prepareToken } from "../../lib/credentials/coreAuthHelpers";

describe("prepareToken", function() {
Copy link
Contributor

@ramya-rao-a ramya-rao-a Jul 21, 2019

Choose a reason for hiding this comment

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

There are 2 test files for MSI creds at the moment.
Can we update the test cases in these files to include a call to getToken(scopes) to check that the returned value is of type AccessToken?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll give it a shot

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a test in each MSI spec file to verify that an AccessToken is returned when a scope is passed to getToken. Is that sufficient?

@ramya-rao-a
Copy link
Contributor

Once we decide whether the fix in Azure/azure-sdk-for-js#4335 should be rolled back ...

Having creds from ms-rest-nodeauth be compatible with core-auth doesnt solve the problem that was being fixed in Azure/azure-sdk-for-js#4335 does it?

Or are we expecting that a combination of the above and Azure/azure-sdk-for-js#4367, solves the issue in Azure/azure-sdk-for-js#4335

@daviwil
Copy link
Contributor Author

daviwil commented Jul 22, 2019

Or are we expecting that a combination of the above and Azure/azure-sdk-for-js#4367, solves the issue in Azure/azure-sdk-for-js#4335

Yep, it's the combination of fixes that mostly solves the problem. Previously the problem was that ms-rest-nodeauth credentials implemented TokenClientCredentials which has its own getToken method that takes no parameters and has a different return type. isTokenCredential would incorrectly identify such a credential as TokenCredential and then use BearerTokenAuthenticationPolicy for it instead of SigningPolicy. This would mean that the credential from ms-rest-nodeauth would get used incorrectly and probably throw an error (or at the very least authentication would fail).

After these new two PRs, we only ever use BearerTokenAuthenticationPolicy inside of ServiceClient so the isTokenCredential check doesn't need to be as strict. In fact, the check for signRequest now becomes a problem for ms-rest-nodeauth credentials because they would not be evaluated as TokenCredential implementations anymore due to having a signRequest method (even though they also implement TokenCredential). Thus the check had to be removed.

This does leave us with one problem, though: we don't have a reliable way to distinguish between a TokenClientCredentials and a TokenCredential because they both have a getToken method, just with different parameters and return type. As @bterlson mentioned in Azure/azure-sdk-for-js#4335, using getToken.length to get the parameter count is unreliable, so at the moment there isn't a better way to distinguish between the two interfaces. This would only be a problem if someone passed us a credential that implemented TokenClientCredentials but not TokenCredential (like one of the credentials from ms-rest-nodeauth before the changes in this PR).

I'd be happy to hear if someone has an idea for how to get around this problem!

@daviwil
Copy link
Contributor Author

daviwil commented Jul 22, 2019

Had a better idea on how to make isTokenCredential a bit more reliable, left it as a comment over on the other PR: Azure/azure-sdk-for-js#4367 (comment)

@@ -120,7 +122,9 @@ export class AzureCliCredentials implements TokenClientCredentials {
* changed else uses the cached accessToken.
* @return The tokenResponse (tokenType and accessToken are the two important properties).
*/
public async getToken(): Promise<TokenResponse> {
public getToken(): Promise<TokenResponse>;
public getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why no async keywords for the overloads?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because the overload definitions are just typings and async is only really needed with a function body that uses await. That said, I'll add the keywords to minimize confusion.

Copy link
Contributor

Choose a reason for hiding this comment

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

My confusion was because in a couple of places we did add the async keyword for the overloads, and in some places we didnt

public getToken(): Promise<TokenResponse> {
public getToken(): Promise<TokenResponse>;
public getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
public async getToken(scopes?: string | string[]): Promise<TokenResponse | AccessToken> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing async keywords on the overloads

Changelog.md Outdated
@@ -1,4 +1,9 @@
# Changelog

## 3.0.0 - 2019/07/19
Copy link
Contributor

Choose a reason for hiding this comment

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

Date would need an update

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll update it before merging

@daviwil daviwil force-pushed the core-auth-compat branch from 565a9fe to 10e63d5 Compare July 23, 2019 19:43
@daviwil
Copy link
Contributor Author

daviwil commented Jan 30, 2020

This PR is no longer necessary as we've made @azure/core-http accept both ServiceClientCredentials and TokenCredential

@daviwil daviwil closed this Jan 30, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants