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 External Account Linking Flow #5338

Merged
merged 13 commits into from
Jan 30, 2018
Merged

Update External Account Linking Flow #5338

merged 13 commits into from
Jan 30, 2018

Conversation

scottbommarito
Copy link
Contributor

#5281

Sign-in Flow change

  • Check if the email received links to existing account
    • Existing NuGet.org account
      • Found the matching MSA/AAD login - all good, login successful.
      • Found but has other external logins(MSA/AAD)? - Reject Slide 14
      • No external logins? - Link to Microsoft account - Slide 15
        • Disallow email change in link external account flow.
    • No accounts with specified email address.
      • Remove the "Create account page" - Slide 16
      • Show the create a new account page flow - Slide 17

@@ -736,4 +736,13 @@ For more information, please contact '{2}'.</value>
<data name="TransformAccount_RequestExists" xml:space="preserve">
<value>Another tranform request was created on {0} with organization admin '{1}'.</value>
</data>
<data name="LinkingMultipleExternalAccountsUnsupported" xml:space="preserve">
Copy link
Contributor

Choose a reason for hiding this comment

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

LinkingMultipleExternalAccountsUnsupported, LinkingMultipleExternalAccountsUnsupportedFailure - the names are too similar.
Can you rename to something clearer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

couldn't come up with better names--they're the same issue, just one is a validation error for a form and one is a validation error for a page...any ideas?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

LinkingMultipleExternalAccountsUnsupported -> AccountIsLinkedToAnotherExternalAccount
LinkingMultipleExternalAccountsUnsupportedFailure -> LinkingMultipleExternalAccountsUnsupported

existingUserLinkingError = Strings.LinkingMultipleExternalAccountsUnsupported;
}

if (existingUser is Organization)
Copy link
Contributor

Choose a reason for hiding this comment

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

else if

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

Copy link
Contributor

@skofman1 skofman1 left a comment

Choose a reason for hiding this comment

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

How was this tested?

{
existingUserCanBeLinked = false;
existingUserLinkingError = Strings.LinkingMultipleExternalAccountsUnsupported;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

else if

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


namespace NuGetGallery
{
public static class CredentialExtensions
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a class called CredentialTypes where we have similar methods. Please put those there

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved

@scottbommarito
Copy link
Contributor Author

scottbommarito commented Jan 24, 2018

@skofman1 You can actually test the entire MSA/AAD integration very easily by following some instructions in the OneNote. @shishirx34 showed it to me when I asked him.

@@ -33,6 +33,13 @@ public class AssociateExternalAccountViewModel
public string ProviderAccountNoun { get; set; }
public string AccountName { get; set; }
public bool FoundExistingUser { get; set; }
public bool ExistingUserCanBeLinked { get; set; }
Copy link
Member

Choose a reason for hiding this comment

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

Can this just return string.IsNullOrEmpty(ExistingUserLinkingError)? Then can remove default ctor below.

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

<value>We found an account with the email address associated with this external account ({0}), but linked to a different external account.</value>
</data>
<data name="LinkingMultipleExternalAccountsUnsupported" xml:space="preserve">
<value>You cannot link your NuGet.org account to multiple external accounts.</value>
Copy link
Member

Choose a reason for hiding this comment

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

Should this direct user to Account page to unlink other external accounts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@anangaur was working on the messaging--not sure the status of it. I suggested doing something similar to him or at least a message explaining how to do so.

Copy link
Contributor

Choose a reason for hiding this comment

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

Please run through the flow for strings with @anangaur once before merging.

@@ -373,12 +382,37 @@ public ActionResult ChallengeAuthentication(string returnUrl, string provider)
{
existingUser = _userService.FindByEmailAddress(email);
}

var existingUserCanBeLinked = true;
Copy link
Member

Choose a reason for hiding this comment

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

would remove existingUserCanBeLinked, per other comment above

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

CultureInfo.CurrentCulture,
existingUserLinkingError,
email);
}
Copy link
Member

