web-dev-qa-db-fra.com

Identité personnalisée à l'aide de MVC5 et OWIN

J'essaie d'ajouter des propriétés personnalisées à ApplicationUser pour un site Web à l'aide de l'authentification MVC5 et OWIN. J'ai lu https://stackoverflow.com/a/10524305/264607 et j'apprécie son intégration au contrôleur de base pour un accès facile aux nouvelles propriétés. Mon problème est que lorsque je définis la propriété HTTPContext.Current.User sur mon nouvel IPrincipal, une erreur de référence NULL apparaît:

[NullReferenceException: Object reference not set to an instance of an object.]
   System.Web.Security.UrlAuthorizationModule.OnEnter(Object source, EventArgs eventArgs) +127
   System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +136
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +69

Voici mon code:

    protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
    {
        if (HttpContext.Current.User.Identity.IsAuthenticated)
        {
            userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));

            ApplicationUser user = userManager.FindByName(HttpContext.Current.User.Identity.Name);

            PatientPortalPrincipal newUser = new PatientPortalPrincipal();
            newUser.BirthDate = user.BirthDate;
            newUser.InvitationCode = user.InvitationCode;
            newUser.PatientNumber = user.PatientNumber;

            //Claim cPatient = new Claim(typeof(PatientPortalPrincipal).ToString(), );

            HttpContext.Current.User = newUser;
        }
    }

public class PatientPortalPrincipal : ClaimsPrincipal, IPatientPortalPrincipal
{
    public PatientPortalPrincipal(ApplicationUser user)
    {
        Identity = new GenericIdentity(user.UserName);
        BirthDate = user.BirthDate;
        InvitationCode = user.InvitationCode;
    }

    public PatientPortalPrincipal() { }

    public new bool IsInRole(string role)
    {
        if(!string.IsNullOrWhiteSpace(role))
            return Role.ToString().Equals(role);

        return false;
    }

    public new IIdentity Identity { get; private set; }
    public WindowsBuiltInRole Role { get; set; }
    public DateTime BirthDate { get; set; }
    public string InvitationCode { get; set; }
    public string PatientNumber { get; set; }
}

public interface IPatientPortalPrincipal : IPrincipal
{

    WindowsBuiltInRole Role { get; set; }
    DateTime BirthDate { get; set; }
    string InvitationCode { get; set; }
    string PatientNumber { get; set; }
}

Je n'ai pas trouvé beaucoup de documentation sur la façon de procéder, j'ai lu ces articles:

http://blogs.msdn.com/b/webdev/archive/2013/10/16/customizing-profile-information-in-asp-net-identity-in-vs-2013-templates.aspx

http://blogs.msdn.com/b/webdev/archive/2013/07/03/understanding-owin-forms-authentication-in-mvc-5.aspx

Les commentaires du deuxième lien m'indiquaient peut-être d'utiliser des revendications ( http://msdn.Microsoft.com/en-us/library/ms734687.aspx?cs-save-lang=1&cs-lang=csharp ), mais l'article lié à ne montre pas comment les ajouter à une IPrincipal (qui est ce que HttpContext.Current.User est), ni où dans le pipeline vous êtes censés les ajouter à une ClaimsIdentity (qui est la classe concrète que User est). Je suis plutôt enclin à utiliser des revendications, mais je dois savoir où ajouter ces nouvelles revendications à l'utilisateur. 

Même si les revendications sont la voie à suivre, je suis curieux de savoir ce que je fais de mal avec mon IPrincipal personnalisé, car il me semble avoir mis en œuvre tout ce qu’il exige.

20
BlackICE

Je peux obtenir quelque chose à l'aide de la sécurité basée sur Claims, donc si vous cherchez à faire quelque chose rapidement, voici ce que j'ai pour le moment:

Dans le processus de connexion de la méthode AccountController (la mienne est dans la méthode SignInAsync), ajoutez une nouvelle revendication à l'identité créée par UserManager:

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
    var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
    identity.AddClaim(new Claim("PatientNumber", user.PatientNumber)); //This is what I added
    AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

Ensuite, dans mes classes de contrôleur de base, j'ai simplement ajouté une propriété:

private string _patientNumber;
public string PatientNumber
{
    get
    {
        if (string.IsNullOrWhiteSpace(_patientNumber))
        {
            try
            {
                var cp = ClaimsPrincipal.Current.Identities.First();
                var patientNumber = cp.Claims.First(c => c.Type == "PatientNumber").Value;
                _patientNumber = patientNumber;
            }
            catch (Exception)
            {
            }
        }
        return _patientNumber;
    }
}

Ce lien vous a été utile pour connaître les revendications: http://msdn.Microsoft.com/en-us/library/ms734687.aspx?cs-save-lang=1&cs-lang=csharp#code-snippet-1


Mise à jour pour le problème avec IPrincipal

Je l'ai retrouvé dans la propriété Identity. Le problème était que je fournissais un constructeur par défaut sur la classe PatientPortalPrincipal qui ne définissait pas la propriété Identity. Ce que j'ai fini par faire a été de supprimer le constructeur par défaut et d’appeler le constructeur approprié à partir de Application_PostAuthenticateRequest, le code mis à jour se trouve ci-dessous

protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
    if (HttpContext.Current.User.Identity.IsAuthenticated)
    {
        userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));

        ApplicationUser user = userManager.FindByName(HttpContext.Current.User.Identity.Name);

        PatientPortalPrincipal newUser = new PatientPortalPrincipal(user);
        newUser.BirthDate = user.BirthDate;
        newUser.InvitationCode = user.InvitationCode;
        newUser.PatientNumber = user.PatientNumber;

        //Claim cPatient = new Claim(typeof(PatientPortalPrincipal).ToString(), );

        HttpContext.Current.User = newUser;
    }
}

