diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index f6c2df14c3..e2f2841dc3 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -22,6 +22,8 @@ public static class Security { + public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; + public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapps"; diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs new file mode 100644 index 0000000000..087f5913e6 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Umbraco.Core.Models.Identity +{ + public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim> + { + /// + /// Gets/sets the user's real name + /// + public string Name { get; set; } + public int StartContentNode { get; set; } + public int StartMediaNode { get; set; } + public string[] AllowedApplications { get; set; } + public string Culture { get; set; } + + /// + /// Overridden to make the retrieval lazy + /// + public override ICollection Logins + { + get + { + if (_getLogins != null && _getLogins.IsValueCreated == false) + { + _logins = new ObservableCollection(); + foreach (var l in _getLogins.Value) + { + _logins.Add(l); + } + //now assign events + _logins.CollectionChanged += Logins_CollectionChanged; + } + return _logins; + } + } + + public bool LoginsChanged { get; private set; } + + void Logins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + LoginsChanged = true; + } + + private ObservableCollection _logins; + private Lazy> _getLogins; + + /// + /// Used to set a lazy call back to populate the user's Login list + /// + /// + public void SetLoginsCallback(Lazy> callback) + { + if (callback == null) throw new ArgumentNullException("callback"); + _getLogins = callback; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs new file mode 100644 index 0000000000..c95722f4e3 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs @@ -0,0 +1,25 @@ +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models.Identity +{ + public interface IIdentityUserLogin : IAggregateRoot, IRememberBeingDirty, ICanBeDirty + { + /// + /// The login provider for the login (i.e. facebook, google) + /// + /// + string LoginProvider { get; set; } + + /// + /// Key representing the login for the provider + /// + /// + string ProviderKey { get; set; } + + /// + /// User Id for the user who owns this login + /// + /// + int UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs new file mode 100644 index 0000000000..09306bb1f0 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Models.Identity +{ + /// + /// Default IUser implementation + /// + /// + /// + /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want + /// references to that so we will create our own here + /// + public class IdentityUser : IUser + where TLogin : IIdentityUserLogin + //NOTE: Making our role id a string + where TRole : IdentityUserRole + where TClaim : IdentityUserClaim + { + /// + /// Email + /// + /// + public virtual string Email { get; set; } + + /// + /// True if the email is confirmed, default is false + /// + /// + public virtual bool EmailConfirmed { get; set; } + + /// + /// The salted/hashed form of the user password + /// + /// + public virtual string PasswordHash { get; set; } + + /// + /// A random value that should change whenever a users credentials have changed (password changed, login removed) + /// + /// + public virtual string SecurityStamp { get; set; } + + /// + /// PhoneNumber for the user + /// + /// + public virtual string PhoneNumber { get; set; } + + /// + /// True if the phone number is confirmed, default is false + /// + /// + public virtual bool PhoneNumberConfirmed { get; set; } + + /// + /// Is two factor enabled for the user + /// + /// + public virtual bool TwoFactorEnabled { get; set; } + + /// + /// DateTime in UTC when lockout ends, any time in the past is considered not locked out. + /// + /// + public virtual DateTime? LockoutEndDateUtc { get; set; } + + /// + /// Is lockout enabled for this user + /// + /// + public virtual bool LockoutEnabled { get; set; } + + /// + /// Used to record failures for the purposes of lockout + /// + /// + public virtual int AccessFailedCount { get; set; } + + /// + /// Navigation property for user roles + /// + /// + public virtual ICollection Roles { get; private set; } + + /// + /// Navigation property for user claims + /// + /// + public virtual ICollection Claims { get; private set; } + + /// + /// Navigation property for user logins + /// + /// + public virtual ICollection Logins { get; private set; } + + /// + /// User ID (Primary Key) + /// + /// + public virtual TKey Id { get; set; } + + /// + /// User name + /// + /// + public virtual string UserName { get; set; } + + /// + /// Constructor + /// + /// + public IdentityUser() + { + this.Claims = new List(); + this.Roles = new List(); + this.Logins = new List(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs b/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs new file mode 100644 index 0000000000..832438f3c3 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs @@ -0,0 +1,38 @@ +namespace Umbraco.Core.Models.Identity +{ + /// + /// EntityType that represents one specific user claim + /// + /// + /// + /// + /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want + /// references to that so we will create our own here + /// + public class IdentityUserClaim + { + /// + /// Primary key + /// + /// + public virtual int Id { get; set; } + + /// + /// User Id for the user who owns this login + /// + /// + public virtual TKey UserId { get; set; } + + /// + /// Claim type + /// + /// + public virtual string ClaimType { get; set; } + + /// + /// Claim value + /// + /// + public virtual string ClaimValue { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs new file mode 100644 index 0000000000..f9c77031e9 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs @@ -0,0 +1,39 @@ +using System; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models.Identity +{ + /// + /// Entity type for a user's login (i.e. facebook, google) + /// + /// + public class IdentityUserLogin : Entity, IIdentityUserLogin + { + public IdentityUserLogin(int id, string loginProvider, string providerKey, int userId, DateTime createDate) + { + Id = id; + LoginProvider = loginProvider; + ProviderKey = providerKey; + UserId = userId; + CreateDate = createDate; + } + + /// + /// The login provider for the login (i.e. facebook, google) + /// + /// + public string LoginProvider { get; set; } + + /// + /// Key representing the login for the provider + /// + /// + public string ProviderKey { get; set; } + + /// + /// User Id for the user who owns this login + /// + /// + public int UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs b/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs new file mode 100644 index 0000000000..5c68a97d86 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs @@ -0,0 +1,26 @@ +namespace Umbraco.Core.Models.Identity +{ + /// + /// EntityType that represents a user belonging to a role + /// + /// + /// + /// + /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want + /// references to that so we will create our own here + /// + public class IdentityUserRole + { + /// + /// UserId for the user that is in the role + /// + /// + public virtual TKey UserId { get; set; } + + /// + /// RoleId for the role + /// + /// + public virtual TKey RoleId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/ExternalLoginDto.cs b/src/Umbraco.Core/Models/Rdbms/ExternalLoginDto.cs new file mode 100644 index 0000000000..c94ee1193f --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/ExternalLoginDto.cs @@ -0,0 +1,35 @@ +using System; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoExternalLogin")] + [ExplicitColumns] + [PrimaryKey("externalLoginId")] + internal class ExternalLoginDto + { + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoExternalLogin")] + public int Id { get; set; } + + [Column("userId")] + public int UserId { get; set; } + + [Column("loginProvider")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string LoginProvider { get; set; } + + [Column("providerKey")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string ProviderKey { get; set; } + + [Column("createDate")] + [Constraint(Default = "getdate()")] + public DateTime CreateDate { get; set; } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs new file mode 100644 index 0000000000..4882df3202 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs @@ -0,0 +1,32 @@ +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Rdbms; + +namespace Umbraco.Core.Persistence.Factories +{ + internal class ExternalLoginFactory + { + public IIdentityUserLogin BuildEntity(ExternalLoginDto dto) + { + var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId, dto.CreateDate); + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + entity.ResetDirtyProperties(false); + return entity; + } + + public ExternalLoginDto BuildDto(IIdentityUserLogin entity) + { + var dto = new ExternalLoginDto + { + Id = entity.Id, + CreateDate = entity.CreateDate, + LoginProvider = entity.LoginProvider, + ProviderKey = entity.ProviderKey, + UserId = entity.UserId + }; + + return dto; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index b77e0843ea..25d5f2a9af 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -81,7 +81,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {40, typeof (ServerRegistrationDto)}, {41, typeof (AccessDto)}, - {42, typeof (AccessRuleDto)} + {42, typeof (AccessRuleDto)}, + + {43, typeof (ExternalLoginDto)} }; #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs new file mode 100644 index 0000000000..b29aff9048 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs @@ -0,0 +1,28 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero +{ + [Migration("7.3.0", 9, GlobalSettings.UmbracoMigrationName)] + public class AddExternalLoginsTable : MigrationBase + { + public override void Up() + { + //Don't exeucte if the table is already there + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + if (tables.InvariantContains("umbracoExternalLogin")) return; + + Create.Table("umbracoExternalLogin") + .WithColumn("id").AsInt32().Identity().PrimaryKey("PK_umbracoExternalLogin") + .WithColumn("userId").AsInt32().NotNullable().ForeignKey("FK_umbracoExternalLogin_umbracoUser_id", "umbracoUser", "id") + .WithColumn("loginProvider").AsString(4000).NotNullable() + .WithColumn("providerKey").AsString(4000).NotNullable() + .WithColumn("createDate").AsDateTime().NotNullable().WithDefault(SystemMethods.CurrentDateTime); + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs new file mode 100644 index 0000000000..bafe30d901 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.Factories; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + internal class ExternalLoginRepository : PetaPocoRepositoryBase, IExternalLoginRepository + { + public ExternalLoginRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) + { + } + + public void DeleteUserLogins(int memberId) + { + using (var t = Database.GetTransaction()) + { + Database.Execute("DELETE FROM ExternalLogins WHERE UserId=@userId", new { userId = memberId }); + + t.Complete(); + } + } + + public void SaveUserLogins(int memberId, IEnumerable logins) + { + using (var t = Database.GetTransaction()) + { + //clear out logins for member + Database.Execute("DELETE FROM ExternalLogins WHERE UserId=@userId", new { userId = memberId }); + + //add them all + foreach (var l in logins) + { + Database.Insert(new ExternalLoginDto + { + LoginProvider = l.LoginProvider, + ProviderKey = l.ProviderKey, + UserId = memberId, + CreateDate = DateTime.Now + }); + } + + t.Complete(); + } + } + + protected override IIdentityUserLogin PerformGet(int id) + { + var sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { Id = id }); + + var macroDto = Database.Fetch(sql).FirstOrDefault(); + if (macroDto == null) + return null; + + var factory = new ExternalLoginFactory(); + var entity = factory.BuildEntity(macroDto); + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((TracksChangesEntityBase)entity).ResetDirtyProperties(false); + + return entity; + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + if (ids.Any()) + { + return PerformGetAllOnIds(ids); + } + + var sql = GetBaseQuery(false); + + return ConvertFromDtos(Database.Fetch(sql)) + .ToArray();// we don't want to re-iterate again! + } + + private IEnumerable PerformGetAllOnIds(params int[] ids) + { + if (ids.Any() == false) yield break; + foreach (var id in ids) + { + yield return Get(id); + } + } + + private IEnumerable ConvertFromDtos(IEnumerable dtos) + { + var factory = new ExternalLoginFactory(); + foreach (var entity in dtos.Select(factory.BuildEntity)) + { + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((TracksChangesEntityBase)entity).ResetDirtyProperties(false); + + yield return entity; + } + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate(); + + var dtos = Database.Fetch(sql); + + foreach (var dto in dtos) + { + yield return Get(dto.Id); + } + } + + protected override Sql GetBaseQuery(bool isCount) + { + var sql = new Sql(); + if (isCount) + { + sql.Select("COUNT(*)").From(SqlSyntax); + } + else + { + sql.Select("*").From(SqlSyntax); + } + return sql; + } + + protected override string GetBaseWhereClause() + { + return "umbracoExternalLogin.id = @Id"; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoExternalLogin WHERE id = @Id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { throw new NotImplementedException(); } + } + + protected override void PersistNewItem(IIdentityUserLogin entity) + { + ((Entity)entity).AddingEntity(); + + var factory = new ExternalLoginFactory(); + var dto = factory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IIdentityUserLogin entity) + { + ((Entity)entity).UpdatingEntity(); + + var factory = new ExternalLoginFactory(); + var dto = factory.BuildDto(entity); + + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IExternalLoginRepository.cs new file mode 100644 index 0000000000..17fc153980 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IExternalLoginRepository.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IExternalLoginRepository : IRepositoryQueryable + { + void SaveUserLogins(int memberId, IEnumerable logins); + void DeleteUserLogins(int memberId); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index fb66dc7ade..d13df47b71 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -122,7 +122,8 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM umbracoUser2NodeNotify WHERE userId = @Id", "DELETE FROM umbracoUserLogins WHERE userID = @Id", "DELETE FROM umbracoUser2app WHERE " + SqlSyntax.GetQuotedColumnName("user") + "=@Id", - "DELETE FROM umbracoUser WHERE id = @Id" + "DELETE FROM umbracoUser WHERE id = @Id", + "DELETE FROM umbracoExternalLogin WHERE id = @Id" }; return list; } diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index be6d35ca27..356e876c54 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -64,6 +64,13 @@ namespace Umbraco.Core.Persistence #endregion + public virtual IExternalLoginRepository CreateExternalLoginRepository(IDatabaseUnitOfWork uow) + { + return new ExternalLoginRepository(uow, + _cacheHelper, + _logger, _sqlSyntax); + } + public virtual IPublicAccessRepository CreatePublicAccessRepository(IDatabaseUnitOfWork uow) { return new PublicAccessRepository(uow, diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index dd688cfe25..e71dd0e00c 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -216,7 +216,6 @@ namespace Umbraco.Core.Security GlobalSettings.TimeOutInMinutes, //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way 1440, - "/umbraco", UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain); } @@ -420,7 +419,6 @@ namespace Umbraco.Core.Security string userData, int loginTimeoutMins, int minutesPersisted, - string cookiePath, string cookieName, string cookieDomain) { @@ -433,7 +431,7 @@ namespace Umbraco.Core.Security DateTime.Now.AddMinutes(loginTimeoutMins), true, userData, - cookiePath + "/" ); // Encrypt the cookie using the machine key for secure transport @@ -444,7 +442,7 @@ namespace Umbraco.Core.Security { Expires = DateTime.Now.AddMinutes(minutesPersisted), Domain = cookieDomain, - Path = cookiePath + Path = "/" }; if (GlobalSettings.UseSSL) diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs new file mode 100644 index 0000000000..54b537faab --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + public class BackOfficeClaimsIdentityFactory : ClaimsIdentityFactory + { + /// + /// Create a ClaimsIdentity from a user + /// + /// + /// + public override async Task CreateAsync(UserManager manager, BackOfficeIdentityUser user, string authenticationType) + { + var baseIdentity = await base.CreateAsync(manager, user, authenticationType); + + var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity, new UserData() + { + Id = user.Id, + Username = user.UserName, + RealName = user.Name, + AllowedApplications = user.AllowedApplications, + Culture = user.Culture, + Roles = user.Roles.Select(x => x.RoleId).ToArray(), + StartContentNode = user.StartContentNode, + StartMediaNode = user.StartMediaNode + }); + + return umbracoIdentity; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs new file mode 100644 index 0000000000..9634b17c73 --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -0,0 +1,157 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Web.Security; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Security +{ + /// + /// A custom password hasher that conforms to the current password hashing done in Umbraco + /// + internal class MembershipPasswordHasher : IPasswordHasher + { + private readonly MembershipProviderBase _provider; + + public MembershipPasswordHasher(MembershipProviderBase provider) + { + _provider = provider; + } + + public string HashPassword(string password) + { + return _provider.HashPasswordForStorage(password); + } + + public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) + { + return _provider.VerifyPassword(providedPassword, hashedPassword) + ? PasswordVerificationResult.Success + : PasswordVerificationResult.Failed; + } + + + } + + /// + /// Back office user manager + /// + public class BackOfficeUserManager : UserManager + { + public BackOfficeUserManager(IUserStore store) + : base(store) + { + } + + #region What we support currently + + //TODO: Support this + public override bool SupportsUserRole + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsQueryableUsers + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsUserLockout + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsUserSecurityStamp + { + get { return false; } + } + + public override bool SupportsUserTwoFactor + { + get { return false; } + } + + public override bool SupportsUserPhoneNumber + { + get { return false; } + } + #endregion + + public static BackOfficeUserManager Create( + IdentityFactoryOptions options, + IOwinContext context, + IUserService userService, + IExternalLoginService externalLoginService, + MembershipProviderBase membershipProvider) + { + if (options == null) throw new ArgumentNullException("options"); + if (context == null) throw new ArgumentNullException("context"); + if (userService == null) throw new ArgumentNullException("userService"); + if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); + + var manager = new BackOfficeUserManager(new BackOfficeUserStore(userService, externalLoginService, membershipProvider)); + + // Configure validation logic for usernames + manager.UserValidator = new UserValidator(manager) + { + AllowOnlyAlphanumericUserNames = false, + RequireUniqueEmail = true + }; + + // Configure validation logic for passwords + manager.PasswordValidator = new PasswordValidator + { + RequiredLength = membershipProvider.MinRequiredPasswordLength, + RequireNonLetterOrDigit = membershipProvider.MinRequiredNonAlphanumericCharacters > 0, + RequireDigit = false, + RequireLowercase = false, + RequireUppercase = false + }; + + //use a custom hasher based on our membership provider + manager.PasswordHasher = new MembershipPasswordHasher(membershipProvider); + + var dataProtectionProvider = options.DataProtectionProvider; + if (dataProtectionProvider != null) + { + manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")); + } + + //custom identity factory for creating the identity object for which we auth against in the back office + manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); + + //NOTE: Not implementing these currently + + //// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user + //// You can write your own provider and plug in here. + //manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider + //{ + // MessageFormat = "Your security code is: {0}" + //}); + //manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider + //{ + // Subject = "Security Code", + // BodyFormat = "Your security code is: {0}" + //}); + + //manager.EmailService = new EmailService(); + //manager.SmsService = new SmsService(); + + return manager; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + } + } +} diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs new file mode 100644 index 0000000000..427bc306da --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Web.Security; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Security +{ + public class BackOfficeUserStore : DisposableObject, IUserStore, IUserPasswordStore, IUserEmailStore, IUserLoginStore + { + private readonly IUserService _userService; + private readonly IExternalLoginService _externalLoginService; + private readonly MembershipProviderBase _usersMembershipProvider; + + public BackOfficeUserStore(IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase usersMembershipProvider) + { + _userService = userService; + _externalLoginService = externalLoginService; + _usersMembershipProvider = usersMembershipProvider; + if (userService == null) throw new ArgumentNullException("userService"); + if (usersMembershipProvider == null) throw new ArgumentNullException("usersMembershipProvider"); + if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); + + _userService = userService; + _usersMembershipProvider = usersMembershipProvider; + _externalLoginService = externalLoginService; + + if (_usersMembershipProvider.PasswordFormat != MembershipPasswordFormat.Hashed) + { + throw new InvalidOperationException("Cannot use ASP.Net Identity with UmbracoMembersUserStore when the password format is not Hashed"); + } + } + + /// + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// + protected override void DisposeResources() + { + throw new NotImplementedException(); + } + + /// + /// Insert a new user + /// + /// + /// + public Task CreateAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Update a user + /// + /// + /// + public Task UpdateAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Delete a user + /// + /// + /// + public Task DeleteAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Finds a user + /// + /// + /// + public Task FindByIdAsync(int userId) + { + throw new NotImplementedException(); + } + + /// + /// Find a user by name + /// + /// + /// + public Task FindByNameAsync(string userName) + { + throw new NotImplementedException(); + } + + /// + /// Set the user password hash + /// + /// + /// + public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash) + { + throw new NotImplementedException(); + } + + /// + /// Get the user password hash + /// + /// + /// + public Task GetPasswordHashAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Returns true if a user has a password set + /// + /// + /// + public Task HasPasswordAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Set the user email + /// + /// + /// + public Task SetEmailAsync(BackOfficeIdentityUser user, string email) + { + throw new NotImplementedException(); + } + + /// + /// Get the user email + /// + /// + /// + public Task GetEmailAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Returns true if the user email is confirmed + /// + /// + /// + public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Sets whether the user email is confirmed + /// + /// + /// + public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed) + { + throw new NotImplementedException(); + } + + /// + /// Returns the user associated with this email + /// + /// + /// + public Task FindByEmailAsync(string email) + { + throw new NotImplementedException(); + } + + /// + /// Adds a user login with the specified provider and key + /// + /// + /// + public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) + { + throw new NotImplementedException(); + } + + /// + /// Removes the user login with the specified combination if it exists + /// + /// + /// + public Task RemoveLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) + { + throw new NotImplementedException(); + } + + /// + /// Returns the linked accounts for this user + /// + /// + /// + public Task> GetLoginsAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Returns the user associated with this login + /// + /// + public Task FindAsync(UserLoginInfo login) + { + //get all logins associated with the login id + var result = _externalLoginService.Find(login).ToArray(); + if (result.Any()) + { + //return the first member that matches the result + var user = (from l in result + select _userService.GetUserById(l.Id) + into member + where member != null + select new BackOfficeIdentityUser + { + Email = member.Email, + Id = member.Id, + LockoutEnabled = member.IsLockedOut, + LockoutEndDateUtc = DateTime.MaxValue.ToUniversalTime(), + UserName = member.Username, + PasswordHash = GetPasswordHash(member.RawPasswordValue) + }).FirstOrDefault(); + + return Task.FromResult(AssignLoginsCallback(user)); + } + + return Task.FromResult(null); + } + + private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user) + { + if (user != null) + { + user.SetLoginsCallback(new Lazy>(() => + _externalLoginService.GetAll(user.Id))); + } + return user; + } + + private string GetPasswordHash(string storedPass) + { + return storedPass.StartsWith("___UIDEMPTYPWORD__") ? null : storedPass; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index ebcd967cc2..e39919d291 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -16,6 +16,19 @@ namespace Umbraco.Core.Security /// public abstract class MembershipProviderBase : MembershipProvider { + + public string HashPasswordForStorage(string password) + { + string salt; + var hashed = EncryptOrHashNewPassword(password, out salt); + return FormatPasswordForStorage(hashed, salt); + } + + public bool VerifyPassword(string password, string hashedPassword) + { + return CheckPassword(password, hashedPassword); + } + /// /// Providers can override this setting, default is 7 /// diff --git a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs index f59f4d1169..bdd3174960 100644 --- a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs +++ b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs @@ -13,21 +13,21 @@ using Umbraco.Core.Security; namespace Umbraco.Core.Security { - internal static class MembershipProviderExtensions + public static class MembershipProviderExtensions { - public static MembershipUserCollection FindUsersByName(this MembershipProvider provider, string usernameToMatch) + internal static MembershipUserCollection FindUsersByName(this MembershipProvider provider, string usernameToMatch) { int totalRecords = 0; return provider.FindUsersByName(usernameToMatch, 0, int.MaxValue, out totalRecords); } - public static MembershipUserCollection FindUsersByEmail(this MembershipProvider provider, string emailToMatch) + internal static MembershipUserCollection FindUsersByEmail(this MembershipProvider provider, string emailToMatch) { int totalRecords = 0; return provider.FindUsersByEmail(emailToMatch, 0, int.MaxValue, out totalRecords); } - public static MembershipUser CreateUser(this MembershipProvider provider, string username, string password, string email) + internal static MembershipUser CreateUser(this MembershipProvider provider, string username, string password, string email) { MembershipCreateStatus status; var user = provider.CreateUser(username, password, email, null, null, true, null, out status); @@ -80,7 +80,7 @@ namespace Umbraco.Core.Security /// /// /// - public static MembershipUser GetCurrentUser(this MembershipProvider membershipProvider) + internal static MembershipUser GetCurrentUser(this MembershipProvider membershipProvider) { var username = membershipProvider.GetCurrentUserName(); return username.IsNullOrWhiteSpace() @@ -93,7 +93,7 @@ namespace Umbraco.Core.Security /// /// /// - public static string GetCurrentUserName(this MembershipProvider membershipProvider) + internal static string GetCurrentUserName(this MembershipProvider membershipProvider) { if (HostingEnvironment.IsHosted) { diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index be6811ad7b..08f53fb76e 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -27,8 +27,28 @@ namespace Umbraco.Core.Security //This just creates a temp/fake ticket : base(new FormsAuthenticationTicket(userdata.Username, true, 10)) { + if (userdata == null) throw new ArgumentNullException("userdata"); UserData = userdata; - AddClaims(); + AddUserDataClaims(); + } + + /// + /// Create a back office identity based on an existing claims identity + /// + /// + /// + public UmbracoBackOfficeIdentity(ClaimsIdentity claimsIdentity, UserData userdata) + //This just creates a temp/fake ticket + : base(new FormsAuthenticationTicket(userdata.Username, true, 10)) + { + if (claimsIdentity == null) throw new ArgumentNullException("claimsIdentity"); + if (userdata == null) throw new ArgumentNullException("userdata"); + + _currentIssuer = claimsIdentity.AuthenticationType; + UserData = userdata; + AddClaims(claimsIdentity); + Actor = claimsIdentity; + AddUserDataClaims(); } /// @@ -39,7 +59,7 @@ namespace Umbraco.Core.Security : base(ticket) { UserData = JsonConvert.DeserializeObject(ticket.UserData); - AddClaims(); + AddUserDataClaims(); } /// @@ -49,24 +69,49 @@ namespace Umbraco.Core.Security private UmbracoBackOfficeIdentity(UmbracoBackOfficeIdentity identity) : base(identity) { + if (identity.Actor != null) + { + _currentIssuer = identity.AuthenticationType; + AddClaims(identity); + Actor = identity.Clone(); + } + UserData = identity.UserData; - AddClaims(); + AddUserDataClaims(); } - public static string Issuer = "UmbracoBackOffice"; + public const string Issuer = "UmbracoBackOffice"; + private readonly string _currentIssuer = Issuer; - //TODO: Another option is to create a ClaimsIdentityFactory when everything is wired up... optional though i think - private void AddClaims() + private void AddClaims(ClaimsIdentity claimsIdentity) + { + foreach (var claim in claimsIdentity.Claims) + { + AddClaim(claim); + } + } + + /// + /// Adds claims based on the UserData data + /// + private void AddUserDataClaims() { AddClaims(new[] { new Claim(Constants.Security.StartContentNodeIdClaimType, StartContentNode.ToInvariantString(), null, Issuer, Issuer, this), new Claim(Constants.Security.StartMediaNodeIdClaimType, StartMediaNode.ToInvariantString(), null, Issuer, Issuer, this), new Claim(Constants.Security.AllowedApplicationsClaimType, string.Join(",", AllowedApplications), null, Issuer, Issuer, this), + + //TODO: Similar one created by the ClaimsIdentityFactory not sure we need this new Claim(Constants.Security.UserIdClaimType, Id.ToString(), null, Issuer, Issuer, this), new Claim(Constants.Security.CultureClaimType, Culture, null, Issuer, Issuer, this), new Claim(Constants.Security.SessionIdClaimType, SessionId, null, Issuer, Issuer, this), - new Claim(ClaimTypes.Role, string.Join(",", Roles), null, Issuer, Issuer, this) + + //TODO: Role claims are added by the default ClaimsIdentityFactory based on the result from + // the user manager manager.GetRolesAsync method so not sure if we can do that there or needs to be done here + // and each role should be a different claim, not a single string + + //new Claim(ClaimTypes.Role, string.Join(",", Roles), null, Issuer, Issuer, this) }); } @@ -80,7 +125,7 @@ namespace Umbraco.Core.Security /// public override string AuthenticationType { - get { return Issuer; } + get { return _currentIssuer; } } public int StartContentNode diff --git a/src/Umbraco.Core/Security/UmbracoMembershipProviderBase.cs b/src/Umbraco.Core/Security/UmbracoMembershipProviderBase.cs index 12ca2b4b4a..328d8ad335 100644 --- a/src/Umbraco.Core/Security/UmbracoMembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/UmbracoMembershipProviderBase.cs @@ -8,6 +8,8 @@ namespace Umbraco.Core.Security /// public abstract class UmbracoMembershipProviderBase : MembershipProviderBase { + + public abstract string DefaultMemberTypeAlias { get; } /// diff --git a/src/Umbraco.Core/Services/ExternalLoginService.cs b/src/Umbraco.Core/Services/ExternalLoginService.cs new file mode 100644 index 0000000000..f33f1c492b --- /dev/null +++ b/src/Umbraco.Core/Services/ExternalLoginService.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Services +{ + public class ExternalLoginService : RepositoryService, IExternalLoginService + { + public ExternalLoginService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger) + : base(provider, repositoryFactory, logger) + { + } + + /// + /// Returns all user logins assigned + /// + /// + /// + public IEnumerable GetAll(int userId) + { + using (var repo = RepositoryFactory.CreateExternalLoginRepository(UowProvider.GetUnitOfWork())) + { + return repo.GetByQuery(new Query().Where(x => x.UserId == userId)); + } + } + + /// + /// Returns all logins matching the login info - generally there should only be one but in some cases + /// there might be more than one depending on if an adminstrator has been editing/removing members + /// + /// + /// + public IEnumerable Find(UserLoginInfo login) + { + using (var repo = RepositoryFactory.CreateExternalLoginRepository(UowProvider.GetUnitOfWork())) + { + return repo.GetByQuery(new Query() + .Where(x => x.ProviderKey == login.ProviderKey && x.LoginProvider == login.LoginProvider)); + } + } + + /// + /// Save user logins + /// + /// + /// + public void SaveUserLogins(int userId, IEnumerable logins) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateExternalLoginRepository(uow)) + { + repo.SaveUserLogins(userId, logins); + uow.Commit(); + } + } + + /// + /// Deletes all user logins - normally used when a member is deleted + /// + /// + public void DeleteUserLogins(int userId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateExternalLoginRepository(uow)) + { + repo.DeleteUserLogins(userId); + uow.Commit(); + } + } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IExternalLoginService.cs b/src/Umbraco.Core/Services/IExternalLoginService.cs new file mode 100644 index 0000000000..e1b1a161d8 --- /dev/null +++ b/src/Umbraco.Core/Services/IExternalLoginService.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Services +{ + /// + /// Used to store the external login info, this can be replaced with your own implementation + /// + public interface IExternalLoginService : IService + { + /// + /// Returns all user logins assigned + /// + /// + /// + IEnumerable GetAll(int userId); + + /// + /// Returns all logins matching the login info - generally there should only be one but in some cases + /// there might be more than one depending on if an adminstrator has been editing/removing members + /// + /// + /// + IEnumerable Find(UserLoginInfo login); + + /// + /// Save user logins + /// + /// + /// + void SaveUserLogins(int userId, IEnumerable logins); + + /// + /// Deletes all user logins - normally used when a member is deleted + /// + /// + void DeleteUserLogins(int userId); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index 484226ae78..7803abe98e 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Umbraco.Core.Models; diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index f1eb873db7..d72115e49d 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -41,6 +41,7 @@ namespace Umbraco.Core.Services private Lazy _memberTypeService; private Lazy _memberGroupService; private Lazy _notificationService; + private Lazy _externalLoginService; /// /// public ctor - will generally just be used for unit testing all items are optional and if not specified, the defaults will be used @@ -91,8 +92,10 @@ namespace Umbraco.Core.Services IDomainService domainService = null, ITaskService taskService = null, IMacroService macroService = null, - IPublicAccessService publicAccessService = null) + IPublicAccessService publicAccessService = null, + IExternalLoginService externalLoginService = null) { + if (externalLoginService != null) _externalLoginService = new Lazy(() => externalLoginService); if (auditService != null) _auditService = new Lazy(() => auditService); if (localizedTextService != null) _localizedTextService = new Lazy(() => localizedTextService); if (tagService != null) _tagService = new Lazy(() => tagService); @@ -145,6 +148,9 @@ namespace Umbraco.Core.Services var provider = dbUnitOfWorkProvider; var fileProvider = fileUnitOfWorkProvider; + if (_externalLoginService == null) + _externalLoginService = new Lazy(() => new ExternalLoginService(provider, repositoryFactory, logger)); + if (_publicAccessService == null) _publicAccessService = new Lazy(() => new PublicAccessService(provider, repositoryFactory, logger)); @@ -415,5 +421,9 @@ namespace Umbraco.Core.Services get { return _memberGroupService.Value; } } + public IExternalLoginService ExternalLoginService + { + get { return _externalLoginService.Value; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index f0f09c499c..f76cb2667c 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -67,12 +67,13 @@ False ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll - - ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + + False + ..\packages\Microsoft.Owin.Security.Cookies.3.0.0\lib\net45\Microsoft.Owin.Security.Cookies.dll - - ..\packages\Microsoft.Owin.Security.OAuth.2.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll - True + + False + ..\packages\Microsoft.Owin.Security.OAuth.3.0.0\lib\net45\Microsoft.Owin.Security.OAuth.dll True @@ -347,6 +348,8 @@ + + @@ -368,11 +371,13 @@ + + @@ -400,8 +405,10 @@ + + @@ -409,7 +416,9 @@ + + @@ -420,8 +429,10 @@ + + diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index 93c9737158..e8702a60bc 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -14,8 +14,8 @@ - - + + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 00d3cda1ab..04e6672b4b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -17,9 +17,8 @@ $scope.errorMsg = ""; - $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; - - $scope.externalLogins = Umbraco.Sys.ServerVariables.externalLogins; + $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; + $scope.externalLoginProviders = Umbraco.Sys.ServerVariables.externalLogins.providers; $scope.loginSubmit = function (login, password) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 5b73743772..b09c11bf18 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -23,12 +23,12 @@
{{errorMsg}}
- +

-
+