-
Notifications
You must be signed in to change notification settings - Fork 644
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
Conversation
… static when it is provided by model
src/NuGetGallery/Strings.resx
Outdated
@@ -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"> |
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.
LinkingMultipleExternalAccountsUnsupported, LinkingMultipleExternalAccountsUnsupportedFailure - the names are too similar.
Can you rename to something clearer?
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.
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?
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.
LinkingMultipleExternalAccountsUnsupported
-> AccountIsLinkedToAnotherExternalAccount
LinkingMultipleExternalAccountsUnsupportedFailure
-> LinkingMultipleExternalAccountsUnsupported
existingUserLinkingError = Strings.LinkingMultipleExternalAccountsUnsupported; | ||
} | ||
|
||
if (existingUser is Organization) |
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.
else if
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.
done
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.
How was this tested?
{ | ||
existingUserCanBeLinked = false; | ||
existingUserLinkingError = Strings.LinkingMultipleExternalAccountsUnsupported; | ||
} |
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.
else if
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.
done
|
||
namespace NuGetGallery | ||
{ | ||
public static class CredentialExtensions |
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.
There's a class called CredentialTypes where we have similar methods. Please put those there
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.
moved
@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; } |
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.
Can this just return string.IsNullOrEmpty(ExistingUserLinkingError)? Then can remove default ctor below.
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.
done
src/NuGetGallery/Strings.resx
Outdated
<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> |
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 this direct user to Account page to unlink other external accounts?
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.
@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.
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.
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; |
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.
would remove existingUserCanBeLinked, per other comment above
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.
done
CultureInfo.CurrentCulture, | ||
existingUserLinkingError, | ||
email); | ||
} |
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.
Would remove this block and just do string.Format when setting existingUserLinkingError
.
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.
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; | ||
} |
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.
Suggest using if/else if for these two existing user checks
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.
done
if (authenticatedUser.User.Credentials.Any(c => c.IsExternal())) | ||
{ | ||
return SignInFailure(model, linkingAccount, Strings.LinkingMultipleExternalAccountsUnsupported); | ||
} |
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 need this check here? Isn't this failure covered in LinkExternalAccount
below?
Why 2 different error messages for these checks?
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 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)) |
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.
Don't need this check if you have ExistingUserCanBeLinked just return string.IsNullOrEmpty(ExistingUserLinkingError)
.
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.
done
else | ||
{ | ||
@Html.ShowTextBoxFor(m => m.SignIn.UserNameOrEmail) | ||
} | ||
@Html.ShowValidationMessagesFor(m => m.SignIn.UserNameOrEmail) |
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.
@Html.ShowValidationMessagesFor(...)
should go with @Html.ShowTextBoxfor(...)
, right?
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 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; |
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.
nit: sort usings
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.
done
else | ||
{ | ||
@Html.ShowEmailBoxFor(m => m.Register.EmailAddress) | ||
} | ||
@Html.ShowValidationMessagesFor(m => m.Register.EmailAddress) |
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.
@Html.ShowValidationMessagesFor(...)
should go with @Html.ShowEmailBoxfor(...)
, right?
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 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.
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 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.
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 could indicate bad data (honestly more likely that the user hacked the form with F12), but given that
- 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
- this would happen very rarely
- we can debug the issue easier if the user experiencing it can tell us the error message
- 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.
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) |
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.
@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.
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 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)
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 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.
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.
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); |
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.
nit: long line
|
||
if (existingUserLinkingError != null) | ||
{ | ||
existingUserLinkingError = string.Format( |
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.
Would put the string.Format
inline. This doesn't improve code, and inline would be more maintainable if the resource string args ever diverge.
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.
done
else | ||
{ | ||
@Html.ShowEmailBoxFor(m => m.Register.EmailAddress) | ||
} | ||
@Html.ShowValidationMessagesFor(m => m.Register.EmailAddress) |
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 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.
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( |
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 correct fix here would be to extract the name claim correctly from the claimsIdentity
Otherwise the identity here would be email <email>
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.
Fixed it. All of the AAD on dev (and probably also int and prod as well) have incorrect identities as well.
src/NuGetGallery/Strings.resx
Outdated
<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> |
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.
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() |
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.
Also add the test case for register? when the email doesn't match any account?
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.
There were existing tests for that flow--I didn't change the behavior there at all.
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.
Looks good. Please add the register test case and check the strings with @anangaur before merging.
#5281
Sign-in Flow change