Understanding how .NET is manipulating the concept of identity can be very harsh and confusing, especially for beginners, when jumping right to the docs to get started.
In this post, we will try to understand the underlying concepts on which the identity is built with a simple example that we will build step by step, let's get started!
If you already are familiar with the basics and want to deep dive into ASP.NET security, I strongly recommend you to have a look at this awesome post on Reddit about it
Claim
- Identity's most atomic type
A bit of theory
Claims are the simplest entity that is used for identity, they simply are a wrapper for a key and a value according to an issuer, which all of them are strings.
Each of those parts is carrying a certain meaning.
The key is the type of the value held by the claim. For example, if we want the claim to wrap the name of the user, this key could be "name"
. Since this field is just a string, you can put whatever value you want in it.
The value is simply the value of the claim (duh). This field is also a string and as such all other types will have to be serialized.
The issuer is the authority, the system which has delivered this claim. For example, if you are using GitHub to login, GitHub will emit a claim indicating your identifier. Similarly, your system can deliver its own claim.
Another way of seeing a claim is to consider it as a statement. Here are some examples:
- Alice has an id of 123 according to GitHub
- The current user's name is Bob according to the system
A special claim
Despite the claim structure being fairly simple and permissive, there is one claim that is really vital: the subject id.
The subject id is a claim representing the user. It must respect three requirements:
- Be unique - two users of the same system should not be identified by the same identifier
- Be immutable - if a user's identifier changes over time things can go south pretty quickly
- Should not be reused - otherwise you could end up with a new user also being identified as a former one which should have been deactivated
The type for the claim is commonly abbreviated sub
As an example, the name of a user is not a good subject id: if two users are named "Bob", it will break the first requirement.
On the other hand, a GUID can be a good one.
Building our first claims
To create a claim, we just have to provide a key and a value:
var subjectId = new Claim("sub", "E4WlUXrJgy4");
var name = new Claim("name", "Alice"),
var role = new Claim("role", "Admin"),
For complex types, the the ValueType parameter can be used to help for the deserialization
ClaimsIdentity
- Creating the identity
If you grasp what claims represent, the rest of the post should be pretty simple.
The ClaimsIdentity
type is only a wrapper for a bunch of claims, which make sense: the identity of a person is based on several statements about him.
As an example, Alice's identity is the grouping of some things about her:
- According to the government, her name is Alice
- According to the government, her role in the country is citizen
- According to the government, her height is 1m and 63cm
Creating the Alice's identity
In .NET, it will be pretty much the same thing:
// Statements about Alice
var claims = new List<Claim>
{
new Claim("sub", "E4WlUXrJgy4"),
new Claim("name", "Alice"),
new Claim("role", "citizen"),
new Claim("height", "1m63cm"),
};
// Alice's identity according to those statements
var aliceIdentity = new ClaimsIdentity(claims);
And voilà, we materialized Alice's identity. With it, we can get some information about her:
Console.WriteLine(aliceIdentity.IsAuthenticated);
// False
Console.WriteLine(aliceIdentity.Name);
// null
Wait, we did not specified that at all!
Since we created her identity and specified her name, she should be authenticated and have "Alice" as her name.
There are two issues here:
- Our identity is not aware that Alice is authenticated
- Out identity cannot correctly identify Alice's name
Fixing IsAuthenticated
If we take a closer look at the available constructors, we can wee that it is possible to provide a string representing the authentication type.
Since we did not, it was set to null
by default and our identity assumed that, if the user has not used any authentication method, he cannot be authenticated.
Let's add the authentication method:
- var aliceIdentity = new ClaimsIdentity(claims);
+ var aliceIdentity = new ClaimsIdentity(claims, "pwd");
We can now test if it worked:
Console.WriteLine(aliceIdentity.IsAuthenticated);
// True
Done, let's fix the other issue
Retrieving her name
When we were looking at the constructors, you might have noticed that some of them had a nameType
parameter.
In fact, if we do not specify the name of the types used for the name and the role of the user, it will be defaulted to some XML value that does not look like "name"
at all:
Let's specify both those types and see if it has fixed our problem:
- var aliceIdentity = new ClaimsIdentity(claims, "pwd");
+ var aliceIdentity = new ClaimsIdentity(claims, "pwd", "name", "role");
We can now test if it worked:
Console.WriteLine(aliceIdentity.Name);
// Alice
Yay!
ClaimsPrincipal
Since we have our identity, we can now create what .NET is using to authenticate our user: the ClaimsPrincipal
.
Using Alice's identity, doing so is pretty straightforward:
var claims = new List<Claim>
{
new Claim("sub", "E4WlUXrJgy4"),
new Claim("name", "Alice"),
new Claim("role", "citizen"),
new Claim("height", "1m63"),
};
var aliceIdentity = new ClaimsIdentity(claims, "pwd", "name", "role");
var principal = new ClaimsPrincipal(aliceIdentity);
This is this object that is exposed by
HttpContext.Current.User
Based on this, we can assert what our user role is:
Console.WriteLine(principal.IsInRole("citizen"));
// True
However, please notice that IsInRole
is case sensitive!
Console.WriteLine(principal.IsInRole("Citizen"));
// False
Retrieving a single claim of a given type
To retrieve a claim of our identity, we can use FindFirst
that will give us either the the value of the claim with this type, or null
if there is no claim with this type:
var name = principal.FindFirst("name");
// Alice
var unknown = principal.FindFirst("address");
// null
Retrieving all claims of a type
Since everything is based on a collection of claims, several claims can be of the same type.
In our case, Alice may have several roles and nicknames:
var claims = new List<Claim>
{
new Claim("sub", "E4WlUXrJgy4"),
new Claim("name", "Alice"),
new Claim("role", "citizen"),
+ new Claim("role", "medic"),
new Claim("height", "1m63"),
};
To retrieve all of them, you can use the method FindAll
:
Also note that in this case, calls to
IsInRole
with either or the role will return true!Console.WriteLine(principal.IsInRole("citizen")); // True Console.WriteLine(principal.IsInRole("medic")); // True
Other assertions
If you want to verify that the current principal identity contains a specific claim, you can do so:
var hasAliceName = principal.HasClaim("name", "Alice");
// True
For a more complex assertion, you may instead use a predicate:
var aliceName = principal.HasClaim(claim
=> claim.Type == "name" && claim.Value == "Alice");
Wrapping up
In this post we covered the atoms from which the ClaimsPrincipal
is made
Of course this is rarely used as such but I hoped it gave you a little bit more understanding of some of the classes used to manipulate the concept of identity!
Photo by Sheldon Kennedy on Unsplash
Top comments (0)