Choose a reason for hiding this comment

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

Would remove this block and just do string.Format when setting existingUserLinkingError.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When we implement more error messages with different format strings we can do that, but right now I don't see a reason to do so.

{
existingUserCanBeLinked = false;
existingUserLinkingError = Strings.LinkingOrganizationUnsupported;
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggest using if/else if for these two existing user checks

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

if (authenticatedUser.User.Credentials.Any(c => c.IsExternal()))
{
return SignInFailure(model, linkingAccount, Strings.LinkingMultipleExternalAccountsUnsupported);
}
Copy link
Member

Choose a reason for hiding this comment

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

Do you need this check here? Isn't this failure covered in LinkExternalAccount below?

Why 2 different error messages for these checks?

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 is for the form POST, not the initial callback. There needs to be verification here in case somebody hacks the form to put the wrong value in.

The messages are basically the same, but adapted for the context in which they appear.

}
else
{
@:the @Model.External.ProviderAccountNoun '@Model.External.AccountName'.
if (!string.IsNullOrEmpty(Model.External.ExistingUserLinkingError))
Copy link
Member

Choose a reason for hiding this comment

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

Don't need this check if you have ExistingUserCanBeLinked just return string.IsNullOrEmpty(ExistingUserLinkingError).

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

else
{
@Html.ShowTextBoxFor(m => m.SignIn.UserNameOrEmail)
}
@Html.ShowValidationMessagesFor(m => m.SignIn.UserNameOrEmail)
Copy link
Member

Choose a reason for hiding this comment

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

@Html.ShowValidationMessagesFor(...) should go with @Html.ShowTextBoxfor(...), right?

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 don't think so--if MVC returns errors for the validation of the UserNameOrEmail field, but the field is uneditable (with ShowHiddenFor), we still want to see the errors. The user seeing the errors can send the error to us so we can figure out the issue.

@@ -16,6 +16,7 @@
using NuGetGallery.Infrastructure.Authentication;
using Xunit;
using System.Web;
using System.Globalization;
Copy link
Member

Choose a reason for hiding this comment

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

nit: sort usings

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

else
{
@Html.ShowEmailBoxFor(m => m.Register.EmailAddress)
}
@Html.ShowValidationMessagesFor(m => m.Register.EmailAddress)
Copy link
Member

Choose a reason for hiding this comment

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

@Html.ShowValidationMessagesFor(...) should go with @Html.ShowEmailBoxfor(...), right?

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 don't think so--if MVC returns errors for the validation of the EmailAddress field, but the field is uneditable (with ShowHiddenFor), we still want to see the errors. The user seeing the errors can send the error to us so we can figure out the issue.

Copy link
Member

Choose a reason for hiding this comment

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

I disagree here - validation errors are for user input, right? Validation errors help users take corrective actions.

If EmailAddress is not editable and fails validation, wouldn't it indicate bad data on our side. I would not show a validation error in this case, but would log and direct the user to contact support.

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 could indicate bad data (honestly more likely that the user hacked the form with F12), but given that

  1. it would be nasty to have to predict from the form submission endpoint whether or not the error was on an uneditable field or not and then log the model errors
  2. this would happen very rarely
  3. we can debug the issue easier if the user experiencing it can tell us the error message
  4. the user will likely be more annoyed if the form refuses to submit but there's no visible message

I would rather keep this like this.

Ultimately this isn't that big of a deal, which is part of why I'm suggesting this easy solution that allows any users that encounter it to approach us and doesn't require any nontrivial code changes.

@scottbommarito
Copy link
Contributor Author

@skofman1 @chenriksson

Addressed all feedback. Also tested with MSA/AAD (I had only tested with AAD2) and got all working.

return actualType.Equals(expectedType, StringComparison.OrdinalIgnoreCase);
}

private static bool IsSubType(string actualType, string expectedTypePrefix)
Copy link
Contributor