Cela fait fonctionner le tout!

17
BlackICE

Vous obtenez une exception car HttpContext.Current.User.Identity.IsAuthenticated renvoie false au moment de la vérification (de même que HttpContext.Current.Request.IsAuthenticated).

Si vous supprimez l'instruction if (HttpContext.Current.User.Identity.IsAuthenticated), cela fonctionnera correctement (au moins cette partie du code).

J'ai essayé une chose simple comme celle-ci:

BaseController.cs

public abstract class BaseController : Controller
{
    protected virtual new CustomPrincipal User
    {
        get { return HttpContext.User as CustomPrincipal; }
    }
}

CustomPrincipal.cs

public class CustomPrincipal : IPrincipal
{
    public IIdentity Identity { get; private set; }
    public bool IsInRole(string role) { return false; }

    public CustomPrincipal(string username)
    {
        this.Identity = new GenericIdentity(username);
    }

    public DateTime BirthDate { get; set; }
    public string InvitationCode { get; set; }
    public int PatientNumber { get; set; }
}

Global.asax.cs

protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
     CustomPrincipal customUser = new CustomPrincipal(User.Identity.Name);

     customUser.BirthDate = DateTime.Now;
     customUser.InvitationCode = "1234567890A";
     customUser.PatientNumber = 100;

     HttpContext.Current.User = customUser;
}

HomeController.cs

public ActionResult Index()
{
    ViewBag.BirthDate = User.BirthDate;
    ViewBag.InvitationCode = User.InvitationCode;
    ViewBag.PatientNumber = User.PatientNumber;

    return View();
}

Et cela fonctionne bien. Donc à moins que ce code:

userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));

ApplicationUser user = userManager.FindByName(HttpContext.Current.User.Identity.Name);

ne renvoie pas d'objet utilisateur valide (personnalisé), le problème vient de l'instruction if().

Votre mise à jour semble bonne et si vous êtes prêt à stocker des données sous forme de revendications dans un cookie, vous pouvez les utiliser, bien que je déteste personnellement le bloc try {} catch. 

Ce que je fais à la place est ceci:

BaseController.cs

[AuthorizeEx]
public abstract partial class BaseController : Controller
{
    public IOwinContext OwinContext
    {
        get { return HttpContext.GetOwinContext(); }
    }

    public new ClaimsPrincipal User
    {
        get { return base.User as ClaimsPrincipal; }
    }

    public WorkContext WorkContext { get; set; }
}

Je décore la classe de contrôleur de base avec un attribut personnalisé.

AuthorizeExAttribute.cs:

public class AuthorizeExAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        Ensure.Argument.NotNull(filterContext);

        base.OnAuthorization(filterContext);

        IPrincipal user = filterContext.HttpContext.User;
        if (user.Identity.IsAuthenticated)
        {
            var ctrl = filterContext.Controller as BaseController;
            ctrl.WorkContext = new WorkContext(user.Identity.Name);
        }
    }
}

Et WorkContext.cs:

public class WorkContext
{
    private string _email;

    private Lazy<User> currentUser;

    private IAuthenticationService authService;
    private ICacheManager cacheManager;

    public User CurrentUser
    {
        get 
        { 
            var cachedUser = cacheManager.Get<User>(Constants.CacheUserKeyPrefix + this._email);
            if (cachedUser != null)
            {
                return cachedUser;
            }
            else
            {
                var user = currentUser.Value;

                cacheManager.Set(Constants.CacheUserKeyPrefix + this._email, user, 30);

                return user;
            }
        }
    }

    public WorkContext(string email)
    {
        Ensure.Argument.NotNullOrEmpty(email);

        this._email = email;

        this.authService = DependencyResolver.Current.GetService<IAuthenticationService>();
        this.cacheManager = DependencyResolver.Current.GetService<ICacheManager>();

        this.currentUser = new Lazy<User>(() => authService.GetUserByEmail(email));
    }

J'accède ensuite au WorkContext comme ceci:

public class DashboardController : BaseController
{
    public ActionResult Index()
    {
        ViewBag.User = WorkContext.CurrentUser;

        return View();
    }
}

J'utilise Ninject's Dependency Resolver pour résoudre authService et cacheManager, mais vous pouvez ignorer la mise en cache et remplacer authService avec l'identité ASP.NET UserManager je crois.

Je voulais aussi rendre hommage au projet car la classe WorkContext est fortement inspirée du projet NugetGallery.

4
LukeP

Je parie HttpContext.Current.User est null. Donc au lieu de cela:

if (HttpContext.Current.User.Identity.IsAuthenticated)

vous pouvez essayer ceci:

if (HttpContext.Current.Request.IsAuthenticated)
3
Brock Allen

J'ai eu la même erreur. 

Mon problème était qu'avec les utilisateurs anonymes je ne mettais pas la IIdentity sur IPrincipal. Je l'ai fait uniquement lorsque les utilisateurs se sont connectés avec leur nom d'utilisateur. Sinon, l'identité était nulle.

Ma solution était de toujours définir IIdentity. Si l'utilisateur n'est pas authentifié (utilisateur anonyme), IIdentity.IsAuthenticated est défini sur false. Sinon, c'est vrai.

Mon code:

private PrincipalCustom SetPrincipalIPAndBrowser()
{
     return new PrincipalCustom
     {
       IP = RequestHelper.GetIPFromCurrentRequest(HttpContext.Current.Request),
       Browser = RequestHelper.GetBrowserFromCurrentRequest(HttpContext.Current.Request),

    /* User is not authenticated, but Identity must be set anyway. If not, error occurs */
       Identity = new IdentityCustom { IsAuthenticated = false }
     };
}
0
FrenkyB