Skip to content

Auth: Support required endorsements #2019

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

Merged
merged 5 commits into from
Jun 7, 2019
Merged

Conversation

carlosscastro
Copy link
Member

@carlosscastro carlosscastro commented Jun 6, 2019

  • Support for enforcing required endorsements in the auth layer
  • Created AuthConfiguration abstraction received by the BotAdapter. It is a class and not an interface since interfaces cannot be added fields to after first release, so it is quite inconvenient.
  • Changed JwtTokenValidator and JwtToken extractor to support testing of endorsements, including mocking the endorsement retriever, which can now optionally be received as a constructor parameter.
  • Added JwtTokenExtractor tests to verify the change and further allow future coverage increases in this code area

Copy link
Member

@dandriscoll dandriscoll 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 logic is right but method comments are holdover and need to be cleaned up.

A fallacy I want to avoid is devs thinking: if I call EndorsementsValidator.Validate(), my token is valid.

Instead, I want them to think: if I call EndorsementsValidator.Validate(), I have performed one step in proving the token is valid.

@@ -17,18 +17,18 @@ public static class EndorsementsValidator
/// <see cref="Schema.Activity.ChannelId"/> property is set to "webchat" and the signing party
/// of the JWT token must have a corresponding endorsement of “Webchat”.
/// </summary>
/// <param name="channelId">The ID of the channel to validate, typically extracted from the activity's
/// <param name="expectedEndorsement">The expected endorsement. Generally the ID of the channel to validate, typically extracted from the activity's
Copy link
Member

Choose a reason for hiding this comment

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

The right way to think about this is:

  • In nearly all cases, the channel ID is the required endorsement
  • In some cases, the developer may elect to require an additional endorsement, but this is always additive to the channel ID endorsement
  • In very rare cases, a developer will elect to remove the channel ID endorsement, but they are operating in a less secure / unsupported mode at this point.

/// <see cref="Schema.Activity.ChannelId"/> property, that to which the Activity is affinitized.</param>
/// <param name="endorsements">The JWT token’s signing party is permitted to send activities only for
/// specific channels. That list, the set of channels the service can sign for, is called the the endorsement list.
/// The activity’s <see cref="Schema.Activity.ChannelId"/> MUST be found in the endorsement list, or the incoming
/// activity is not considered valid.</param>
/// <returns>True if the channel ID is found in the endorsements list; otherwise, false.</returns>
public static bool Validate(string channelId, HashSet<string> endorsements)
public static bool Validate(string expectedEndorsement, HashSet<string> endorsements)
Copy link
Member

Choose a reason for hiding this comment

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

Following the logic above, I expect that new endorsements are added to the channel ID endorsement.

We could have a mode where all endorsements are removed, but this is advanced and I'm not sure we have a use case for it.

Copy link
Member

Choose a reason for hiding this comment

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

Ahh, I see how the channel ID endorsement verification is enforced at the layer above. Seems OK to me. Maybe just comment that multiple endorsements are enforced by calling this method multiple times.

Copy link
Member

Choose a reason for hiding this comment

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

Reading more through the comment on this method, it should be rewritten to something more like:

    /// <summary>
    /// Verify that the specified endorsement exists on the JWT token. Call this method multiple times to validate multiple endorsements.
    /// </summary>
    /// <remarks>
    /// JWT token signing keys contain endorsements matching channel IDs they're approved to sign for. They also can contain keywords representing compliance certifications. This code ensures that a channel ID or compliance certification is present on the signing key used for the request's token.
    /// </remarks>

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, EndorsementsValidator.Validate is only a collection existence check which was somewhat underwhelming to find out.

