An unhandled exception of System.InvalidOperationException breaks my MVC App?

2.4k views Asked by At

I am taking the concept of building a OWIN login from a empty MVC and I am just starting to add the part with using my database to login the user after creating a Identity claim to put in the URL.

This is my code to create the claim to login the user

public class AuthenticationController : Controller
{
    IAuthenticationManager Authentication
    {
        get { return HttpContext.GetOwinContext().Authentication; }
    }

    [GET("Login")]
    public ActionResult Show()
    {
        return View();
    }

    [POST("Login")]
    [ValidateAntiForgeryToken]
    public ActionResult Login(LoginModel input)
    {
        if (ModelState.IsValid)
        {
            if (input.HasValidUsernameAndPassword())
            {
                var identity = new ClaimsIdentity(new[] 
                {
                        new Claim(ClaimTypes.Name, input.Username),
                },
                DefaultAuthenticationTypes.ApplicationCookie,
                ClaimTypes.Name, ClaimTypes.Role);

                // if you want roles, just add as many as you want here (for loop maybe?)
                if (input.isAdministrator)
                {
                    identity.AddClaim(new Claim(ClaimTypes.Role, "Admins"));
                }
                else
                {
                    identity.AddClaim(new Claim(ClaimTypes.Role, "User"));
                }
                // tell OWIN the identity provider, optional
                // identity.AddClaim(new Claim(IdentityProvider, "Simplest Auth"));

                Authentication.SignIn(new AuthenticationProperties
                {
                    IsPersistent = input.RememberMe
                }, identity);

                return RedirectToAction("show", "authentication");
            }
        }
        return View("show", input);
    }

    [GET("logout")]
    public ActionResult Logout()
    {
        Authentication.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
        return RedirectToAction("Login");
    }
}

Here is my code for the Show.cshtml

@using (Html.BeginForm())
{
@Html.AntiForgeryToken()

<div class="form-horizontal">
    <h4>Details</h4>
    <hr />
    @Html.ValidationSummary(true)

    <div class="form-group">
        @Html.LabelFor(model => model.Username, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.Username)
            @Html.ValidationMessageFor(model => model.Username)
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.Password, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.Password)
            @Html.ValidationMessageFor(model => model.Password)
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.RememberMe, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.RememberMe)
            @Html.ValidationMessageFor(model => model.RememberMe)
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Login" class="btn btn-default" />
        </div>
    </div>
</div>
}

This is my code where I call my database and store my data for the logged in user

public class LoginModel
{
    [Required]
    public string Username { get; set; }
    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    public bool RememberMe { get; set; }

    public bool isAdministrator { get; set; }

    public bool HasValidUsernameAndPassword()
    {
        bool result = false;
        int countChecker = 0;
        using (SqlConnection connection = new SqlConnection("server=server; database=db; user id=user; password=user"))
        {
            connection.Open();
            using (SqlCommand command = new SqlCommand("SELECT count(*) FROM ACCOUNTS WHERE active = 1 AND UserName = @param1 AND PasswordField = @param2", connection))
            {
                command.Parameters.Clear();
                command.Parameters.AddWithValue("@param1", Username);
                command.Parameters.AddWithValue("@param2", Password);
                SqlDataReader reader = command.ExecuteReader();
                while (reader.Read())
                {
                    for (int i = 0; i < reader.FieldCount; i++)
                    {
                        countChecker = Convert.ToInt32(reader[i].ToString());

                    }
                }
                if (countChecker > 0)
                {
                    result = true;
                    using (SqlConnection connect = new SqlConnection("server=server; database=db; user id=user; password=user"))
                    {
                        connect.Open();
                        using (SqlCommand com = new SqlCommand("SELECT Administrator FROM ACCOUNTS WHERE active = 1 AND UserName = @param1 AND PasswordField = @param2", connect))
                        {
                            com.Parameters.Clear();
                            com.Parameters.AddWithValue("@param1", Username);
                            com.Parameters.AddWithValue("@param2", Password);
                            SqlDataReader read = com.ExecuteReader();
                            while (read.Read())
                            {
                                for (int i = 0; i < read.FieldCount; i++)
                                {
                                    isAdministrator = Convert.ToBoolean(read[i].ToString());
                                }
                            }
                        }
                        connect.Dispose();
                        connect.Close();
                    }
                }
                else
                {
                    result = false;
                }
            }
            connection.Dispose();
            connection.Close();
        }
        return result;
    }
}

My Login(LoginModel input) completes and returns to Show.cshtml and then has a exception right here @Html.AntiForgeryToken(). The exception I get is:

A claim of type 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier' or 'http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider' was not present on the provided ClaimsIdentity. To enable anti-forgery token support with claims-based authentication, please verify that the configured claims provider is providing both of these claims on the ClaimsIdentity instances it generates. If the configured claims provider instead uses a different claim type as a unique identifier, it can be configured by setting the static property AntiForgeryConfig.UniqueClaimTypeIdentifier.

I am new to working with MVCs and I can't tell which one of the two is missing from ActionResult Login(LoginModel input). Can someone please explain to me which one I am missing.

I added the suggestion from Corey to my Global.asax:

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
        AttributeRoutingConfig.RegisterRoutes(RouteTable.Routes);
    }
}

Now I get this:

Additional information: A claim of type 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier' was not present on the provided ClaimsIdentity.

I changed the AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier; to AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name; and I don't get any more exceptions. Would this be the correct syntax?

1

There are 1 answers

3
Cory Charlton On BEST ANSWER

This article might help:

Unfortunately this error is a little confusing because it says “nameidentifier or identityprovider” even though you may have one of the two. By default you need both.

Anyway, if you’re not using ACS as your STS then the above error pretty much tells you what’s needed to solve the problem. You need to tell MVC which claim you want to use to uniquely identify the user. You do this by setting the AntiForgeryConfig.UniqueClaimTypeIdentifier property (typically in App_Start in global.asax). For example (assuming you want to use nameidentifier as the unique claim):

protected void Application_Start()
{
    ...

    AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
}