Adding authentication in AspNetCore 2.0

<EDIT> A few days ago they changed it again….
Instead of .AddCookieAuthentication(….
It’s now just .AddCookie();
</EDIT>

Or rather Aspnetcore 2.0.0-preview2-006497 since they changed it again….

First, download the latest bits from .NET Core 2.0 and install it.
Open a developer command prompt and check version with

dotnet –version

It should say 2.0.0-preview2-006497 for you to be sure that my instructions will work 🙂
Create a new folder for your project and create a new mvc project with

dotnet new mvc

After it is done we will add the dependencies for authentication

dotnet add package Microsoft.AspNetCore.Authentication -v “2.0.0-preview2-final”

and

dotnet add package Microsoft.AspNetCore.Http -v “2.0.0-preview2-final”

Now we add, one by one the authentication providers we want. In Startup.cs in ConfigureServices:


services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookieAuthentication(CookieAuthenticationDefaults.AuthenticationScheme, option =>
{
option.LoginPath = "/home/login";
})
.AddTwitterAuthentication(o =>
{
o.ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"];
o.ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"];
});

view raw

add-auth.cs

hosted with ❤ by GitHub

The biggest change in this version is perhaps that you only add

app.UseAuthentication();

To the pipeline in the Configure method.

So. Done with that part. Oh, forgot the usings. Add these to the top


using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.Mvc;

view raw

using.cs

hosted with ❤ by GitHub

A few more than necessary but I will get to them. Now the project should start with

dotnet run

But it still allows for anonymous access.

Add the attribute [Authorize] to your HomeController together with a matching using like so:


using Microsoft.AspNetCore.Authorization;
namespace [namespace].Controllers
{
[Authorize]
public class HomeController : Controller
{

view raw

controller1.cs

hosted with ❤ by GitHub

So. Again start the project and browse to http://localhost:5000/. You will be redirected to /home/login and get an error since that page does not exist (yet).

In the HomeController.cs add this code


[AllowAnonymous]
public async Task<IActionResult> Login(string username, string password)
{
if (IsValidUser(username, password))
{
var claims = new List<Claim>(2);
claims.Add(new Claim(ClaimTypes.Name, username));
claims.Add(new Claim(ClaimTypes.Role, "GroupThatUserIsIn",
ClaimValueTypes.String, "IHaveIssuedThis"));
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(new ClaimsIdentity(claims,
CookieAuthenticationDefaults.AuthenticationScheme)));
return RedirectToAction("Index");
}
return View();
}
private bool IsValidUser(string username, string password)
{
return username == "foo" && password == "bar";
}

This is used when you manage all the users and passwords yourself (please don’t).
But seriously, sometimes you have an old back-end system that you are building a new front-web for and it has all the user info.

I created a super simplistic view for this action. Create a new file in the folder Views/Home called Login.cshtml with this content


<form action="/home/login" method="post">
<input name="username" />
<input name="password" type="password"/>
<input type="submit" value="Go"/>
</form>
<a href='/login-twitter' \">I prefer Twitter</a>

view raw

Login.cshtml

hosted with ❤ by GitHub

Told you. Simplistic. Make sure these “usings” are in place in your HomeController.cs


using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication;

Now, go back up to the project root folder and run the application again. This time the login page is displayed. If you try to login with your hard coded username and password you will be logged in and redirected to /. If you inspect the ClaimsPrincipal when debugging you will see that your claims are visible under the Identity-property.

cookieinfo

Great. Let the user logout as well.


public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Index");
}

Add a link somewhere that redirects the user to /home/logout. Done.

So. How about Twitter?

First, add a new app at apps.twitter.com . Click “Create new app” and fill out the form. You can set callback url to localhost:5000. Go to the Keys and Access Tokens-tab and copy them to your appSettings.json file


},
"Authentication": {
"Twitter": {
"ConsumerKey": "<Your key here>",
"ConsumerSecret": "<Your secret here>"
}
}
}

Please note that if you intend to publish the code somewhere, don’t store these credentials here. Use secrets instead.

Remember the fancy looking login page? The Twitter-link was /login-twitter. Just because it is fun I will hard-wire this url into the processing pipeline.