@skofman1 skofman1 Jan 25, 2018

Choose a reason for hiding this comment

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

@scottbommarito , I understand you wanted to make the methods use shared code, but in this case, since it's only a single line (credential.Type.StartsWith()) the result is overly complex. For example: IsPassword(Credential), instead of having a single line, now calls into IsPassword(type) => IsSubType() => string.StartsWith()
It's hard to read and not necessary.
Please undo this 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.

I initially agreed with you and was going to make this change, but then I realized that despite the fact that it does create deeper call stacks, it also substantially reduces the code because there is no longer the need for every method to have an if (type == null) throw new ArgumentNullException in addition to the fact that it unifies all of these StartsWith and Equals calls.

@shishirx34's PR, that came after this one, also touches this code and creates about 20 lines for something that could be closer to 10 with this restructuring. (https://github.com/NuGet/NuGetGallery/pull/5355/files#diff-624219da9e2d31ff3446f6c5be30fe20R43)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the point shouldn't always be to reduce the number of lines if it makes the code unreadable. If few lines to a non-often changing code are repetitive its fine if they are duplicated. I agree with @skofman1 and feel like this is an unnecessary refactoring leading to hard to read code, please undo these changes and keep the code easy to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed to be one-liners that return false if credential or type is null

var identityInfo = ClaimsExtentions.GetIdentityInformation(claimsIdentity, DefaultAuthenticationType);
// The claims returned by AzureActiveDirectory have the email as the name claim are missing the email claim.
// Copy the object returned by the method but set the email as the name.
return new IdentityInformation(identityInfo.Identifier, identityInfo.Name, identityInfo.Name, identityInfo.AuthenticationType, identityInfo.TenantId);
Copy link
Member

Choose a reason for hiding this comment

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

nit: long line


if (existingUserLinkingError != null)
{
existingUserLinkingError = string.Format(
Copy link
Member

Choose a reason for hiding this comment

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

Would put the string.Format inline. This doesn't improve code, and inline would be more maintainable if the resource string args ever diverge.

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

else
{
@Html.ShowEmailBoxFor(m => m.Register.EmailAddress)
}
@Html.ShowValidationMessagesFor(m => m.Register.EmailAddress)
Copy link
Member

Choose a reason for hiding this comment

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

I disagree here - validation errors are for user input, right? Validation errors help users take corrective actions.

If EmailAddress is not editable and fails validation, wouldn't it indicate bad data on our side. I would not show a validation error in this case, but would log and direct the user to contact support.

@chenriksson
Copy link
Member

Please see my comment on validation, but otherwise changes look good to me.


// The claims returned by AzureActiveDirectory have the email as the name claim are missing the email claim.
// Copy the object returned by the method but set the email as the name.
return new IdentityInformation(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the correct fix here would be to extract the name claim correctly from the claimsIdentity Otherwise the identity here would be email <email>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed it. All of the AAD on dev (and probably also int and prod as well) have incorrect identities as well.

<value>We found an account with the email address associated with this external account ({0}), but linked to a different external account.</value>
</data>
<data name="LinkingMultipleExternalAccountsUnsupported" xml:space="preserve">
<value>You cannot link your NuGet.org account to multiple external accounts.</value>
Copy link
Contributor

Choose a reason for hiding this comment

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

Please run through the flow for strings with @anangaur once before merging.

@@ -243,6 +243,134 @@ public class TheSignInAction : TestContainer
GetMock<AuthenticationService>().VerifyAll();
}

public async Task WhenAttemptingToLinkExternalToExistingAccountWithNoExternalAccounts_AllowsLinking()
Copy link
Contributor

Choose a reason for hiding this comment

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

Also add the test case for register? when the email doesn't match any account?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There were existing tests for that flow--I didn't change the behavior there at all.

Copy link
Contributor

@shishirx34 shishirx34 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. Please add the register test case and check the strings with @anangaur before merging.

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.

4 participants