Quantcast
Channel: Christos S. – chsakell's Blog
Viewing all 43 articles
Browse latest View live

ASP.NET Core Identity Series – OAuth 2.0, OpenID Connect & IdentityServer

$
0
0

As the web evolved over the years it proved that the traditional security options and mechanics such as client-server authentication, had several limitations and couldn’t cover (at least properly) the cases introduced by the evolution. Take for example the case where a third-party application requires access to your profile data in a different web application such as Facebook. Years ago this would require to provide your Facebook credentials to the third-party so it can access your account information. This of course, raised several problems such as:

  • Third-party applications must be able to store the user’s credentials
  • Servers that host the protected resources must support password authentication
  • Third-party applications gain access probably to all of the owner’s protected resources
  • In case the owner decides to revoke access to the third-party application, password change is required something that will cause the revocation to all other third-party apps
  • The owner’s credentials are way too much vulnerable and any compromise of a third-party application would result in compromise of all the user’s data

OAuth 2.0 & OpenID Connect to the rescue

Fortunately OAuth protocol introduced and along with OpenID Connect provided a wide range of options for properly securing applications in the cloud. In the world of .NET applications this was quickly connected with an open source framework named IdentityServer which allows you to integrate all the protocol implementations in your apps. IdentityServer made Token-based authentication, Single-Sign-On, centralized and restricted API access a matter of a few lines of code. What this post is all about is to learn the basic concepts of OAuth 2.0 & OpenID Connect so that when using IdentityServer in your .NET Core applications you are totally aware of what’s happening behind the scenes. The post is a continuation of the ASP.NET Core Identity Series where the main goal is to understand ASP.NET Core Identity in depth. More specifically here’s what’s we gonna cover:

  • Explain what OAuth 2.0 is and what problems it solves
  • Learn about OAuth 2.0 basic concepts such as Roles, Tokens and Grants
  • Introduce OpenID Connect and explain its relation with OAuth 2.0
  • Learn about OpenID Connect Flows
  • Understand how to choose the correct authorization/authentication flow for securing your apps
  • Learn how to integrate IdentityServer to your ASP.NET Core application

It won’t be a walk in the park though so make sure to bring all your focus from now on.

The source code for the series is available here. Each part has a related branch on the repository. To follow along with this part clone the repository and checkout the identity-server branch as follow:

   git clone https://github.com/chsakell/aspnet-core-identity.git
   cd .\aspnet-core-identity
   git fetch
   git checkout identity-server
   

It is recommended (but not required) that you read the first 3 posts of the series before continue. This will help you understand better the project we have built so far.

The theory for OAuth 2.0 and OpenID Connect is also available in the following presentation.

OAuth 2.0 Framework

OAuth 2.0 is an open standard authorization framework that can securely issue access tokens so that third-party applications gain limited access to protected resources. This access may be on behalf of the resource owner in which case the resource owner’s approval is required or on its own behalf. You have probably used OAuth many times but haven’t realized it yet. Have you ever been asked by a website to login with your Facebook or Gmail account in order to proceed? Well.. that’s pretty much OAuth where you are being redirected to the authorization server’s authorization endpoint and you give your consent that you allow the third-party application to access specific scopes of your main account (e.g., profile info in Facebook, Gmail or read repositories in GitHub). We mentioned some strange words such as resource owner or authorization server but we haven’t defined what exactly they represent yet so let’s do it now.

OAuth 2.0 Participants

Following are the participants or the so called Roles that evolved and interact each other in OAuth 2.0.

  • Resource Owner: It’s the entity that owns the data, capable of granting access to its protected resources. When this entity is a person then is referred as the End-User
  • Authorization Server: The server that issues access tokens to the client. It is also the entity that authenticates the resource owner and obtains authorization
  • Client: The application that wants to access the resource owner’s data. The client obtains an access token before start sending protected resource requests
  • Resource Server: The server that hosts the protected resources. The server is able to accept and respond to protected resource requests that contain access tokens

OAuth 2.0 Abstraction Flow

The abstract flow illustrated in the following image describes the basic interaction between the roles in OAuth 2.0.

  • The client requests authorization from the resource owner. This can be made either directly with the resource owner (user provides directly the credentials to the client) or via the authorization server using a redirection URL
  • The client receives an authorization grant representing the resource owner’s authorization. OAuth 2.0 provides 4 different types of grants but can also be extended. The grand type depends on the method used by the client to request authorization and the types supported by the authorization server
  • The client uses the authorization grant received and requests an access token by the authorization server’s token endpoint
  • Authorization server authenticates the client, validates the authorization grant and if valid issues an access token
  • The client uses the access token and makes a protected resource request
  • The resource server validates the access token and if valid serves the request

Before explain the 4 different grants in OAuth 2.0 let’s see the types of clients in OAuth:

  • Confidential clients: Clients that are capable to protect their credentials – client_key & client_secret. Web applications (ASP.NET, PHP, Java) hosted on secure servers are examples of this type of clients
  • Public clients: Clients that are incapable of maintaining the confidentiality of their credentials. Examples of this type of clients are mobile devices or browser-based web applications (angular, vue.js, etc..)

Authorization Grants

There are 4 basic grants that clients may use in OAuth 2.0 in order to get an access token, the Authorization Code, the Implicit, Client Credentials and the Resource Owner Password Credentials grant.

Authorization Code

The authorization code grant is a redirection based flow, meaning an authorization server is used as an intermediary between the client and the resource owner. In this flow the client directs the resource owner to an authorization server via the user-agent. After the resource owner’s consent, the owner directs back to the client with an authorization code. Let’s see the main responsibilities for each role on this grant

And here’s the entire Flow

  • A: The Resource owner is directed to the authorization endpoint through the user-agent. The Client includes its identifier, requested scope, local state, and a redirection URI to which the authorization server will send the user-agent back once access is granted (or denied). The client’s request looks like this:
            GET /authorize?
                response_type=code&
                client_id=<clientId>&
                scope=email+api_access&
                state=xyz&
                redirect_uri=https://example.com/callback
            

    The response_type which is equal to code means that the authorization code grant will be used. The client_id is the client’s identifier and the scope defines what the client ask access for

  • B: The authorization server authenticates the resource owner via the user-agent. The resource owner then grants or denies the client’s access request usually via a consent page
  • C: In case the resource owner grants access, the authorization server redirects the user-agent back to the client using the redirection URI provided earlier in the query parameter: redirect_uri. The redirection URI includes the authorization code in a code query string parameter and any state provided by the client on the first step. A redirection URI along with an authorization code looks like this:
            GET /https://example.com/callback?
                code=SplxlOBeZQQYbYS6WxSbIA&
                state=xyz
            
  • D: The client requests an access token from the authorization server’s token endpoint by including the authorization code received in the previous step. The client also authenticates with the authorization server. For verification reason, the request also includes the redirection URI used to obtain the authorization code
    The request looks like this:
            POST /token HTTP/1.1
            Host: auth-server.example.com
            Authorization: Basic F0MzpnWDFmQmF0M2JW
            Content-Type: application/x-www-form-urlencoded
                    
            grant_type=authorization_code&
            code=SplxlOBeZQQYbYS6WxSbIA&
            redirect_uri=https://example.com/callback
            
  • E: The authorization server authenticates the client, validates the authorization code, and ensures that the redirection URI received matches the URI used to redirect the client in the third step. If valid, the authorization server responds back with an access token and optionally, a refresh token. The response looks like this:
            HTTP/1.1 200 OK
            Content-Type: application/json;charset=UTF-8
       
            {
              "access_token":"2YotnFZFEjr1zCsipAA",
              "token_type":"bearer",
              "expires_in":3600,
              "refresh_token":"tGzv3JOkF0TlKWIA"
            }
            

The Authorization Code grant is the one that provides the greater level of security since a) resource owner’s credentials are never exposed to the client, b) it’s a redirection based flow, c) client authenticates with the resource server and d) the access token is transmitted directly to the client without exposing it through the resource owner’s user-agent (implicit grant case)

Implicit Grant

Implicit grant type is a simplified version of the authorization code where the client is issued an access token directly through the owner’s authorization rather than issuing a new request using an authorization code.

Following are the steps for the implicit grant type.

  • A: Client initiates the flow and directs the resource owner’s user-agent to the authorization endpoint. The request includes the client’s identifier, requested scope, any local state to be preserved and a redirection URI to which the authorization server will send the user-agent back once access is granted. A sample request looks like this:
            GET /authorize?
                response_type=token&
                client_id=<clientId>&
                scope=email+api_access&
                state=xyz&
                redirect_uri=https://example.com/callback
            

    Note that this time the response_type parameter has the value token instead of code, indicating that implicit grant is used

  • B: The authorization server authenticates the resource owner via the user-agent. The resource owner then grants or denies the client’s access request, usually via a consent page
  • C: In case the resource owner grants access, the authorization server directs the owner back to the client using the redirection URI. The access token is now included in the URI fragment. The response looks like this:
    
            GET /https://example.com/callback?
                access_token=SpBeZQWxSbIA&
                expires_in=3600&
                token_type=bearer&
                state=xyz
                
            
  • D: The user-agent follows the redirection instructions and makes a request to the web-hosted client resource. This is typically an HTML page with a script to extract the token from the URI
  • E: The web page executes the script and extracts the access token from the URI fragment
  • F: The user-agent finally passes the access token to the client

Implicit grant is optimized for public clients that typically run in a browser such as full Javascript web apps. There isn’t a separate request for receiving the access token which makes it a little bit more responsive and efficient for that kind of clients. On the other hand, it doesn’t include client authentication and the access token is exposed directly in the user-agent.

Resource Owner Password Credentials

The Resource Owner Password Credentials grant is a very simplified, non-directional flow where the Resource Owner provides the client with its username and password and the client itself use them to ask directly for an access token from the authorization server.

  • A: The resource owner provides the client with its username and password
  • B: The client requests an access token from the authorization server’s token endpoint by including the credentials provided by the resource owner. During the request the client authenticates with the authorization server. The request looks like this:
            POST /token HTTP/1.1
            Host: auth-server.example.com:443
            Authorization: Basic F0MzpnWDFmQmF0M2JW
            Content-Type: application/x-www-form-urlencoded
    
            grant_type=password&
            username=chsakell&
            password=random_password
    
            

    Notice that the grant_type is equal to password for this type of grant

  • C: The authorization server authenticates the client and validates the resource owner credentials. If all are valid issues an access token.
            HTTP/1.1 200 OK
            Content-Type: application/json;charset=UTF-8
    
            {
            "access_token":"2YotnFZFEjr1zCsipAA",
            "token_type":"bearer",
            "expires_in":3600,
            "refresh_token":"tGzv3JOkF0TlKWIA"
            }
            

This grant type is suitable for trusted clients only and when the other grant types are not available (e.g. not a browser based client and user-agent cannot be used)

Client Credentials Grant

The Client Credentials grant is again a simplified grant type that works entirely without a resource owner (you can say that the client IS the resource owner).

  • A: The client authenticates with the authorization server and requests an access token from the token endpoint. The authorization request looks like this:
            POST /token HTTP/1.1
            Host: auth-server.example.com:443
            Authorization: Basic F0MzpnWDFmQmF0M2JW
            Content-Type: application/x-www-form-urlencoded
            
            grant_type=client_credentials&
            scope=email&api_access
            

    Notice that the grant_type parameter is equal to client_credentials

  • B: The authorization server authenticates the client and if valid, issues an access token
            HTTP/1.1 200 OK
            Content-Type: application/json;charset=UTF-8
       
            {
              "access_token":"2YotnFZFEjr1zCsipAA",
              "token_type":"bearer",
              "expires_in":3600
            }   
            

This grant type is commonly used when the client acts on its own behalf. A very common case is when internal micro-services communicate with each other. The client also MUST be a confidential client.

Token Types

During the description of each Grant type you may have noticed that apart of the access_token an additional refresh_token may be returned by the authorization server. A refresh token may be returned only for the Authorization Code and the Resource Owner Password Credentials grants. Implicit grant doesn’t support refresh tokens and shouldn’t be included in the access token response of the Client Credentials grant. But what is the different between an access and a refresh token anyway?

The image illustrates the different between the two token types:

  • An access token is used to access protected resources and represents authorization issued to the client. It replaces different authorization constructs (e.g., username and password) with a single token understood by the resource server
  • A refresh token on the other hand which is also issued to the client by the Authorization server, is used to obtain new access token when current token becomes invalid or expires. If authorization server issues a refresh token, it is included when issuing an access token. The refresh token can only be used by the authorization server

OpenID Connect

