-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Description
Which exact Umbraco version are you using? For example: 8.13.1 - don't just write v8
9 rc1
Bug summary
When login from external provider, the second login you add throws an exception Cannot insert duplicate key row in object 'dbo.umbracoExternalLogin' with unique index 'IX_umbracoExternalLogin_LoginProvider'. I believe this index should be non-unique.
Specifics
I've added an oidc external login provider to v9-rc1 so creatively called "Umbraco.oidc". It works great when the first user signs up; however it pops an exception when he second user signs up or connects their account to the external provider.
I'm pretty sure LoginProvider column in umbracoExternalLogin table should not have a unique index on it.
This is the exception
SqlException: Cannot insert duplicate key row in object 'dbo.umbracoExternalLogin' with unique index 'IX_umbracoExternalLogin_LoginProvider'. The duplicate key value is (Umbraco.oidc). The statement has been terminated.
System.Data.SqlClient.SqlConnection.OnError(SqlException exception, bool breakConnection, Action<Action> wrapCloseInAction)
System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, bool breakConnection, Action<Action> wrapCloseInAction)
System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, bool callerHasConnectionLock, bool asyncClose)
System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, out bool dataReady)
System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
System.Data.SqlClient.SqlBulkCopy.RunParser(BulkCopySimpleResultSet bulkCopyHandler)
System.Data.SqlClient.SqlBulkCopy.CopyBatchesAsyncContinuedOnSuccess(BulkCopySimpleResultSet internalResults, string updateBulkCommandText, CancellationToken cts, TaskCompletionSource<object> source)
System.Data.SqlClient.SqlBulkCopy.CopyBatchesAsyncContinued(BulkCopySimpleResultSet internalResults, string updateBulkCommandText, CancellationToken cts, TaskCompletionSource<object> source)
System.Data.SqlClient.SqlBulkCopy.CopyBatchesAsync(BulkCopySimpleResultSet internalResults, string updateBulkCommandText, CancellationToken cts, TaskCompletionSource<object> source)
System.Data.SqlClient.SqlBulkCopy.WriteToServerInternalRestContinuedAsync(BulkCopySimpleResultSet internalResults, CancellationToken cts, TaskCompletionSource<object> source)
System.Data.SqlClient.SqlBulkCopy.WriteToServerInternalRestAsync(CancellationToken cts, TaskCompletionSource<object> source)
System.Data.SqlClient.SqlBulkCopy.WriteToServerInternalAsync(CancellationToken ctoken)
System.Data.SqlClient.SqlBulkCopy.WriteRowSourceToServerAsync(int columnCount, CancellationToken ctoken)
System.Data.SqlClient.SqlBulkCopy.WriteToServer(DataTable table, DataRowState rowState)
System.Data.SqlClient.SqlBulkCopy.WriteToServer(DataTable table)
NPoco.SqlBulkCopyHelper.BulkInsert<T>(IDatabase db, IEnumerable<T> list, SqlBulkCopyOptions sqlBulkCopyOptions)
NPoco.SqlBulkCopyHelper.BulkInsert<T>(IDatabase db, IEnumerable<T> list)
NPoco.DatabaseTypes.SqlServerDatabaseType.InsertBulk<T>(IDatabase db, IEnumerable<T> pocos)
NPoco.Database.InsertBulk<T>(IEnumerable<T> pocos)
Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement.ExternalLoginRepository.Save(int userId, IEnumerable<IExternalLogin> logins)
Umbraco.Cms.Core.Services.Implement.ExternalLoginService.Save(int userId, IEnumerable<IExternalLogin> logins)
Umbraco.Cms.Core.Security.BackOfficeUserStore.UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
Microsoft.AspNetCore.Identity.UserManager<TUser>.UpdateUserAsync(TUser user)
Microsoft.AspNetCore.Identity.UserManager<TUser>.AddLoginAsync(TUser user, UserLoginInfo login)
Umbraco.Cms.Web.BackOffice.Security.BackOfficeSignInManager.LinkUser(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo)
Umbraco.Cms.Web.BackOffice.Security.BackOfficeSignInManager.AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo, ExternalSignInAutoLinkOptions autoLinkOptions)
Umbraco.Cms.Web.BackOffice.Security.BackOfficeSignInManager.ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor)
Umbraco.Cms.Web.BackOffice.Controllers.BackOfficeController.ExternalSignInAsync(ExternalLoginInfo loginInfo, Func<IActionResult> response)
Umbraco.Cms.Web.BackOffice.Controllers.BackOfficeController.RenderDefaultOrProcessExternalLoginAsync(AuthenticateResult authenticateResult, Func<IActionResult> defaultResponse, Func<IActionResult> externalSignInResponse)
Umbraco.Cms.Web.BackOffice.Controllers.BackOfficeController.Default()
Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor+TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, object controller, object[] arguments)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask<IActionResult> actionResultValueTask)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
Umbraco.Cms.Web.Website.Middleware.PublicAccessMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass6_1+<<UseMiddlewareInterface>b__1>d.MoveNext()
Umbraco.Cms.Web.BackOffice.Middleware.BackOfficeExternalLoginProviderErrorMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass6_1+<<UseMiddlewareInterface>b__1>d.MoveNext()
Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
StackExchange.Profiling.MiniProfilerMiddleware.Invoke(HttpContext context) in C:\projects\dotnet\src\MiniProfiler.AspNetCore\MiniProfilerMiddleware.cs
Umbraco.Cms.Web.Common.Middleware.UmbracoRequestMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Umbraco.Cms.Web.Common.Middleware.UmbracoRequestMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass6_1+<<UseMiddlewareInterface>b__1>d.MoveNext()
Umbraco.Cms.Web.Common.Middleware.PreviewAuthenticationMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass6_1+<<UseMiddlewareInterface>b__1>d.MoveNext()
Umbraco.Cms.Web.Common.Middleware.UmbracoRequestLoggingMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass6_1+<<UseMiddlewareInterface>b__1>d.MoveNext()
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
Steps to reproduce
This is hard to reproduce, you would need an OAuth and OIDC server. Likely easier if you just enable Google Authentication OAuth 2. In my case I'm connecting to our own OIDC/OAuth2 server using an extension to the IUmbracoBuilder as below
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Web.BackOffice.Security;
using Umbraco.Extensions;
using System.Threading.Tasks;
public static class AuthenticationServerExtensions
{
public static IUmbracoBuilder AddAuthenticationServer(this IUmbracoBuilder builder)
{
var authServerOptions = new AuthenticationServerOptions();
builder.Config.GetSection(AuthenticationServerOptions.AuthenticationServer).Bind(authServerOptions);
// create the autoLinkOptions with the OnAutoLinking action set
var autoLinkOptions = new ExternalSignInAutoLinkOptions(
autoLinkExternalAccount: true,
defaultUserGroups: new string[] {"editor"}
);
var autoLinkAction = new AutoLinkAction();
autoLinkOptions.OnAutoLinking = autoLinkAction.Action;
builder.AddBackOfficeExternalLogins(backOfficeLoginOPtions =>
{
backOfficeLoginOPtions.AddBackOfficeLogin(new BackOfficeExternalLoginProviderOptions(
"btn-success",
"fa-cloud",
autoLinkOptions,
denyLocalLogin: authServerOptions.DenyLocalLogin,
autoRedirectLoginToExternalProvider: authServerOptions.AutoRedirectLoginToExternalProvider
), backOfficeAuthOptions =>
{
backOfficeAuthOptions.AddOpenIdConnect("Umbraco.oidc", "City Account", openIdOptions =>
{
// use cookies
openIdOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// pass configured options along
openIdOptions.Authority = authServerOptions.Authority;
openIdOptions.ClientId = authServerOptions.ClientId;
openIdOptions.ClientSecret = authServerOptions.ClientSecret;
// Use the authorization code flow
openIdOptions.ResponseType = OpenIdConnectResponseType.Code;
openIdOptions.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
// map claims
openIdOptions.TokenValidationParameters.NameClaimType = "name";
openIdOptions.TokenValidationParameters.RoleClaimType = "role";
openIdOptions.RequireHttpsMetadata = true;
openIdOptions.GetClaimsFromUserInfoEndpoint = true;
openIdOptions.SaveTokens = true;
// add scopes
openIdOptions.Scope.Add("openid");
openIdOptions.Scope.Add("email");
openIdOptions.Scope.Add("roles");
// options.SecurityTokenValidator = new JwtSecurityTokenHandler
// {
// // Disable the built-in JWT claims mapping feature.
// InboundClaimTypeMap = new Dictionary<string, string>()
// };
openIdOptions.UsePkce = true;
openIdOptions.Events.OnAuthorizationCodeReceived = (AuthorizationCodeReceivedContext context) => {
return Task.CompletedTask;
};
openIdOptions.Events.OnTokenValidated = (TokenValidatedContext) => {
return Task.CompletedTask;
};
openIdOptions.Events.OnUserInformationReceived = (UserInformationReceivedContext ctx) => {
return Task.CompletedTask;
};
});
});
});
return builder;
}
}
Invoke in startup by adding AddAuthenticationServer() for example
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddComposers()
.AddAuthenticationServer()
.Build();
Link up one user. It should work great. Link up a second user and you get the exception.
Expected result / actual result
It shouldn't toss an exception.