@@ -124,7 +132,13 @@ public async Task<ClaimsIdentity> GetIdentityAsync(string scheme, string paramet

private bool HasAllowedIssuer(string jwtToken)
{
if (!_tokenValidationParameters.ValidateIssuer)
{
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand this line. Has this ever worked? The logic has me quite confused...

Copy link
Member Author

Choose a reason for hiding this comment

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

During tests I was trying to skip issuer validation and noticed that even if I set it to false it kept validating issuers. And it was because this line was not there.

In production, generally ValidateIssuer is true, so it was always correct to validate the issuer.

Copy link
Contributor

@cleemullins cleemullins left a comment

Choose a reason for hiding this comment

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

🕐

@cleemullins
Copy link
Contributor

I ran the CI build against this branch, and got this error:

Failed   ConnectAssignsUniqueIds
Error Message:
 Test method Microsoft.Bot.Configuration.Tests.ConnectionTests.ConnectAssignsUniqueIds threw exception: 
System.Exception: service with 1 is already connected
Stack Trace:
    at Microsoft.Bot.Configuration.BotConfiguration.ConnectService(ConnectedService newService) in d:\a\1\s\libraries\Microsoft.Bot.Configuration\BotConfiguration.cs:line 267
   at Microsoft.Bot.Configuration.Tests.ConnectionTests.ConnectAssignsUniqueIds() in d:\a\1\s\tests\Microsoft.Bot.Configuration.Tests\ConnectionTests.cs:line 22

Standard Output Messages:

@cleemullins
Copy link
Contributor

(I was trying to get the "Compat" list. I'll try that again, although it means running a build w/ no tests...)

@cleemullins
Copy link
Contributor

Our Binary Compat tool reports the following:

Compat issues with assembly Microsoft.Bot.Builder:
MembersMustExist : Member 'Microsoft.Bot.Builder.BotFrameworkAdapter..ctor(Microsoft.Bot.Connector.Authentication.ICredentialProvider, Microsoft.Bot.Connector.Authentication.IChannelProvider, Microsoft.Rest.TransientFaultHandling.RetryPolicy, System.Net.Http.HttpClient, Microsoft.Bot.Builder.IMiddleware, Microsoft.Extensions.Logging.ILogger)' does not exist in the implementation but it does exist in the contract.
Total Issues: 1


##[warning]There were 1 differences between the assemblies
##[section]Finishing: Compare Binaries

and this:

Compat issues with assembly Microsoft.Bot.Connector:
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.ChannelValidation.AuthenticateChannelToken(System.String, Microsoft.Bot.Connector.Authentication.ICredentialProvider, System.Net.Http.HttpClient, System.String)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.ChannelValidation.AuthenticateChannelToken(System.String, Microsoft.Bot.Connector.Authentication.ICredentialProvider, System.String, System.Net.Http.HttpClient, System.String)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.EmulatorValidation.AuthenticateEmulatorToken(System.String, Microsoft.Bot.Connector.Authentication.ICredentialProvider, Microsoft.Bot.Connector.Authentication.IChannelProvider, System.Net.Http.HttpClient, System.String)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.EnterpriseChannelValidation.AuthenticateChannelToken(System.String, Microsoft.Bot.Connector.Authentication.ICredentialProvider, Microsoft.Bot.Connector.Authentication.IChannelProvider, System.String, System.Net.Http.HttpClient, System.String)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.GovernmentChannelValidation.AuthenticateChannelToken(System.String, Microsoft.Bot.Connector.Authentication.ICredentialProvider, System.String, System.Net.Http.HttpClient, System.String)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.JwtTokenExtractor..ctor(System.Net.Http.HttpClient, Microsoft.IdentityModel.Tokens.TokenValidationParameters, System.String, System.Collections.Generic.HashSet<System.String>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.JwtTokenExtractor.GetIdentityAsync(System.String, System.String)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.JwtTokenExtractor.GetIdentityAsync(System.String, System.String, System.String)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.JwtTokenValidation.AuthenticateRequest(Microsoft.Bot.Schema.IActivity, System.String, Microsoft.Bot.Connector.Authentication.ICredentialProvider, Microsoft.Bot.Connector.Authentication.IChannelProvider, System.Net.Http.HttpClient)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.JwtTokenValidation.ValidateAuthHeader(System.String, Microsoft.Bot.Connector.Authentication.ICredentialProvider, Microsoft.Bot.Connector.Authentication.IChannelProvider, System.String, System.String, System.Net.Http.HttpClient)' does not exist in the implementation but it does exist in the contract.
Total Issues: 10
##[warning]There were 10 differences between the assemblies

@carlosscastro
Copy link
Member Author

Thanks for the reviews @cleemullins and @dandriscoll! All the comments are completely reasonable, I'll address them tonight and send an update.

@cleemullins
Copy link
Contributor

        _tokenValidationParameters.RequireSignedTokens = true;

I believe this ctor should call the newer ctor, as the overlap in what they do is nearly complete. (only the _endorsementsData is different)


Refers to: libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs:72 in 0f66b75. [](commit_id = 0f66b75, deletion_comment = False)

@cleemullins
Copy link
Contributor

A Passing build (Tests + Compat) for this branch is here:
https://fuselabs.visualstudio.com/SDK_v4/_build/results?buildId=63320

@carlosscastro
Copy link
Member Author

Thanks for the reviews!

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