When describing OAuth 2.0 we said that its purpose is to issue access tokens in order to provide limited access to protected resources, in other words OAuth 2.0 provides authorization but it doesn’t provide authentication. The actual user is never authenticate directly with the client application itself. Access tokens provide a level of pseudo-authentication with no identity implication at all. This pseudo-authentication doesn’t provide information about when, where or how the authentication occurred. This is where OpenID Connect enters and fills the authentication gap or limitations in OAuth 2.0.
OpenID Connect is a simple identity layer on top of the OAuth 2.0 protocol. It enables clients to verify the identity of the End-User based on the authentication performed by an authorization server. It obtains basic profile information about the End-User in an interoperable and REST-like manner (introduction of new REST endpoints). It uses Claims to communicate information about the End-User and extends OAuth in a way that cloud based applications can:

  • Get identity information
  • Retrieve details about the authentication event
  • Allow federated Single Sign On

Let’s see the basic terminology used in OpenID Connect.

  1. End-User: Human participant – in OAuth this refers to the resource owner having their own identity as one of their protected resources
  2. Relying Party: OAuth 2.0 client application. Requires End-User authentication and Claims from an OpenID Provider
  3. Identity Provider: An OAuth 2.0 Authorization Server that authenticates the End-User and provides Claims to the Relying Party about the authentication event and the End-User
  4. Identity Token: A JSON Web Token (JWT) containing claims about the authentication event. It may contain other claims as well


As OpenID Connect sits on top of OAuth 2.0, it makes sense if we say that it uses some of the OAuth 2.0 flows. In fact, OpenID Connect can follow the Authorization Code flow, the Implicit and the Hybrid which is a combination of the previous two. The flows are exactly the same with the only difference that an id_token is issued along with the access_token. Whether the flow is a pure OAuth 2.0 or an OpenID Connect is determined by the presence if the openid scope in the authorization request.

OAuth 2.0 & OpenID Connect Terminology

Don’t get confused by the different terminology that OpenID Connect uses, they are just different names for the same entities

  • End User (OpenID Connect) – Resource Owner (OAuth 2.0)
  • Relying Party (OpenID Connect) – Client (OAuth 2.0)
  • OpenID Provider (OpenID Connect) – Authorization Server (OAuth 2.0)
Identity Token & JWT

The identity token contains the information about the authentication performed and is returned as a JSON Web Token. But what is a JSON Web Token anyway? JSON Web Tokens is an open standard method for representing claims that can be securely transferred between two parties. They are digitally signed meaning the information is verified and trusted that there is no alteration of data during the transfer. They are compact and can be send via URL, POST request or HTTP header. They are Self-Contained meaning they are validated locally by resource servers using the Authorization Server signing key. This is very important to remember and understand it – the token is issued from the authorization server and normally, when sent to the resource server would require to send it back to the authorization server for validation!

JWT Structure

A JWT is a encoded string that has 3 distinct parts: the header, the payload and the signature:

  • Header: A Base64Url encoded JSON that has two properties: a) alg – the algorithm like HMAC SHA256 or RSA used to generate the signature and b) typ the type of the JWT token
  • Payload: A Base64Url encoded JSON that contains the claims which are user details or additional metadata
  • Signature: It ensures that data haven’t changed during the transfer by combining the base64 header and payload with a secret

Claims and Scopes

Claim is an individual piece of information in a key-value pair. Scopes are used to request specific sets of claims. OpenId scope is mandatory scope to specify that OpenID Connect should be used. You will see later on when describing the OpenID Connect flows, that all scopes will contain the openid word, meaning this is an OpenID Connect authorization request. OpenID Connect defines a standard set of basic profile claims. Pre-defined sets of claims can be requested using specific scope values. Individual claims can be requested using the claims request parameter. Standard claims can be requested to be returned either in the UserInfo response or in the ID Token. The following table shows the association between standard scopes with the claims provided.

If you add the email scope in an OpenID Connect request, then both email and email_verified claims will be returned.

OAuth 2.0 & OpenID Connect Endpoints

OAuth 2.0 provides endpoints to support the entire authorization process. Obviously, these endpoints are also used by OpenID Connect which in turn adds a new one named UserInfo Endpoint.

  • Authorization endpoint: Used by the client to obtain
    authorization from the resource owner via user-agent redirection. Performs Authentication of the End-User which is directed through User-Agent. This is the endpoint where you directed when you click the Login with some-provider button
  • Token endpoint: Used by the client to exchange an authorization
    grant for an access token. It returns an access token, an id token in case it’s an OpenID Connect request and optionally a refresh token
  • UserInfo endpoint: This is an addition to OAuth 2.0 by the OpenID Connect and its purpose is to return claims about the authenticated end-user. The request to this endpoint requires an access token retrieved by an authorization request
  • Client endpoint: This is actually an endpoint that belongs to the client, not to the authorization server. It is used though by the authorization server to return responses back to the client via the resource owner’s user-agent

OpenID Connect Flows

Let’s see how Authorization Code and Implicit flows work with OpenID Connect. We ‘ll leave the Hybrid flow out of the scope of this post.

Authorization Code


Generally speaking the flow is exactly the same as described in the OAuth 2.0 authorization code grant. The first difference is that since we need to initiate an OpenID Connect flow instead of a pure OAuth flow, we add the openid scope in the authorization request (which is sent to the authorization endpoint..). The response_type parameter remains the same, code

GET /authorize?
    response_type=code&
	client_id=<clientId>&
	scope=openid profile email&
	state=xyz&
        redirect_uri=https://example.com/callback

The response is again a redirection to the client’s redirection URI with a code fragment.

GET /https://example.com/callback?
	code=SplxlOBeZQQYbYS6WxSbIA&
    state=xyz

Following is the request to the token endpoint, same as described in the OAuth 2.0.

POST /token HTTP/1.1
Host: auth-server.example.com
Authorization: Basic F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https://example.com/callback

The difference though is that now we don’t expect only an access_token and optionally a refresh_token but also an id_token.

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8

{
    "access_token":"2YotnFZFEjr1zCsipAA",
    "id_token":"2YotnFZFEjr1zCsipAA",
    "token_type":"bearer",
    "expires_in":3600,
    "refresh_token":"tGzv3JOkF0TlKWIA"
}

The id_token itself contains basic information about the authentication event along with a subject identifier such as the user’s id or name. For any additional claims or scopes that are added in the initial authorization request (e.g. email, profile) the client sends an extra request to the authorization endpoint. This request requires the access token retrieved in the previous step.

GET /userinfo HTTP/1.1
Host: auth-server.example.com
Authorization: Bearer F0MzpnWDFmQmF0M2JW

Notice that the access token is sent as a bearer token. The UserInfo response contains the claims asked on the initial request

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8

{
    "sub":"12345”,
    "name":"Christos Sakellarios",
    "given_name":”Christos”,
    "picture":"http://example.com/chsakell/me.jpg"
}
Implicit Flow

Recall from the implicit flow described in the OAuth 2.0 that this is a simplified version of authentication flow where the access token is returned directly as the result of the resource owner’s authorization.

In the OpenID Connect implicit flow there are two cases:

  1. Both ID Token and Access Token are returned: In this case the access token will be used to send an extra request to the UserInfo endpoint and get the additional claims defined on the scope parameter. In this case you set the response_type authorization’s request parameter to id_token token meaning you expect both an id_token & an access_token The authorization’s request in this case looks like this:
            GET /authorize?
                response_type=id_token token&
                client_id=<clientId>&
                scope=openid profile&
                state=xyz&
                redirect_uri=https://example.com/callback
            
  2. Only ID Token is returned: In this case you have no intentions to make an extra call to the UserInfo endpoint for getting additional claims but you want them directly on the id token. To do this you set the response_type equal to id_token
            GET /authorize?
                response_type=id_token&
                client_id=<clientId>&
                scope=openid profile&
                state=xyz&
                redirect_uri=https://example.com/callback
            

    ID Token will contain the standard claims along with those asked in the scope

IdentityServer 4

It would take a lot of effort to implement all the specs defined by OAuth 2.0 and OpenID Connect by yourself, luckily though, you don’t have to because there is IdentityServer. All that IdentityServer does is adds the spec compliant OpenID Connect and OAuth 2.0 endpoints to an ASP.NET Core application through middleware. This means that by adding its middleware to your application’s pipeline you get the authorization and token endpoints we have talked about and all the core functionality needed (redirecting, granting access, token validation, etc..) for implementing the spec. All you have to do is provide some basic pages such as the Login, Logout and Logout views. It the IdentityServer4 you will find lots of samples which I recommend you to spend some time and study them. In this post we will use the project we have built so far during the series and cover the following scenario:

  • AspNetCoreIdentity web application will play the role of a third-party application or a Relying party if you prefer
  • There will be a hypothetical Social Network where you have an account. This account of course is an entire different account from the one you have in the AspNetCoreIdentity web application
  • There will be a SocialNetwork.API which exposes your contacts on the Social Network
  • The SocialNetwork.API will be protected through an IdentityServer for which will be a relevant project in the solution
  • The idea is to share something with your SocialNetwork contacts through the AspNetCoreIdentity web app. To achieve this, the AspNetCoreIdentity web app needs to receive an access token from IdentityServer app and use it to access the protected resource which is the SocialNetwork.API


As illustrated on the previous image, our final goal is to send a request to the protected resource in the SocialNetwork.API. We will use the most secure flow which is the Authorization Code with OpenID Connect. Are you ready? Let’s see some code!

Authorization Server Setup

The IdentityServer project in the solution was created as an empty .NET Core Web Application. Its role is to act as the Identity Provider (or as the Authorization Server if you prefer – from now on we will use Identity Provider when we refer to this project). The first thing you need to do to integrate IdentityServer in your app is to install the IdentityServer4 NuGet package. This will provide the core middleware to be plugged in your pipeline. Since this series are related to ASP.NET Core Identity we will also use the IdentityServer4.AspNetIdentity and the IdentityServer4.EntityFramework integration packages.

IdentityServer4.AspNetIdentity provides a configuration API to use the ASP.NET Identity management library for IdentityServer users. IdentityServer4.EntityFramework package provides an EntityFramework implementation for the configuration and operational stores in IdentityServer. But what does this mean anyway? IdentityServer uses some type of infrastructure in order to provide its functionality and more specifically:

  • Configuration data: Data for defining resources and clients
  • Operational data: Data produced by the IdentityServer, such as tokens, codes and consents

When you integrate EntityFramework it means that the database will contain all the required tables for IdentityServer to work. Let’s see how this looks like.

Keep in mind that they are handled by two different DbContext classes, PersistedGrantDbContext and ConfigurationDbContext. Now let’s switch to the Startup class and see how we plug IdentityServer into the pipeline. First we add the services for ASP.NET Identity in the way we have learned through the series, nothing new yet..

services.AddDbContext<ApplicationDbContext>(options =>
{
    if (useInMemoryStores)
    {
        options.UseInMemoryDatabase("IdentityServerDb");
    }
    else
    {
        options.UseSqlServer(connectionString);
    }
});

services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

Next thing we need to do is to register the required IdentityServer services and DbContext stores.

var builder = services.AddIdentityServer(options =>
{
    options.Events.RaiseErrorEvents = true;
    options.Events.RaiseInformationEvents = true;
    options.Events.RaiseFailureEvents = true;
    options.Events.RaiseSuccessEvents = true;
})
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
    options.ConfigureDbContext = opt =>
    {
        if (useInMemoryStores)
        {
            opt.UseInMemoryDatabase("IdentityServerDb");
        }
        else
        {
            opt.UseSqlServer(connectionString);
        }
    };
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
    options.ConfigureDbContext = opt =>
    {
        if (useInMemoryStores)
        {
            opt.UseInMemoryDatabase("IdentityServerDb");
        }
        else
        {
            opt.UseSqlServer(connectionString);
        }
    };

    // this enables automatic token cleanup. this is optional.
    options.EnableTokenCleanup = true;
})
.AddAspNetIdentity<IdentityUser>();

AddAspNetIdentity may take a custom IdentityUser of your choice, for example a class ApplicationUser that extends IdentityUser. ASP.NET Identity services needs to be registered before integrating IdentityServer because the latter needs to override some configuration from ASP.NET Identity. In the ConfigureServices function you will also find a call to builder.AddDeveloperSigningCredential() which creates a temporary key for signing tokens. It’s OK for development but you need to be replace it with a valid persistent key when moving to production environment.

We use a useInMemoryStores variable read from the appsettings.json file to indicate whether we want to use an actual SQL Server database or not. If this variable is false then we make use of the EntityFramework’s UseInMemoryDatabase functionality, otherwise we hit an actual database which of course needs to be setup first. IdentityServer also provides the option to keep store data in memory as shown below:

    var builder = services.AddIdentityServer()
        .AddInMemoryIdentityResources(Config.GetIdentityResources())
        .AddInMemoryApiResources(Config.GetApis())
        .AddInMemoryClients(Config.GetClients());
    

But since we use EntityFramework integration we can use its UseInMemoryDatabase in-memory option