So. Head over to Startup.cs and paste this code into the Configure-method.


app.Map("/login-twitter", login =>
{
login.Run(async context =>
{
await context.ChallengeAsync("Twitter", new AuthenticationProperties() {
RedirectUri = "/" });
return;
});
});

view raw

Startup.cs

hosted with ❤ by GitHub

Now you redirected to a Twitter page and depending if you are already logged into Twitter or not the page either asks you to logon or only to authorize your new app to connect to Twitter.

Tip: If you want to dress the current user with more claims than Twitter sent, you can always add them. Like your internal user id of that Twitter-identified user.


public async Task<IActionResult> Index()
{
var principal = User.Identity as ClaimsIdentity;
var idClaim = principal.Claims.Where(i => i.Type == "https://marcusclasson.com/claims/id")
.SingleOrDefault();
if(idClaim == null)
{
principal.AddClaim(new Claim("https://marcusclasson.com/claims/id", "MyCustomId"));
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(User);
}
return View();
}

Code on Github

Done.

Advertisement

The pitfalls of LINQ deferred execution

Let’s face it, we all love the simplicity of Linq. The fluent syntax, the easy to read – almost sql-like – syntax. However, there are som pitfalls that I’ve seen colleagues fall into unknowingly. One of them is what is called  the deferred execution.

By design, you don’t execute a Linq command, you only specify it. The execution is not performed until the result is required. Hence deferred.

 

Take a look at the following code


//Prepare test data. Could be a set returned
//from a database query
var list = new List<int>();
for (int i = 0; i < 1000000; i++)
{
list.Add(i);
}
//Filter out a small subset of the data [zip code, annual income]
var listSmall = list.Where(i => i > 100000 && i < 150000);
//Now use the small subset and loop through it
//E.g. examine the first 1000 rows
var result = new List<int>();
for (int i = 0; i < 1000; i++)
{
int _i = listSmall.Where(o => o == 100100 + i).Single();
result.Add(_i);
}

view raw

gistfile1.cs

hosted with ❤ by GitHub

Albeit a bit contrieved it is not an unusual pattern. I have a large list that i narrow down to a subset that I would like to work on (zip codes, gender) and then I examine this subset by looping through it.

On my machine this took 15000 ms (I’ve removed the Stopwatch stuff for clarity). This is not reasonable even though we have 1,000,000 records.

 

The reason is that listSmall is not a list (yet)! It is just a defined query. So, every time we execute

listSmall.Where(o => o == 100100 + i).Single()

we are, in fact, executing

list.Where(i => i > 100000 && i < 150000).Where(o => o == 100100 + i).Single()

So, instead of going through 50,000 records a 1000 times, we are searching 1,000,000 records! Not what we intended indeed. The way to solve this is to force Linq to execute the initial filter. The easiest way to do this is to simply append ToList() at the end. Like so:

var listSmall = list.Where(i => i > 100000 && i < 150000).ToList();

Now, the code runs in 400 ms. That’s what I call improvement.

 

Another scenario that has it cause in the same Linq feature. Inspect the following code


//Prepare test data. Could be a set returned
//from a database query
var list = new List<int>();
for (int i = 0; i < 1000000; i++)
{
list.Add(i);
}
//Prepare a list to hold the results
//E.g list of all users born a certain year
var listOfInts = new List<IEnumerable<int>>();
for (int i = 0; i < 10; i++)
{
//Select from the large list all users
//that satisfy the criteria
listOfInts.Add(list.Where(a => a == i));
}
//Now, loop through all years and select the
//first user for every year
foreach(var l in listOfInts)
{
Console.WriteLine(l.First());
}

view raw

gistfile1.cs

hosted with ❤ by GitHub

Here we have a recordset that contains a lot of users and I want to select and group all users from specific years. I loop through the set and select the users. I then store the result in an array. Imagine my surprise when I later loop through my yearbook and see that all users are from the same year. What happended?

Well, since I didn’t, actually, retreive the users in the first loop but only specified the query, when I finally did execute the query the loop is done and i == 0. Through closure, i is visible to my query snippet and is used to select users but – by then – it is 10.

 

The solution is once again to force execution by appending ToList() to the where statement at row 15.

Happy LINQing.