Next we need to register 3 things: a) Which are the API resources needs to be protected, b) which are the clients and how they can get access tokens, meaning what flows they are allowed to use and last but not least c) what are the OpenID Connect scopes allowed. This configuration exists in the Config class as shown below.

public static IEnumerable<IdentityResource> GetIdentityResources()
{
    return new List<IdentityResource>
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
    };
}

Scopes represent something you want to protect and that clients want to access. In OpenID Connect though, scopes represent identity data like user id, name or email address and they need to be registered.

public static IEnumerable<ApiResource> GetApis()
{
    return new List<ApiResource>
    {
        new ApiResource("SocialAPI", "Social Network API")
    };
}
public static IEnumerable<Client> GetClients()
{
    return new List<Client>
    {
        new Client
        {
            ClientId = "AspNetCoreIdentity",
            ClientName = "AspNetCoreIdentity Client",
            AllowedGrantTypes = GrantTypes.Code,
            RequirePkce = true,
            RequireClientSecret = false,

            RedirectUris =           { "http://localhost:5000" },
            PostLogoutRedirectUris = { "http://localhost:5000" },
            AllowedCorsOrigins =     { "http://localhost:5000" },

            AllowedScopes =
            {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
                "SocialAPI"
            }
        }
    };
}

We register the AspNetCoreIdentity client and we defined that it can use the authorization code flow to receive tokens. The redirect URIs needs to be registered as it has to match the authorization’s request redirect URI parameter. We have also defined that this client is allowed to request the openid, profile OpenID Connect scopes plus the SocialAPI for accessing the SocialNetwork.API resources. Client will be hosted in http://localhost:5000. The AllowedGrantTypes property is where you define how clients get access to the protected resources. Intellisense shows that there are several options to pick.

Each option will require the client to act respectively and send the appropriate authorization request to the server for getting access and id tokens. Now that we have defined IdentityServer configuration data we have to load them. You will find a DatabaseInitializer class that does this.

private static void InitializeIdentityServer(IServiceProvider provider)
{
    var context = provider.GetRequiredService<ConfigurationDbContext>();
    if (!context.Clients.Any())
    {
        foreach (var client in Config.GetClients())
        {
            context.Clients.Add(client.ToEntity());
        }
        context.SaveChanges();
    }

    if (!context.IdentityResources.Any())
    {
        foreach (var resource in Config.GetIdentityResources())
        {
            context.IdentityResources.Add(resource.ToEntity());
        }
        context.SaveChanges();
    }

    if (!context.ApiResources.Any())
    {
        foreach (var resource in Config.GetApis())
        {
            context.ApiResources.Add(resource.ToEntity());
        }
        context.SaveChanges();
    }
}

This class also registers a default IdentityUser so that you can login when you fire up the application. You will also find a register link in case you want to create your own user.

var userManager = provider.GetRequiredService<UserManager<IdentityUser>>();
var chsakell = userManager.FindByNameAsync("chsakell").Result;
if (chsakell == null)
{
    chsakell = new IdentityUser
    {
        UserName = "chsakell"
    };
    var result = userManager.CreateAsync(chsakell, "$AspNetIdentity10$").Result;
    if (!result.Succeeded)
    {
        throw new Exception(result.Errors.First().Description);
    }

    chsakell = userManager.FindByNameAsync("chsakell").Result;

    result = userManager.AddClaimsAsync(chsakell, new Claim[]{
        new Claim(JwtClaimTypes.Name, "Chris Sakellarios"),
        new Claim(JwtClaimTypes.GivenName, "Christos"),
        new Claim(JwtClaimTypes.FamilyName, "Sakellarios"),
        new Claim(JwtClaimTypes.Email, "chsakellsblog@blog.com"),
        new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
        new Claim(JwtClaimTypes.WebSite, "https://chsakell.com"),
        new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'localhost 10', 'postal_code': 11146, 'country': 'Greece' }", 
            IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json)
    }).Result;
    // code omitted

Notice that we assigned several claims for this user but only a few belongs to the open id profile scope that the AspNetCoreIdentity client can get access to. We ‘ll see in action what this means.

SocialNetwork.API

SocialNetwork.API is a simple .NET Core Web application exposing the api/contacts protected endpoint.

[HttpGet]
[Authorize]
public ActionResult<IEnumerable<Contact>> Get()
{
    return new List<Contact>
    {
        new Contact
        {
            Name = "Francesca Fenton",
            Username = "Fenton25",
            Email = "francesca@example.com"
        },
        new Contact {
            Name = "Pierce North",
            Username = "Pierce",
            Email = "pierce@example.com"
        },
        new Contact {
            Name = "Marta Grimes",
            Username = "GrimesX",
            Email = "marta@example.com"
        },
        new Contact{
            Name = "Margie Kearney",
            Username = "Kearney20",
            Email = "margie@example.com"
        }
    };
}

All you have to do to protect this API using the OpenID Provider we described, is define how authorization and authentication works for this project in the Startup class.

services.AddAuthorization();

services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = "http://localhost:5005";
        options.RequireHttpsMetadata = false;

        options.Audience = "SocialAPI";
    });

Here we define that Bearer scheme will be the default authentication scheme and that we trust the OpenID Provider hosted in port 5005. The Audience must match the API resource name we defined before.

Client setup

The client uses a javascript library named oidc-client which you can find here. You can find the same functionality for interacting with OpenID Connect flows written in popular client side frameworks (angular, vue.js, etc..). The client needs to setup its own configuration which must match the Identity Provider’s setup. There is an openid-connect.service.ts file that does this.

declare var Oidc : any;

@Injectable()
export class OpenIdConnectService {
    
    config = {
        authority: "http://localhost:5005",
        client_id: "AspNetCoreIdentity",
        redirect_uri: "http://localhost:5000",
        response_type: "code",
        scope: "openid profile SocialAPI",
        post_logout_redirect_uri: "http://localhost:5000",
    };
    userManager : any; 

    constructor() {
        this.userManager = new Oidc.UserManager(this.config);
    }

    public getUser() {
        return this.userManager.getUser();
    }

    public login() {
        return this.userManager.signinRedirect();;
    }

    public signinRedirectCallback() {
        return new Oidc.UserManager({ response_mode: "query" }).signinRedirectCallback();
    }

    public logout() {
        this.userManager.signoutRedirect();
    }
}

The library exposes an Oidc object that provides all the OpenID Connect features. Notice that the config object matches exactly the configuration expected by the authorization server. The response_type is equal to code and along with the openid scope means that the authorization response result is expected to have both an access token and an id token. Since this is an authorization code flow, the access token retrieved will be used to send an extra request to the UserInfo endpoint and get the user claims for the profile scope. The share.component angular component checks if you are logged in with your Social Network account and if so sends a request to the SocialNetwork.API by adding the access token in an Authorization header.

export class SocialApiShareComponent {

    public socialLoggedIn: any;
    public contacts: IContact[] = [];
    public socialApiAccessDenied : boolean = false;

    constructor(public http: Http,
        public openConnectIdService: OpenIdConnectService,
        public router: Router, public stateService: StateService) {
        openConnectIdService.getUser().then((user: any) => {
            if (user) {
                console.log("User logged in", user.profile);
                console.log(user);
                this.socialLoggedIn = true;

                const headers = new Headers();
                headers.append("Authorization", `Bearer ${user.access_token}`);

                const options = new RequestOptions({ headers: headers });

                const socialApiContactsURI = "http://localhost:5010/api/contacts";

                this.http.get(socialApiContactsURI, options).subscribe(result => {
                    this.contacts = result.json() as IContact[];

                }, error => {
                    if (error.status === 401) {
                        this.socialApiAccessDenied = true;
                    }
                });
            }

        });
    }

    login() {
        this.openConnectIdService.login();
    }

    logout() {
        this.openConnectIdService.logout();
    }
}

Now let’s see in action the entire flow. In case you want to use SQL Server database for the IdentityServer make sure you run through the following steps:

Using Visual Studio
  1. Open the Package Manager Console and cd to the IdentityServer project path
  2. Migrations have already run for you so the only thing you need to do is update the database for the 3 db contexts. To do so, change the connection string in the appsettings.json file to reflect your SQL Server environment and run the following commands:
            Update-Database -Context ApplicationDbContext
            
            Update-Database -Context PersistedGrantDbContext
            
            Update-Database -Context ConfigurationDbContext
            
Without Visual Studio
  1. Open a terminal and cd to the IdentityServer project path
  2. Migrations have already run for you so the only thing you need to do is update the database for the 3 db contexts. To do so, change the connection string in the appsettings.json file to reflect your SQL Server environment and run the following commands:
            dotnet ef database update -Context ApplicationDbContext
            
            dotnet ef database update -Context PersistedGrantDbContext
            
            dotnet ef database update -Context ConfigurationDbContext
            

Fire up all the projects and in the AspNetCoreIdentity web application click the Share from the menu. The oidc library will detect that you are not logged in with your Social Network account and present you with the following screen.

Click the login button and see what happens. The first network request is the authorization request to the authorization endpoint:

http://localhost:5005/connect/authorize?
	client_id=AspNetCoreIdentity&
	redirect_uri=http://localhost:5000&
	response_type=code&
	scope=openid profile SocialAPI&
	state=be1916720a2e4585998ae504d43a3c7c&
	code_challenge=pxUY7Dldu3UtT1BM4YGNLEeK45tweexRqbTk79J611o&
    code_challenge_method=S256

You need to be logged in to access this endpoint and thus you are being redirected to login with your Social Network account.

Use the default user credentials created for you chsakell$AspNetIdentity10$ and press login. After a successful login and only if you haven’t already grant access to the AspNetCoreIdentity client you will be directed to the Consent page.

There are two sections for granting access, one for your personal information which asked because of the openid and profile OpenID Connect scopes and another one coming from the Social.API scope. Grant access to all of them to continue. After granting access you will be directed to the initial request to the authorization endpoint. IdentityServer created a code for you and directed the user-agent back to the client’s redirection URI by appending the code in the fragment.

http://localhost:5000/?
	code=090c6f68783c5b5fc267073990417c82ebfa01c1b70bc6107002ab0ae919dd8a
	&scope=openid profile SocialAPI&state=be1916720a2e4585998ae504d43a3c7c
	&session_state=7wBKoHgC7ld3_oO9e9wx-v_BfUa_mz9y6YDfwLKBhIQ.d0c4ee7f77d5da232806e05613067915

As we described the next step in the authorization code flow is to use this code and request for an access token from the token endpoint. The client though doesn’t know exactly where that endpoint resides so it makes a request to the http://localhost:5005/.well-known/openid-configuration. This is an IdentityServer’s configuration endpoint where you can find information about your Identity Provider setup.

The client reads the URI for the token endpoint and sends a POST the request:

Request URL: http://localhost:5005/connect/token
Request Method: POST

client_id: AspNetCoreIdentity
code: 090c6f68783c5b5fc267073990417c82ebfa01c1b70bc6107002ab0ae919dd8a
redirect_uri: http://localhost:5000
code_verifier: ad55ea0f077249ac99e190f576babb7bb9d14dcb229f4c1bb2fe1d0f87dc93d601374a833e4640f0b035c55a87d27a4d
grant_type: authorization_code

Identity provider returns both an access_token and a id_token

{
    "id_token":"<value-stripped-for-displaying-purposes>",
    "access_token":"<value-stripped-for-displaying-purposes>",
    "expires_in":3600,
    "token_type":"Bearer"
 }


Are you curious to find out what those JWT token say? Copy them and paste to jwt.io debugger. Here’s the header and payload for the access token.

// HEADER
{
    "alg": "RS256",
    "kid": "cbd3483398a40cf777e490cd2244deb3",
    "typ": "JWT"
}

// PAYLOAD
{
    "nbf": 1552313271,
    "exp": 1552316871,
    "iss": "http://localhost:5005",
    "aud": [
      "http://localhost:5005/resources",
      "SocialAPI"
    ],
    "client_id": "AspNetCoreIdentity",
    "sub": "09277cac-422d-43ee-b099-f99ff76bceda",
    "auth_time": 1552312960,
    "idp": "local",
    "scope": [
      "openid",
      "profile",
      "SocialAPI"
    ],
    "amr": [
      "pwd"
    ]
}
// HEADER
{
    "alg": "RS256",
    "kid": "cbd3483398a40cf777e490cd2244deb3",
    "typ": "JWT"
}

// PAYLOAD
{
    "nbf": 1552313271,
    "exp": 1552313571,
    "iss": "http://localhost:5005",
    "aud": "AspNetCoreIdentity",
    "iat": 1552313271,
    "at_hash": "AM-fvLMnrmHCFu9nGDmY3Q",
    "sid": "aa8df27adf631604d855533b67c307ea",
    "sub": "09277cac-422d-43ee-b099-f99ff76bceda",
    "auth_time": 1552312960,
    "idp": "local",
    "amr": [
      "pwd"
    ]
  }

What’s interesting is that the id token doesn’t contain the claims that belongs to the profile scope asked in the authorization request and this is of course the expected behavior. By default you will find a sub claim which matches the user’s id and some other information about the authentication event occurred. As described in the theory, the client in this flow uses the access token and sends an extra request to the UserInfo and point to get the user’s claims.

Request URL: http://localhost:5005/connect/userinfo
Request Method: GET

Authorization: Bearer <access-token>

And here’s the response..

{
    "sub":"09277cac-422d-43ee-b099-f99ff76bceda",
    "name":"Chris Sakellarios",
    "given_name":"Christos",
    "family_name":"Sakellarios",
    "website":"https://chsakell.com",
    "preferred_username":"chsakell"
 }

Let me remind you that we have added a claim for address for this user but we don’t see it on the response since address doesn’t belong to the profile scope nor is supported by our IdentityServer’s configuration. Last but not least you will see the request to the SocialNetwork.API protected resource.

Request URL: http://localhost:5010/api/contacts
Request Method: GET

Accept: application/json, text/plain, */*
Authorization: Bearer </access-token>

If all work as intended you will see the following view.

Discussion

I believe that’s more than enough for a single post so we ‘ll stop here. The idea was to understand the basic concepts of OAuth 2.0 and OpenID Connect so that you are aware what’s going on when you use IdentityServer to secure your applications. No one expects from you to know by the book all the protocol specifications but now that you have seen a complete flow in action you will be able to handle any similar case for your projects. Any time you need to implement a flow, read the specs and make the appropriate changes in your apps.
Now take a step back and think outside of the box. What does OAuth 2.0, OpenID Connect and IdentityServer provide us eventually? If you have a single web app, (server side or not, it doesn’t matter..) and the only thing required is a simple sign in, then all these you ‘ve learnt might not be a good fit for you. On the other hand, in case you go big and find yourself having a bunch of different clients, accessing different APIs which in turn accessing other internal APIs (micro-services) then you must be smart and act big as well. Instead of implementing a different authentication method for each type of your clients or reinventing the wheal to support limited access, use IdentityServer.

Orange arrows describe getting access tokens for accessing protected APIs while gray arrows illustrate communication between different components in the architecture. The IdentityServer will play the role of the centralized security token service which provides limited access per client type. This is where you define all of your clients and the way they are authorized via flows while each client requires a minimum configuration to start an authorization flow. Protected resources and APIs, regardless their type all they need is to handle bearer tokens and that’s all.

In case you find my blog’s content interesting, register your email to receive notifications of new posts and follow chsakell’s Blog on its Facebook or Twitter accounts.

Facebook Twitter
.NET Web Application Development by Chris S.
facebook twitter-small
twitter-small

ASP.NET Core Identity Series – External provider authentication & registration strategy

$
0
0

There is no doubt that external provider authentication is a must have feature in new modern applications and makes sense because users are able to easily register new accounts and also login using their social account credentials. The entire process is based on OAuth 2.0 flows which were presented in detail in the OAuth 2.0, OpenID Connect & IdentityServer blog post of the ASP.NET Core Identity Series. In case you haven’t read it, I totally recommend you to do so. Web applications redirect users to sign in to the selected external provider which in turn redirects back to a callback url providing basic profile information such as email address, full name and username. This info is used to register a new account or sign in the user if already registered. If I were to tell what is the biggest advantage or gain when using social login I would say one world, TRUST. A trust that is built on the very well formed OAuth 2.0 protocol and the fact that user accounts are already confirmed in the external providers.

Love all, trust a few. – William Shakespeare

This post is a continuation of the ASP.NET Core Identity Series:

The post is divided in the following sections:

In the first section we are going to configure authentication with Google, Facebook, Twitter, Microsoft, GitHub, LinkedIn and DropBox. We could add more but as you will see later, the process is always the same with a few minor changes for some providers (e.g. Twitter). For each provider there will be a step by step guide with screenshots along with the required commands and code to setup in our web application.

The second section gets even more interesting. The first part of enabling social login is to configure the external providers in the application. The second part is how you actually use them. Let’s consider the scenario where you have created a user account using the normal registration process, meaning you provided your email address, a username and a password. At some point the web site allows you to sign in using your Facebook account but unfortunately your facebook account uses a different email address. Does this mean that signing in with Facebook should create a new account? Or should you have the option to associate your Facebook account with your already confirmed one? The second one sounds a lot better because you will continue to use the same account containing the same data as before (e.g. orders, emails, chat messages, etc..). In order to support this kind of functionality you need to create rules and flows and implementing these two is what the registration strategy is all about. And of course ASP.NET Core Identity supports by default user account association with external login providers.

Configuring external providers authentication in ASP.NET Core

The steps to enable authentication for an external provider are always the same:

  • Step 1: Create an application to the external provider
  • Step 2: Configure the callback url properly
  • Step 3: Register the authentication handler for the external provider in Startup/ConfigureServices

All external providers (Facebook, Google, GitHub, etc..) provide a developer(s) website that you can use it and leverage their APIs and services. Here are some developer websites.

First you navigate to the developer app URL and create a new application for the website you want to add external provider authentication. The required details are usually the name and the URL of your website. To enable authentication in your app you have to set the callback URL which points to a route in your website. This is not an actual route you have created in your web app but a route that the registered authentication handler listens to and handles the external authentication result properly. By default the callback URL an authentication handler listens to is usually signin-<provider-name>, for example the google authentication handler listens to signin-google. This means that if your website’s URL is https://mywebsite.com then the callback URL at Google would be https://mywebsite.com/signin-google. Of course callback URLs are totally configurable, you can add more than one or change them at any time you want. Enough with the theory, let’s see in action how to configure external providers authentication.

The source code for the series is available here. Each part has a related branch on the repository. To follow along with this part clone the repository and checkout the external-authentication branch as follow:

git clone https://github.com/chsakell/aspnet-core-identity.git
cd .\aspnet-core-identity
git checkout external-authentication

In this post we will be working with th AspNetCoreIdentity project in the solution and we will add external providers authentication for multiple providers.

Let me remind you that the app already implements many ASP.NET Core Identity related features and can be run with or without an SQL Server. You can find instructions to setup the project on the README file. More over the AspNetCoreIdentity web app project is configured to run at http://localhost:5000 and this is the base URL that we will be using when we configuring the OAuth callback URL for the external providers.

In case you don’t want to set authentication for all external providers, just read the instructions for those you are interested in

Configure Google Authentication

  • Navigate to the Integrate Google Sign-In page and click the CONFIGURE A PROJECT button. Here you will create a Google API Console project and client ID
  • Enter the name for your project and click Next. This name won’t be used when the app requests access to your Google account. Here’s an example.
  • Next enter the name that will be shown on the user consent screen. The consent screen is shown to the user the very first time the app requests access to your Google account.

    Click Next and proceed to setup the callback URL
  • Select Web server for our application environment and add http://localhost:5000/signin-google as an authorized redirect URI.
  • The following screen displays the Client ID and Client Secret generated for your new Console project.
  • Switch to AspNetCoreIdentity project and search for the ExternalProvidersRegistrations class file. You will find a section to setup your Google project’s client Id and Secret.
    // Google
    
    //dotnet user-secrets set "Authentication:Google:ClientId" ""
    //dotnet user-secrets set "Authentication:Google:ClientSecret" ""
    
    if (configuration["Authentication:Google:ClientId"] != null)
    {
        services.AddAuthentication().AddGoogle(o =>
        {
            o.ClientId = configuration["Authentication:Google:ClientId"];
            o.ClientSecret = configuration["Authentication:Google:ClientSecret"];
        });
    }
    

    All you have to do is open the Package Manager Console, cd to the AspNetCoreIdentity folder and run the dotnet user-secrets set command using your google project’s credentials.

    dotnet user-secrets set "Authentication:Google:ClientId" "<your-client-id>"
    dotnet user-secrets set "Authentication:Google:ClientSecret" "<your-client-secret-id>"
    

  • One last thing you need to know is that you can always re-configure your console project’s settings in console.developers.google.com. When you do that make sure you select the correct project from the top list and select Credentials from the left menu.

    If you click OAuth client you will see the OAuth configuration setup for the project.

    It’s always good to set a logo for your app and this can be done through the OAuth consent screen

    Configure Facebook Authentication

  • Navigate to the Facebook apps page, click the Add a New App button and fill the Create a New App ID popup form. The display name will be the name of the app will appear in the consent page.
  • When the App ID is created you will be redirected to the app’s page. Click the plus icon next to the Products menu item on the bottom left
  • Locate the Facebook Login product and click Set Up
  • Click the Settings menu item under the Facebook Login list and add http://localhost:5000/signin-facebook as an authorized redirect URI. Don’t forget to save the changes.

    Lately Facebook returns a message that localhost redirects do not need to be added. If you get this message just proceed with the next step

  • Click the Basic menu item under the Settings to retrieve your app’s credentials, App ID and App Secret
  • Switch to AspNetCoreIdentity project and search for the ExternalProvidersRegistrations class file. You will find a section to setup your Facebook app’s app Id and secret.
     
    // Facebook
    
    // dotnet user-secrets set Authentication:Facebook:AppId ""
    // dotnet user-secrets set Authentication:Facebook:AppSecret ""
    
    if (configuration["Authentication:Facebook:AppId"] != null)
    {
        services.AddAuthentication().AddFacebook(facebookOptions =>
        {
            facebookOptions.AppId = configuration["Authentication:Facebook:AppId"];
            facebookOptions.AppSecret = configuration["Authentication:Facebook:AppSecret"];
        });
    }
    

    Open the Package Manager Console, cd to the AspNetCoreIdentity folder and run the dotnet user-secrets set command using your facebook app’s credentials.

    dotnet user-secrets set Authentication:Facebook:AppId "<your-app-id>"
    dotnet user-secrets set Authentication:Facebook:AppSecret "<your-app-secret>"
    
  • Configure Twitter Authentication

  • Navigate to the Twitter apps page, click the Create an App button and fill the App details form. You will find that some fields are required, such as the Website URL. In case you don’t have valid values just fill with a fake one. The most important thing is to add the http://localhost:5000/signin-twitter as an authorized callback URI.

  • Fill and consent to any developer related terms required by Twitter
  • In the app’s view click the Keys and tokens tab to find the Twitter credentials for your app
  • Switch to AspNetCoreIdentity project and search for the ExternalProvidersRegistrations class file. You will find a section to setup your Twitter’s Consumer API key and secret.
    // Twitter
    
    // dotnet user-secrets set Authentication:Twitter:ConsumerAPIKey ""
    // dotnet user-secrets set Authentication:Twitter:ConsumerAPISecret ""
    
    if (configuration["Authentication:Twitter:ConsumerAPIKey"] != null)
    {
        services.AddAuthentication().AddTwitter(twitterOptions =>
        {
            twitterOptions.ConsumerKey = configuration["Authentication:Twitter:ConsumerAPIKey"];
            twitterOptions.ConsumerSecret = configuration["Authentication:Twitter:ConsumerAPISecret"];
            twitterOptions.RetrieveUserDetails = true;
        });
    }
    

  • You might think you have finished setting up Twitter authentication but you haven’t. Our app needs to read the external provider logged in user’s email address and by default Twitter doesn’t give this permission. Here’s a sneak peek to a debugging session while logged in with Twitter (we will study the code in more detail in the registration strategy..)

    As you can see there’s no email claim to retrieve and proceed with the authentication process. What you need to do is go back to the Twitter app’s App details view and fill the Terms of Service URL and Privacy policy URL fields.

    Next click the Permissions tab and check the Request email address from users checkbox. You won’t be able to check it unless you fill the previous fields.

    After doing so, here’s how the debugging session looks like.
  • Configure Microsoft Account Authentication

  • Navigate to the Azure apps registration page, click the New registration button and fill the form as follow. You can give your own name but make sure you set the Redirect URI to http://localhost:5000/signin-microsoft.
  • Select the Certificates & secrets from the left menu and click New client secret

    Check never and click >Add.

    Copy and save the client secret created.
  • To view your Application client id select Overview from the left menu.
  • Now that you have your app’s credentials switch to AspNetCoreIdentity project and search for the ExternalProvidersRegistrations class file. You will find a section to setup your Micorosoft app’s credentials.
    // Microsoft
    
    // dotnet user-secrets set Authentication:Microsoft:ClientId ""
    // dotnet user-secrets set Authentication:Microsoft:ClientSecret ""
    
    if (configuration["Authentication:Microsoft:ClientId"] != null)
    {
        services.AddAuthentication().AddMicrosoftAccount(microsoftOptions =>
        {
            microsoftOptions.ClientId = configuration["Authentication:Microsoft:ClientId"];
            microsoftOptions.ClientSecret = configuration["Authentication:Microsoft:ClientSecret"];
        });
    }
    

  • In case you want to add a logo for your app select Branding from the left menu and upload your logo.
  • Configure GitHub Authentication

  • Navigate to the GitHub developers page and select OAuth Apps from the left menu. Next click Register a new application if it’s the first time you create an app on GitHub or the New OAuth app button.
  • Fill the form and make sure to add http://localhost:5000/signin-github as an authorized callback URL.
  • When the app is created you can get its client id and client secret. You can also set an application logo.
  • Switch to AspNetCoreIdentity project and search for the ExternalProvidersRegistrations class file. You will find a section to setup your GitHub app’s credentials.
    // dotnet user-secrets set Authentication:GitHub:ClientId ""
    // dotnet user-secrets set Authentication:GitHub:ClientSecret ""
    
    if (configuration["Authentication:GitHub:ClientId"] != null)
    {
        services.AddAuthentication().AddGitHub(gitHubOptions =>
        {
            gitHubOptions.ClientId = configuration["Authentication:GitHub:ClientId"];
            gitHubOptions.ClientSecret = configuration["Authentication:GitHub:ClientSecret"];
        });
    }
    

  • Configure LinkedIn Authentication

  • Navigate to the LinkedIn developers page and click the Create app button.
  • Fill all the form fields and optionally set an app logo. For the Company field set any company you want since it’s not going to be validated unless you request to.
  • In the app’s page select the Auth tab and get its OAuth credentials.
  • Switch to AspNetCoreIdentity project and search for the ExternalProvidersRegistrations class file. You will find a section to setup your LinkedIn app’s credentials.
    // LinkedIn
    
    // dotnet user-secrets set Authentication:LinkedIn:ClientId ""
    // dotnet user-secrets set Authentication:LinkedIn:ClientSecret ""
    
    if (configuration["Authentication:LinkedIn:ClientId"] != null)
    {
        services.AddAuthentication().AddLinkedIn(linkedInOptions =>
        {
            linkedInOptions.ClientId = configuration["Authentication:LinkedIn:ClientId"];
            linkedInOptions.ClientSecret = configuration["Authentication:LinkedIn:ClientSecret"];
            //linkedInOptions.CallbackPath = "/signin-linkedin";
        });
    }
    

  • Configure DropBox Authentication

  • Navigate to the DropBox developers apps page and click the Create app button.
  • Select Dropbox API and check the App folder – access to a single folder created specifically for your app checkbox. Next name your app as you wish.

    When you create a new app, DropBox will also create a new folder for this app.
  • In the app’s Settings tab add http://localhost:5000/signin-dropbox as an authorized redirect URI. Notice that the Settings view also contains the App key and App secret.
  • Switch to AspNetCoreIdentity project and search for the ExternalProvidersRegistrations class file. You will find a section to setup your DropBox app’s credentials.
    // DropBox
    
    // dotnet user-secrets set Authentication:DropBox:ClientKey ""
    // dotnet user-secrets set Authentication:DropBox:ClientSecret ""
    
    if (configuration["Authentication:DropBox:ClientKey"] != null)
    {
        services.AddAuthentication().AddDropbox(dropBoxOptions =>
        {
            dropBoxOptions.ClientId = configuration["Authentication:DropBox:ClientKey"];
            dropBoxOptions.ClientSecret = configuration["Authentication:DropBox:ClientSecret"];
            //dropBoxOptions.CallbackPath = "/signin-dropbox";
        });
    }
    

  • The Branding tab can be used to set your application logo. It seems that you can set a logo that already exists on your DropBox account.
  • Now that we have set authentication for all these providers let’s see how the consent page looks like for some of them:

    Facebook consent page


    Twitter consent page


    Microsoft consent page


    GitHub consent page


    LinkedIn consent page


    Dropbox consent page

    Any time you want to remove access given to an app, sign in to the specific provider and find the page for your account permissions. For example for Google permissions you can navigate to myaccount.google.com/permissions.

    External providers registration strategy

    After adding external providers authentication to your web application you will soon realize that there will be cases where you need to make a decision. The simplest scenario is where you have already registered an account through the normal process by providing your email, a username and a password. After using your credentials for a long time to authenticate, someday you decide that you want to sign in through your Facebook account. The thing is that your Facebook account is registered with a different email address which brings us to the million-dollar question: Is the app going to create a new account for that email address or is it going to provide the option to associate the Facebook address to an existing account? And if it’s the latter what is the right process to do so? In this post and the associated source code we will provide both the options, allowing the user to decide either to create a new account or associate an existing one. Before start exploring the source code we need to write down the rules and flows this strategy follows.

  • Rule #1: Users can sign in only if their email is confirmed. Normally websites let you sign in even if you haven’t confirmed your email address but prevent you from using their services (such as placing orders for example) till you do so. Our app isn’t that complex so we can stick with this rule for simplicity reasons
  • Rule #2: Users authenticated by an external provider are considered trusted. This means that if a user tries to register a new account through an external provider and selects the option to register a new account using the email address used in the provider, then the user won’t have to confirm the email address because it’s already confirmed by the provider
  • Rule #3: Users authenticated by an external provider that select the option to associate the email address used in the provider with an existing account registered with a different email address, will have to confirm the external provider association through the existing account’s email address
  • Rule #4: An association can only happen with already confirmed accounts
  • Rule #5: Users authenticated by an external provider but have an existing account with the same email address that hasn’t been confirmed, have to confirm the association which eventually will automatically confirm the existing account as well. The reason is simple: Consider the scenario where some stranger uses your email address and registers an account through the normal process. This means that a stranger knows the password for that account. You on the other side decide someday to sign in through an external provider that uses the same email address. If we automatically confirm the account that already exists in the database then the stranger has instant access to your account through the normal authentication process (username & password)
  • Now that we know the rules let’s take a look at the flows running during registration and external providers association process.

    • When registering a new account through the normal process, a confirmation email is sent to the email address used. The email contains a link that changes the user account EmailConfirmed status to True
    • When trying to associate an external login provider with an existing confirmed account that has a different email address, a confirmation email is sent to that address. Clicking on the link in the email adds the login provider to the existing account
    • When trying to associate an external login provider with an existing un-confirmed account that has a different email address, nothing happens. A message is shown to the user that the existing account’s email address needs to be confirmed first
    • Registering a new account through an external login provider marks instantly the account as confirmed and hence no confirmation email is sent

    External provider registration strategy implementation

    Time to check how all the above requirements are implemented. We will start by making email confirmation required. In the Startup when you configure Identity you need to set the following:

    services.AddIdentity<IdentityUser, IdentityRole>(config =>
    {
        config.SignIn.RequireConfirmedEmail = true;
    })
    .AddEntityFrameworkStores<IdentityDbContext>()
    .AddDefaultTokenProviders();
    

    There are two controllers related to user accounts, the default AccountController and the SocialAccountController that deals with all the external provider related tasks. In the Login action of the AccountController we will add a check to see if the user can sign in.

    if (await _userManager.CheckPasswordAsync(user, model.Password))
    {
        // Rule #1
        if (!await _signInManager.CanSignInAsync(user))
        {
            result.Status = Status.Error;
            result.Data = "<li>Email confirmation required</li>";
    
            return result;
        }
    // code omitted 
    

    Always remember to use the CanSignInAsync method of the SignInManager because it knows to run all validations have been configured in the Identity system.

    public virtual async Task<bool> CanSignInAsync(TUser user)
    {
        if (Options.SignIn.RequireConfirmedEmail && !(await UserManager.IsEmailConfirmedAsync(user)))
        {
            Logger.LogWarning(0, "User {userId} cannot sign in without a confirmed email.", await UserManager.GetUserIdAsync(user));
            return false;
        }
        if (Options.SignIn.RequireConfirmedPhoneNumber && !(await UserManager.IsPhoneNumberConfirmedAsync(user)))
        {
            Logger.LogWarning(1, "User {userId} cannot sign in without a confirmed phone number.", await UserManager.GetUserIdAsync(user));
            return false;
        }
        if (Options.SignIn.RequireConfirmedAccount && !(await _confirmation.IsConfirmedAsync(UserManager, user)))
        {
            Logger.LogWarning(4, "User {userId} cannot sign in without a confirmed account.", await UserManager.GetUserIdAsync(user));
            return false;
        }
        return true;
    }
    

    Now if you create a new user through the default process its EmailConfirmed column in the AspNetUsers table will be False.

    And if you try to sign in with your credential you will see the following error message.

    Next thing we need to do is add email support to our app. This is very easy to accomplish using the SendGrid service. It’s totally free so go ahead and create a new account. You will be able to send at least 100 emails per day so it’s more than enough for development purposes. After creating the account open the API Keys view under the Settings menu on the left and create a new API key.

    Copy your API Key. You also need your account’s username which you can find in the Account Details menu item. Next run the following commands in the Package Manager Console:

    dotnet user-secrets set SendGridUser "<your-sendgrid-username>"
    dotnet user-secrets set SendGridKey "<your-sendgrid-apikey>"
    

    When registering a new account through the normal process a new email confirmation token is created and sent to the email address used. This is done in the AccountController Register action.

    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
    var callbackUrl = Url.Action("ConfirmEmail", "Account",
        values: new { userId = user.Id, code = code },
        protocol: Request.Scheme);
    
    await _emailSender.SendEmailAsync(user.Email, "Confirm your email",
        $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
    
    return new ResultVM
    {
        Status = Status.Success,
        Message = "Email confirmation is pending",
        Data = user
    };
    

    You can generate several types of tokens as you can see from the implementation of the GenerateEmailConfirmationTokenAsync method.

    public virtual Task<string> GenerateEmailConfirmationTokenAsync(TUser user)
    {
        ThrowIfDisposed();
        return GenerateUserTokenAsync(user, Options.Tokens.EmailConfirmationTokenProvider, ConfirmEmailTokenPurpose);
    }
    

    The Url.Action will create a link to the ConfirmEmail action of the same controller. This action calls the UserManager ConfirmEmailsAsync method which validates the token and if valid will update the user record in the database as well.

    public virtual async Task<IdentityResult> ConfirmEmailAsync(TUser user, string token)
    {
        ThrowIfDisposed();
        var store = GetEmailStore();
        if (user == null)
        {
            throw new ArgumentNullException(nameof(user));
        }
    
        if (!await VerifyUserTokenAsync(user, Options.Tokens.EmailConfirmationTokenProvider, ConfirmEmailTokenPurpose, token))
        {
            return IdentityResult.Failed(ErrorDescriber.InvalidToken());
        }
        await store.SetEmailConfirmedAsync(user, true, CancellationToken);
        return await UpdateUserAsync(user);
    }
    

    Let’s understand what happens when you add support for an external provider authentication through an extension method such as services.AddAuthentication().AddGoogle. We will examine the case of the Google provider. What happens is that the builder registers a handler of type Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler.

    public static class GoogleExtensions
    {
        // code omitted 
    
        public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<GoogleOptions> configureOptions)
            => builder.AddOAuth<GoogleOptions, GoogleHandler>(authenticationScheme, displayName, configureOptions);
    }
    

    Each of these handlers has some type of default OAuthOptions configuration, so now you know where the /signin-google, signin-facebook etc.. comes from.

    public class GoogleOptions : OAuthOptions
    {
        /// <summary>
        /// Initializes a new <see cref="GoogleOptions"/>.
        /// </summary>
        public GoogleOptions()
        {
            CallbackPath = new PathString("/signin-google");
            AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint;
            TokenEndpoint = GoogleDefaults.TokenEndpoint;
            UserInformationEndpoint = GoogleDefaults.UserInformationEndpoint;
            Scope.Add("openid");
            Scope.Add("profile");
            Scope.Add("email");
    
            ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
            ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
            ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
            ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
            ClaimActions.MapJsonKey("urn:google:profile", "link");
            ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
        }
    
        /// <summary>
        /// access_type. Set to 'offline' to request a refresh token.
        /// </summary>
        public string AccessType { get; set; }
    }
    

    These handlers know how to create a ChallengeUrl which is the Url that redirects the user to the external provider for signing in.

    protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
    {
        // Google Identity Platform Manual:
        // https://developers.google.com/identity/protocols/OAuth2WebServer
    
        var queryStrings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        queryStrings.Add("response_type", "code");
        queryStrings.Add("client_id", Options.ClientId);
        queryStrings.Add("redirect_uri", redirectUri);
    
        AddQueryString(queryStrings, properties, GoogleChallengeProperties.ScopeKey, FormatScope, Options.Scope);
        AddQueryString(queryStrings, properties, GoogleChallengeProperties.AccessTypeKey, Options.AccessType);
        AddQueryString(queryStrings, properties, GoogleChallengeProperties.ApprovalPromptKey);
        AddQueryString(queryStrings, properties, GoogleChallengeProperties.PromptParameterKey);
        AddQueryString(queryStrings, properties, GoogleChallengeProperties.LoginHintKey);
        AddQueryString(queryStrings, properties, GoogleChallengeProperties.IncludeGrantedScopesKey, v => v?.ToString().ToLower(), (bool?)null);
    
        var state = Options.StateDataFormat.Protect(properties);
        queryStrings.Add("state", state);
    
        var authorizationEndpoint = QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings);
        return authorizationEndpoint;
    }
    

    When a OAuth handler fires, its CreateTicketAsync method runs and if the response payload from the external provider is valid then an AuthenticationTicket is returned.

    protected override async Task<AuthenticationTicket> CreateTicketAsync(
        ClaimsIdentity identity,
        AuthenticationProperties properties,
        OAuthTokenResponse tokens)
    {
        // Get the Google user
        var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
    
        var response = await Backchannel.SendAsync(request, Context.RequestAborted);
        if (!response.IsSuccessStatusCode)
        {
            throw new HttpRequestException($"An error occurred when retrieving Google user information ({response.StatusCode}). Please check if the authentication information is correct.");
        }
    
        using (var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync()))
        {
            var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
            context.RunClaimActions();
            await Events.CreatingTicket(context);
            return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
        }
    }
    

    As we mentioned, each OAuth handler has some default values and one of them is the AuthenticationScheme.

    public static partial class GoogleDefaults
    {
        public const string AuthenticationScheme = "Google";
        public static readonly string AuthorizationEndpoint;
        public static readonly string DisplayName;
        public static readonly string TokenEndpoint;
        public static readonly string UserInformationEndpoint;
    }
    

    The AuthenticationBuilder.AddOAuth extension method ends up adding mappings of Authentication schemes with the relative handlers..

    public static AuthenticationBuilder AddOAuth<TOptions, THandler>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TOptions> configureOptions)
        where TOptions : OAuthOptions, new()
        where THandler : OAuthHandler<TOptions>
    {
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, OAuthPostConfigureOptions<TOptions, THandler>>());
        return builder.AddRemoteScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
    }
    

    When you run the app you will see all external providers that have been setup on the login screen.

    This is an API call to the Providers action of the SocialAccountController that invokes the _signInManager.GetExternalAuthenticationSchemesAsync method.

    [HttpGet]
    public async Task<IActionResult> Providers()
    {
        var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync();
    
        return Ok(schemes.Select(s => s.DisplayName).ToList());
    }
    

    The GetExternalAuthenticationSchemesAsync method returns all the schemes that have been added for external login providers.

    public virtual async Task<IEnumerable<AuthenticationScheme>> GetExternalAuthenticationSchemesAsync()
    {
        var schemes = await _schemes.GetAllSchemesAsync();
        return schemes.Where(s => !string.IsNullOrEmpty(s.DisplayName));
    }
    

    This concludes how the authentication handlers are added and work behind the scenes. Each of the external providers icon you see on the login screen points to the SocialAccountController Login action, passing the respective provider parameter value.

    [HttpGet]
    public IActionResult Login(string provider, string returnUrl = null)
    {
        var redirectUrl = Url.Action("Callback", "SocialAccount");
        var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
        return new ChallengeResult(provider, properties);
    }
    

    The redirectUrl is the action method that will handle the response redirect from the external provider and in our case is the Callback action of the same controller. When the external provider’s response hits this action, first we retrieve the provider’s info.

    var info = await _signInManager.GetExternalLoginInfoAsync();
    

    Next it tries to sign in using the login provider name and its provider key.

    var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey,
                    isPersistent: false, bypassTwoFactor: true);
    

    Let’s pause a little bit here to see what this means. A user account or an AspNetUsers table record, can be bound with multiple external providers through the AspNetUserLogins table. In the following screenshot you can see that my account has 3 external providers that may use different email addresses from the original one.

    The provider key is unique for every user in the external provider. The ExternalLoginSignInAsync method tries to find a user that has added a login provider having a specific provider key.

    public virtual async Task<SignInResult> ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent, bool bypassTwoFactor)
    {
        var user = await UserManager.FindByLoginAsync(loginProvider, providerKey);
        if (user == null)
        {
            return SignInResult.Failed;
        }
    
        var error = await PreSignInCheck(user);
        if (error != null)
        {
            return error;
        }
        return await SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor);
    }
    

    If a user with this provider details found, the PreSignInCheck method will check if the user can sign in.

    protected virtual async Task<SignInResult> PreSignInCheck(TUser user)
    {
        if (!await CanSignInAsync(user))
        {
            return SignInResult.NotAllowed;
        }
        if (await IsLockedOut(user))
        {
            return await LockedOut(user);
        }
        return null;
    }
    

    What follows next in the Callback action is the business logic we have decided to implement.

    • On Success: It means that the user has already added this external provider and the account is confirmed
      var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey,
                      isPersistent: false, bypassTwoFactor: true);
      if (result.Succeeded)
      {
          return LocalRedirect(returnUrl);
      }
      // code omitted
      
    • Failure & account with email address doesn’t exist:The user is redirected to the register view to choose either to create a new account or associate it with an existing one
    • Failure & account with email exists but is unconfirmed: A confirmation email is sent. If the user clicks the email link two actions will follow: The account will be confirmed and the external provider will be added as well. The action that will handle this is the ConfirmExternalProvider action in the Account controller.
      // RULE #5
      if (!userDb.EmailConfirmed)
      {
          var token = await _userManager.GenerateEmailConfirmationTokenAsync(userDb);
      
          var callbackUrl = Url.Action("ConfirmExternalProvider", "Account",
              values: new
              {
                  userId = userDb.Id,
                  code = token,
                  loginProvider = info.LoginProvider,
                  providerDisplayName = info.LoginProvider,
                  providerKey = info.ProviderKey
              },
              protocol: Request.Scheme);
      
          await _emailSender.SendEmailAsync(userDb.Email, $"Confirm {info.ProviderDisplayName} external login",
              $"Please confirm association of your {info.ProviderDisplayName} account by clicking <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>here</a>.");
      
          return LocalRedirect(
              $"{returnUrl}?message=External account association with {info.ProviderDisplayName} is pending.Please check your email");
      }
      
    • Failure & account with email exists and is already confirmed: This means that we can proceed by adding the login provider to that account.
      // Add the external provider
      await _userManager.AddLoginAsync(userDb, info);
      
      await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey,
      isPersistent: false, bypassTwoFactor: true);
      
      return LocalRedirect(
          $"{returnUrl}?message={info.ProviderDisplayName} has been added successfully");
      

    When signing in with an external provider it’s important to use the ExternalLoginSignInAsync method. This will add the AuthenticationMethod claim to the user and you will be able to find the provider the user is signed in to the app.

    [HttpGet]
    public UserStateVM Authenticated()
    {
        return new UserStateVM
        {
            IsAuthenticated = User.Identity.IsAuthenticated,
            Username = User.Identity.IsAuthenticated ? User.Identity.Name : string.Empty,
            AuthenticationMethod = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.AuthenticationMethod)?.Value
        };
    }
    

    External provider association with an existing account

    When you sign in with an external provider and no user with the email used in the provider found, the app redirects you to the register page giving you two options. The first one is to simply provide a username and create a new account.

    Hitting register will invoke the associate action of the SocialAccountController passing the following params on the request’s body. Notice that all the required provider’s details are available (read from the query string) to add the login provider to the new user account to be created.

    On the other hand, if you click the associate an existing account checkbox and choose to associate the external provider with an existing account, you need to enter the email address of the existing account you wish to associate your provider login.

    This time the associateExistingAccount param is True. The code will check if the existing account exists in the database and if it doesn’t it will return a simple message. If it does exist it will check its email confirmation status. If the email isn’t confirmed again it will send back a simple message that the existing account should be confirmed first before associating an external provider (Rule #5). If the account is confirmed then a confirmation email will be sent to the existing account’s email address in order to confirm and add the external provider login (Rule #4).

    if (userDb != null)
    {
        // Rule #5
        if (!userDb.EmailConfirmed)
        {
            return new ResultVM
            {
                Status = Status.Error,
                Message = "Invalid data",
                Data = $"<li>Associated account (<i>{associate.AssociateEmail}</i>) hasn't been confirmed yet.</li><li>Confirm the account and try again</li>"
            };
        }
    
        // Rule #4
        var token = await _userManager.GenerateEmailConfirmationTokenAsync(userDb);
    
        var callbackUrl = Url.Action("ConfirmExternalProvider", "Account",
            values: new
            {
                userId = userDb.Id,
                code = token,
                loginProvider = associate.LoginProvider,
                providerDisplayName = associate.LoginProvider,
                providerKey = associate.ProviderKey
            },
            protocol: Request.Scheme);
    
        await _emailSender.SendEmailAsync(userDb.Email, $"Confirm {associate.ProviderDisplayName} external login",
            $"Please confirm association of your {associate.ProviderDisplayName} account by clicking <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>here</a>.");
    
        return new ResultVM
        {
            Status = Status.Success,
            Message = "External account association is pending. Please check your email"
        };
    }
    

    That’s it, we finished! I encourage you run the AspNetCoreIdentity app and test all the authentication methods we mentioned in the post. One thing that I have intentionally left for the end, is to advise you to never share the external provider details in emails. We used it in our app for simplicity but in a production environment, you should use some type of encryption and/or a database store. Also, don’t forget that when registering a new account through an external provider the record in the AspNetUsers database table has NULL value for the PasswordHash value. This means that if the external provider is not available (Facebook & Instagram have lots of incidents lately..), the users won’t be able to sign in. For this reason, you should create a view for the user to set a password as well. I will probably push it to the repo for you in the near future.

    We have seen how to add external login provider authentication for many providers and how their handlers work behind the scenes. We also implemented an external login provider registration strategy that allows the user to choose either to create a new account from the external provider login details or associate it with an existing one.

    In case you find my blog’s content interesting, register your email to receive notifications of new posts and follow chsakell’s Blog on its Facebook or Twitter accounts.

    Facebook Twitter
    .NET Web Application Development by Chris S.
    facebook twitter-small
    twitter-small

    ASP.NET Core Identity Series – Two Factor Authentication

    $
    0
    0

    Two-Factor Authentication is an additional security layer used to address the vulnerabilities of a standard password-only approach. All popular websites such as Facebook, Twitter, LinkedIn or DropBox recommend their users to enable the feature and prevent unauthorized access to their accounts or at least minimize the probability of compromising them. How does it work? In a nutshell, after authenticating using the standard username-password or email-password credentials, the user is asked to provide a code that only he/she has access to. This code is generated usually by a Time-based One-time Password Algorithm running on the user’s smartphone’s authenticator app, which means that is valid for only a small time of period. So 2FA is a Multi-Factor Authentication model that requires a combination of something you have and something you know in order to access your account. One question arising is how the authenticator knows to generate valid time-based codes. As we will explain later in the post the web app and the authenticator app usually share a key, the authenticator key, which is used to generate the tokens. When you decide to enable 2FA in a website, you will be asked to enter this shared key to your smartphone’s authenticator app. This key can be either manually typed or shared via a QR Code and automatically added to your app. ASP.NET Core Identity totally supports 2FA Time-based One-time Password Algorithm (TOTP) and this is what this post is all about. We will implement all the available 2FA steps one by one and also explain how it works behind the scenes. After understanding its behavior we will override some default implementations to enhance the security level that 2FA provides. Let’s see the contents of the post in detail:

    • Implement all Two-Factor Authentication related tasks:
      • Enable/Disable 2FA – QR Code included
      • Generate/Reset recovery tokens
      • Reset authenticator app
    • Explore the 2FA code and database schema
    • Enhance the security level of 2FA by overriding the default implementation
      • Encrypt authenticator key
      • Encrypt recovery tokens

    The source code for the series is available here. Each part has a related branch on the repository. To follow along with this part clone the repository and checkout the two-factor-authentication branch as follow:

    git clone https://github.com/chsakell/aspnet-core-identity.git
    cd .\aspnet-core-identity
    git checkout two-factor-authentication
    

    This post is part of the ASP.NET Core Identity Series:

    Enable Two-Factor Authentication

    All 2FA features have been added to the AspNetCoreIdentity web app project and a new manage/account route contains all the related UI logic. There are two tabs, one to display the current status of the 2FA and another one to configure it.

    There are 3 properties on the account that we are interested in, the 2FA Enabled the Has Authenticator and the 2FA Client remember statuses. All the 2FA backend implementation logic exists in the TwoFactorAuthenticationController and when the manage/account route is activated a call to its Details action is made.

    [HttpGet]
    [Authorize]
    public async Task<AccountDetailsVM> Details()
    {
        var user = await _userManager.GetUserAsync(User);
        var logins = await _userManager.GetLoginsAsync(user);
    
        return new AccountDetailsVM
        {
            Username = user.UserName,
            Email = user.Email,
            EmailConfirmed = user.EmailConfirmed,
            PhoneNumber = user.PhoneNumber,
            ExternalLogins = logins.Select(login => login.ProviderDisplayName).ToList(),
            TwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user),
            HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
            TwoFactorClientRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user),
            RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user)
        };
    }
    

    _userManager.GetTwoFactorEnabledAsync(user) checks the TwoFactorEnabled column in the AspNetUsers table because we use the Entity’s Framework implementation. It’s just a flag defining either if the 2FA is enabled or not.

    If you want to override where this value comes from, all you have to do is provide a custom implementation for the IUserTwoFactorStore interface.

    public virtual async Task<bool> GetTwoFactorEnabledAsync(TUser user)
    {
        ThrowIfDisposed();
        var store = GetUserTwoFactorStore();
        if (user == null)
        {
            throw new ArgumentNullException(nameof(user));
        }
        return await store.GetTwoFactorEnabledAsync(user, CancellationToken);
    }     
    

    The _userManager.GetAuthenticatorKeyAsync(user) returns the authenticator key (if exists) that is used to generate valid time-based tokens. This defines if the user has configured an authenticator app or not and it’s different than the previous one which tells if 2FA is enabled. This means that you may have setup an authenticator but at the same time you can also have disabled the 2FA. To setup an authenticator in our app, select the 2 Factor Authentication menu item in the Security tab.

    Next click the Setup authenticator button.

    This will present you a key to enter in your authenticator app and a QR Code in case you prefer to scan it instead of typing it.

    What changed in the database is that a User Token record has been added for your user.

    Notice that the Value column is the exact key generated and asked to type in your authenticator app. If you get back in the Profile tab you will see that Has Authenticator column is now true. This means that _userManager.GetAuthenticatorKeyAsync(user) checks if there’s a record in the AspNetUserTokens table for the logged in user that has LoginProvider = [AspNetUserStore].

    public virtual Task<string> GetAuthenticatorKeyAsync(TUser user)
    {
        ThrowIfDisposed();
        var store = GetAuthenticatorKeyStore();
        if (user == null)
        {
            throw new ArgumentNullException(nameof(user));
        }
        return store.GetAuthenticatorKeyAsync(user, CancellationToken);
    }
    

    InMemoryUserStore and the abstract UserStoreBase classes provide implementations for the IUserAuthenticatorKeyStore interface. As shown below, both of them use the same values for the LoginProvider name, the AuthenticatorKeyTokenName and the RecoveryCodeTokenName.

    private const string AuthenticatorStoreLoginProvider = "[AspNetAuthenticatorStore]";
    private const string AuthenticatorKeyTokenName = "AuthenticatorKey";
    private const string RecoveryCodeTokenName = "RecoveryCodes";
    

    Two-Factor Authentication functionality is also available with the In-Memory database provider. To test it in our app, use “InMemoryProvider”: true in appsettings.json

    We haven’t seen yet anything about recovery codes, but you can already guess that when created, a new record in the AspNetUserTokens table will be added, having the same LoginProvider value but RecoveryCodes as the Name value. Let’s pause for a little and understand what happened when you clicked the Setup authenticator button. A GET request made to the SetupAuthenticator action.

    [HttpGet]
    [Authorize]
    public async Task<AuthenticatorDetailsVM> SetupAuthenticator()
    {
        var user = await _userManager.GetUserAsync(User);
        var authenticatorDetails = await GetAuthenticatorDetailsAsync(user);
    
        return authenticatorDetails;
    }
    
    private async Task<AuthenticatorDetailsVM> GetAuthenticatorDetailsAsync(IdentityUser user)
    {
        // Load the authenticator key & QR code URI to display on the form
        var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
        if (string.IsNullOrEmpty(unformattedKey))
        {
            await _userManager.ResetAuthenticatorKeyAsync(user);
            unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
        }
    
        var email = await _userManager.GetEmailAsync(user);
    
        return new AuthenticatorDetailsVM
        {
            SharedKey = FormatKey(unformattedKey),
            AuthenticatorUri = GenerateQrCodeUri(email, unformattedKey)
        };
    }
    

    If no authenticator key found we call the _userManager.ResetAuthenticatorKeyAsync(user) method to create one. This method can also be used to Reset authenticator app which simply changes the key value on the store. The FormatKey method just adds a space every 4 letters so it can more readable to user. To create a QR Code you need a valid authenticator app Uri that contains all the required information for your authenticator app to work properly.

    private string GenerateQrCodeUri(string email, string unformattedKey)
    {
        const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
    
        return string.Format(
            AuthenticatorUriFormat,
            _urlEncoder.Encode("ASP.NET Core Identity"),
            _urlEncoder.Encode(email),
            unformattedKey);
    }
    

    If you hover on the QR Code generated you can see this URI.

    The most important is that contains the key to be used for generating 6 digits authentication tokens using the Time-based One-time Password Algorithm (TOTP). In javascript, a qr code library used to paint the QR Code. As you can see you can configure several properties to match your requirements.

    self.generatedQRCode = new QRCode(document.getElementById("genQrCode"),
        {
            text: self.authenticatorDetails.authenticatorUri,
            width: 150,
            height: 150,
            colorDark: "#000",
            colorLight: "#ffffff",
            correctLevel: QRCode.CorrectLevel.H
        });
    

    Proceed with either scanning the QR Code on your authenticator app in your smartphone or by manually typing it. Google Authenticator and AUTHY are the most popular authenticator apps. When you click the Verify button, 2FA will be enabled for your account and you will also get 10 recovery codes.

    Make sure to copy and save these codes. Let’s see the VefiryAuthenticator action in code.

    public async Task<ResultVM> VerifyAuthenticator([FromBody] VefiryAuthenticatorVM verifyAuthenticator)
    {
        // code omitted
    
        var verificationCode = verifyAuthenticator.VerificationCode.Replace(" ", string.Empty).Replace("-", string.Empty);
    
        var is2FaTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
            user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
    
        await _userManager.SetTwoFactorEnabledAsync(user, true);
    
        var result = new ResultVM
        {
            Status = Status.Success,
            Message = "Your authenticator app has been verified",
        };
    
        if (await _userManager.CountRecoveryCodesAsync(user) != 0) return result;
    
        var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
        result.Data = new { recoveryCodes };
        return result;
    }
    

    The _userManager.VerifyTwoFactorTokenAsync is the method that knows to verify an authenticator token.

    public virtual async Task<bool> VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token)
    {
        // Make sure the token is valid
        var result = await _tokenProviders[tokenProvider].ValidateAsync("TwoFactor", token, this, user);
        if (!result)
        {
            Logger.LogWarning(10, $"{nameof(VerifyTwoFactorTokenAsync)}() failed for user {await GetUserIdAsync(user)}.");
        }
        return result;
    }
    

    The ValidateAsync method exists in an AuthenticatorTokenProvider class that implements the IUserTwoFactorTokenProvider interface.

    public virtual async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user)
    {
        var key = await manager.GetAuthenticatorKeyAsync(user);
        int code;
        if (!int.TryParse(token, out code))
        {
            return false;
        }
    
        var hash = new HMACSHA1(Base32.FromBase32(key));
        var unixTimestamp = Convert.ToInt64(Math.Round((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds));
        var timestep = Convert.ToInt64(unixTimestamp / 30);
        // Allow codes from 90s in each direction (we could make this configurable?)
        for (int i = -2; i <= 2; i++)
        {
            var expectedCode = Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)(timestep + i), modifier: null);
            if (expectedCode == code)
            {
                return true;
            }
        }
        return false;
    }
    

    Here is where the generated authenticator’s app token is being validated. Back to the VerifyAuthenticator action the code checks if there any recovery tokens exist and if not, creates 10 of them using the userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10) method. The default implementation will add the generated codes semicolon seperated as shown in the database screenshot below.

    Authenticating with 2FA

    At this point, you should have 2FA configured and enabled for your account. Logout and try to sign in, you should be asked to enter either the 6-digit code generated by the authenticator app configured on your phone or use a recovery code generated before.

    The login action will end up in the TwoFaLogin private method.

    private async Task<ResultVM> TwoFaLogin(string code, bool isRecoveryCode, bool rememberMachine = false)
    {
        SignInResult result = null;
    
        var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
    
        var authenticatorCode = code.Replace(" ", string.Empty).Replace("-", string.Empty);
    
        if (!isRecoveryCode)
        {
            result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, true,
                rememberMachine);
        }
        else
        {
            result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(authenticatorCode);
        }
    
        // Code omitted
    }
    

    The code is self explanatory. First it needs to know which user tried to login and presented with the 2FA form. Next, depending on whether the user chose to login with an generated or a recovery code calls the _signInManager.TwoFactorAuthenticatorSignInAsync or _signInManager.TwoFactorRecoveryCodeSignInAsync respectively. On the first case the SignInManager uses the UserManager VerifyTwoFactorTokenAsync we described before and if the token is valid signs in the user.

    public virtual async Task<SignInResult> TwoFactorAuthenticatorSignInAsync(string code, bool isPersistent, bool rememberClient)
        {
            // code omitted
            var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId);
        
            if (await UserManager.VerifyTwoFactorTokenAsync(user, Options.Tokens.AuthenticatorTokenProvider, code))
            {
                await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient);
                return SignInResult.Success;
            }
            // If the token is incorrect, record the failure which also may cause the user to be locked out
            await UserManager.AccessFailedAsync(user);
            return SignInResult.Failed;
        }
    

    If the Remember machine checkbox is checked then an Identity.TwoFactorRememberMe cookie will be saved in your browser and you wont be asked again to type an authenticator code on the browser you used.

    In the second case with the recovery code, it uses the UserManager.RedeemTwoFactorRecoveryCodeAsync method to check that the recovery code provided is valid.

    public virtual async Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string recoveryCode)
    {
        // code omitted
        var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId);
    
        var result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode);
        if (result.Succeeded)
        {
            await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false);
            return SignInResult.Success;
        }
    
        // We don't protect against brute force attacks since codes are expected to be random.
        return SignInResult.Failed;
    }
    

    Here’s the entire flow using a GIF (click it to view it in better quality..).

    Reset recovery codes

    After enabling 2FA, two buttons will be enabled in the app, the Reset recovery codes and the Disable 2FA.

    Clicking the Reset recovery codes button will hit the GenerateRecoveryCodes action in the TwoFactorAuthenticationController.

    [HttpPost]
    [Authorize]
    public async Task<ResultVM> GenerateRecoveryCodes()
    {
        var user = await _userManager.GetUserAsync(User);
    
        var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
    
        // code omitted
    
        var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
    
        return new ResultVM
        {
            Status = Status.Success,
            Message = "You have generated new recovery codes",
            Data = new { recoveryCodes }
        };
    }
    

    The new codes are generated with the same way generated on the first time and replace the existing ones using the ReplaceCodesAsync method.

    public virtual async Task<IEnumerable<string>> GenerateNewTwoFactorRecoveryCodesAsync(TUser user, int number)
    {
        var store = GetRecoveryCodeStore();
    
        var newCodes = new List<string>(number);
        for (var i = 0; i < number; i++)
        {
            newCodes.Add(CreateTwoFactorRecoveryCode());
        }
    
        await store.ReplaceCodesAsync(user, newCodes.Distinct(), CancellationToken);
        var update = await UpdateAsync(user);
        if (update.Succeeded)
        {
            return newCodes;
        }
        return null;
    }
    

    Always save the recovery tokens somewhere secure, maybe on the cloud. In case you lose your phone you will need them to sign in and reset or disable 2FA on your account

    Reset authenticator

    Clicking the Disable 2FA button will hit the Disable2FA action which simply updates the TwoFactorEnabled property on the user to False.

    [HttpPost]
    [Authorize]
    public async Task<ResultVM> Disable2FA()
    {
        var user = await _userManager.GetUserAsync(User);
    
        if (!await _userManager.GetTwoFactorEnabledAsync(user))
        {
            return new ResultVM
            {
                Status = Status.Error,
                Message = "Cannot disable 2FA as it's not currently enabled"
            };
        }
    
        var result = await _userManager.SetTwoFactorEnabledAsync(user, false);
    
        return new ResultVM
        {
            Status = result.Succeeded ? Status.Success : Status.Error,
            Message = result.Succeeded ? "2FA has been successfully disabled" : $"Failed to disable 2FA {result.Errors.FirstOrDefault()?.Description}"
        };
    }
    

    Remember: This won’t affect your authenticator app configuration in your phone because nothing happens at the AspNetUserTokens table. To re-enable it, click the Re-enable 2FA button which is actually the same Setup authenticator button.

    In case you haven’t reset the authenticator before disabling the 2FA, you can use the tokens being generating by your authenticator app, they will be still valid because the authenticator key remains the same in the database. On the other hand, if you reset the authenticator, the key changes so your authenticator app gets useless and its tokens will be invalid. In this case, you need to re-configured (remove and then add) the authenticator key in your app.

    Secure authenticator key and recovery tokens

    From what we have said so far it should be clear that the web app and the authenticator app in our smartphones share the authenticator key. Leaving the default implementation for generating authenticator keys and recovery tokens can be dangerous and expose your accounts to potential hackers. The reason is that everyone that gets access to the authenticator key in the database can create and use valid codes, hence access your account. Databases can be compromised in many ways such as SQL Injection. First, let’s confirm that we can generate valid authenticator codes without the help of the authenticator app in our smartphones. Check the Show possible verification codes checkbox in the QR Code screen and see what happens.

    You can click on one of the codes returned and confirm that verification will pass successfully. Moreover, the most interesting thing is that your authenticator app in your phone will always display one of the codes you see in green…
    When the checkbox is checked, a polling to the ValidAutheticatorCodes action method will start.

    [HttpGet]
    [Authorize]
    public async Task<List<int>> ValidAutheticatorCodes()
    {
        List<int> validCodes = new List<int>();
    
        var user = await _userManager.GetUserAsync(User);
    
        var key = await _userManager.GetAuthenticatorKeyAsync(user);
    
        var hash = new HMACSHA1(Infrastructure.Identity.Internals.Base32.FromBase32(key));
        var unixTimestamp = Convert.ToInt64(Math.Round((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds));
        var timestep = Convert.ToInt64(unixTimestamp / 30);
        // Allow codes from 90s in each direction (we could make this configurable?)
        for (int i = -2; i <= 2; i++)
        {
            var expectedCode = Infrastructure.Identity.Internals.Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)(timestep + i), modifier: null);
            validCodes.Add(expectedCode);
        }
    
        return validCodes;
    }
    

    Do you remember this code? That’s the exact code used in the ValidateAsync method we described before! Of course Base32 and Rfc6238AuthenticationService classes were internal so I fetched them to our app for demonstration reasons. To be honest, I already feel sick knowing that even if I have 2FA enabled my account could be compromised that easy. So can we do something about it? The answer is yes.

    Encrypting Authenticator key

    The first thing to enhance 2FA security is to encrypt the authenticator key stored in the database. So when a new authenticator key is created, instead of this…

    .. we wish to have this!

    Now the value is encrypted and if even if revealed to hackers they are useless because they have no clue on how to get the original value. Let’s get to the implementation for securing the authenticator key. First, you will find a TwoFactorAuthentication:EncryptionEnabled property in the appsettings.json which when equals True, tokens will be encrypted.

    "TwoFactorAuthentication:EncryptionEnabled":  true 
    

    Encryption – Decryption Disclaimer

    We won’t be doing the Ninja with the encryption & decryption here but instead we will use an open-source library named NETCore.Encrypt for simplicity. We will use an AES algorithm which is a symmetric-key algorithm, meaning the same key is used for both encrypting and decrypting the data. You are free though to use your own super secure encryption/decryption logic.
    AES needs a symmetric-key to setup and to get one, fire the app and navigate to http://localhost:5000/api/twofactorauthentication/aeskey. Copy the code, open a terminal, cd to the AspNetCoreIdentity folder and run the following command to set it as a secret key.

        dotnet user-secrets set "TwoFactorAuthentication:EncryptionKey" "<eas-key-here>"
        

    To encrypt the authenticator key we need to override two methods in the UserManager which means we have to create our own implementation of the UserManager and use it in ASP.NET Core Identity. In our case we created the AppUserManager.

    In the Startup configuration we have to tell Identity to use our UserManager implementation.

    services.AddIdentity<IdentityUser, IdentityRole>(config => { config.SignIn.RequireConfirmedEmail = true; })
        .AddEntityFrameworkStores<IdentityDbContext>()
        .AddUserManager<AppUserManager>()
        .AddDefaultTokenProviders();
    

    To understand which methods needs to be overridden we need to trace the ResetAuthenticatorKeyAsync which generates the authenticator tokens.

    public virtual async Task<IdentityResult> ResetAuthenticatorKeyAsync(TUser user)
    {
        ThrowIfDisposed();
        var store = GetAuthenticatorKeyStore();
        if (user == null)
        {
            throw new ArgumentNullException(nameof(user));
        }
        await store.SetAuthenticatorKeyAsync(user, GenerateNewAuthenticatorKey(), CancellationToken);
        await UpdateSecurityStampInternal(user);
        return await UpdateAsync(user);
    }
    

    The GenerateNewAuthenticatorKey is the one that does the job..

    public virtual string GenerateNewAuthenticatorKey()
                => NewSecurityStamp();
    
    private static string NewSecurityStamp()
    {
        byte[] bytes = new byte[20];
        _rng.GetBytes(bytes);
        return Base32.ToBase32(bytes);
    }
    

    So all we have to do to encrypt the authenticator key is to provide a new implementation for the GenerateNewAuthenticatorKey method. Back to the AppUserManager..

    public override string GenerateNewAuthenticatorKey()
    {
        var originalAuthenticatorKey = base.GenerateNewAuthenticatorKey();
    
        // var aesKey = EncryptProvider.CreateAesKey();
    
        bool.TryParse(_configuration["TwoFactorAuthentication:EncryptionEnabled"], out bool encryptionEnabled);
    
        var encryptedKey = encryptionEnabled
            ? EncryptProvider.AESEncrypt(originalAuthenticatorKey, _configuration["TwoFactorAuthentication:EncryptionKey"])
            : originalAuthenticatorKey;
    
        return encryptedKey;
    }
    

    The code first creates the default key and then if encryption is enabled, encrypts it. This is not the only thing required to secure the authenticator key. If you leave it as is you won’t be able to generate or verify tokens because the app at this point doesn’t know how to deal with the encrypted value. The next step is to provide an override for reading the encrypted authenticator key. We have already mentioned the ValidateAsync method that validates the tokens being created by the authenticator app.

    public virtual async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user)
    {
        var key = await manager.GetAuthenticatorKeyAsync(user);
        int code;
        if (!int.TryParse(token, out code))
        {
            return false;
        }
    
        var hash = new HMACSHA1(Base32.FromBase32(key));
        var unixTimestamp = Convert.ToInt64(Math.Round((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds));
        var timestep = Convert.ToInt64(unixTimestamp / 30);
        // Allow codes from 90s in each direction (we could make this configurable?)
        for (int i = -2; i <= 2; i++)
        {
            var expectedCode = Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)(timestep + i), modifier: null);
            if (expectedCode == code)
            {
                return true;
            }
        }
        return false;
    }
    

    All we have to do is override the UserManager.GetAuthenticatorKeyAsync method. Back to the AppUserManager

    public override async Task<string> GetAuthenticatorKeyAsync(IdentityUser user)
    {
        var databaseKey = await base.GetAuthenticatorKeyAsync(user);
    
        if (databaseKey == null)
        {
            return null;
        }
    
        // Decryption
        bool.TryParse(_configuration["TwoFactorAuthentication:EncryptionEnabled"], out bool encryptionEnabled);
    
        var originalAuthenticatorKey = encryptionEnabled
            ? EncryptProvider.AESDecrypt(databaseKey, _configuration["TwoFactorAuthentication:EncryptionKey"])
            : databaseKey;
    
        return originalAuthenticatorKey;
    }
    

    The code gets the authenticator key existing in the store and if encryption is enabled returns the decrypted one. That’s all you have to do to support encrypted authentication keys.

    Encrypting recovery codes

    Encrypting the recovery codes follows almost the same logic. Instead of having hard coded the codes in the store we wish to end up in an encrypted text as follow.

    We override the way each recovery token is created, not all tokens together by providing an implementation for the CreateTwoFactorRecoveryCode in the AppUserManager.

    protected override string CreateTwoFactorRecoveryCode()
    {
        var originalRecoveryCode = base.CreateTwoFactorRecoveryCode();
    
        bool.TryParse(_configuration["TwoFactorAuthentication:EncryptionEnabled"], out bool encryptionEnabled);
    
        var encryptedRecoveryCode = encryptionEnabled
            ? EncryptProvider.AESEncrypt(originalRecoveryCode, _configuration["TwoFactorAuthentication:EncryptionKey"])
            : originalRecoveryCode;
    
        return encryptedRecoveryCode;
    }
    

    First we create the recovery token in the way it used to and then if encryption is enabled we encrypt it. The next step is to re-implement the GenerateNewTwoFactorRecoveryCodesAsync which returns the plain-text list of recovery tokens generated. We want this one because each recovery token in the database is now encrypted.

    public override async Task<IEnumerable<string>> GenerateNewTwoFactorRecoveryCodesAsync(IdentityUser user, int number)
    {
        var tokens = await base.GenerateNewTwoFactorRecoveryCodesAsync(user, number);
    
        var generatedTokens = tokens as string[] ?? tokens.ToArray();
        if (!generatedTokens.Any())
        {
            return generatedTokens;
        }
    
        bool.TryParse(_configuration["TwoFactorAuthentication:EncryptionEnabled"], out bool encryptionEnabled);
    
        return encryptionEnabled
            ? generatedTokens
                .Select(token =>
                    EncryptProvider.AESDecrypt(token, _configuration["TwoFactorAuthentication:EncryptionKey"]))
            : generatedTokens;
    
    }
    

    The last step for completing the recovery token encryption is to override the RedeemTwoFactorRecoveryCodeAsync method in the AppUserManager. This method verifies a recovery token provided by the user. The logic here is slightly different than the authenticator key meaning that the recovery code provided by the user must be encrypted (not decrypted) in order to be matched with one existing in the store.

    public override Task<IdentityResult> RedeemTwoFactorRecoveryCodeAsync(IdentityUser user, string code)
    {
        bool.TryParse(_configuration["TwoFactorAuthentication:EncryptionEnabled"], out bool encryptionEnabled);
    
        if (encryptionEnabled && !string.IsNullOrEmpty(code))
        {
            code = EncryptProvider.AESEncrypt(code, _configuration["TwoFactorAuthentication:EncryptionKey"]);
        }
    
        return base.RedeemTwoFactorRecoveryCodeAsync(user, code);
    }
    

    Discussion

    You must be aware that switching encryption from enabled to disabled or vice versa will break the two-factor authentication feature in your app. For example, if an authenticator key and the recovery tokens were created when the encryption was enabled and you decide to disable the encryption, then the user won’t be able to sign in. It means that this is a one-time decision to be made rather than switching it any time you want. My recommendation is always use the encryption and in case your app already supports 2FA with its default implementation, migrate to the encryption by running any required scripts in your database.

    You probably noticed that when you checked the Show possible verification codes 5 valid codes are always displayed and every few seconds a new one is added. This sliding behavior is explained in a comment at the original code..

    public virtual async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user)
    {
        var key = await manager.GetAuthenticatorKeyAsync(user);
        int code;
        if (!int.TryParse(token, out code))
        {
            return false;
        }
     
        var hash = new HMACSHA1(Base32.FromBase32(key));
        var unixTimestamp = Convert.ToInt64(Math.Round((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds));
        var timestep = Convert.ToInt64(unixTimestamp / 30);
        // Allow codes from 90s in each direction (we could make this configurable?)
        for (int i = -2; i <= 2; i++)
        {
            var expectedCode = Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)(timestep + i), modifier: null);
            if (expectedCode == code)
            {
                return true;
            }
        }
        return false;
    }
    

    I really enjoyed the question “we could make this configurable?”.. Of course, the reason you get 5 valid codes is that there are 5 iterations from -2 to 2. If we could change this to -1 to 1 then we would get only 3 valid codes at a time which is kind of more strict. Anyway, the reason I am mentioning this is that sometimes while typing the code generated by the authenticator on your phone, you might believe it won’t work cause it seems to be expired on the app right before you have actually managed to enter it. You will be surprised though that most of the times it actually works due to this 90s in each direction.. If the code you see in your app is at the middle of the list presented above then you probably have more time than you think to use it. If though the code is the one to be replaced on the next iteration then make sure to enter it as long as you see it in the authenticator.

    Another thing to keep in mind is the white area around the QR Code. This is not for our eyes, it’s a requirement for the authenticator apps to work and detect the QR Code properly. Always remember to leave some extra white pixels around the QR Code

    That’s it, we have finished! We have implemented all the 2FA related tasks in our app and explored what happens at the store level. Last but not least we saw how to enhance 2FA security by encrypting the authenticator tokens and recovery codes being generating in the store.

    In case you find my blog’s content interesting, register your email to receive notifications of new posts and follow chsakell’s Blog on its Facebook or Twitter accounts.

    Facebook Twitter
    .NET Web Application Development by Chris S.
    facebook twitter-small
    twitter-small
    Viewing all 43 articles
    Browse latest View live