Build your own OAuth 2.0 Server and OpenId Connect Provider in ASP.NET Core 6.0

Mohammed Ahmed Hussien - Oct 28 '22 - - Dev Community

Update 20, April 2023
This article is out of date because I added more functionality to this OAuth server, so to be more updated I suggest downloading the project or clone it from my GitHub repo here


Yeah, I know the title of this post is very interesting, and I hope you enjoy this journey to building your OAuth 2.0 Server and OpenId Connect Provider with me.
And I and you, we are a team.
But hey wait, there is also one member in our team, let me introduce you to him, Specification

I'm not going here to explain to you how the OAuth 2.0 and OpenId Connect works, but I will explain for you how to implement what inside specifications, at least the basics and fundamentals.
The Authorization Server that we are going to build is very simple but is complete one.

If you are not interesting to read this article don't worry you can download the completed source code from my Github repo from here

First of all, let me give you an overview about what we're going to build right here.

  • Authorization Server (in ASP.NET Core MVC)
  • ASP.NET Core MVC Application

First let us Create our Client application (ASP.NET Core MVC)

Open the Visual Studio and create an Empty ASP.NET Core App (see below) named PlatformNet6 (you can give it a name you like)

Image description

Choose NET6 (LTS) version (see the pic below)

Image description

Create a folder named Controllers and inside this folder create a new controller class named HomeController.cs
In the recent HomeController class you will find one Action Method named Index, Press right click on the name of the project and create a new folder named Views, back to Index Action Method on the HomeControllers.cs press right click on it and choose the Add View option from the menu.
The HomeController class should look like:



namespace PlatformNet6.Controllers
{
    [Authorize]
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

By using Nuget Packages install this Library:



Microsoft.AspNetCore.Authentication.OpenIdConnect


Enter fullscreen mode Exit fullscreen mode

Open the Program.cs class and drop the code that shown below:



var builder = WebApplication.CreateBuilder(args);
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    builder.Services.AddAuthentication(config =>
    {
        config.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        config.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
     .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
     .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            // this is my Authorization Server Port
            options.Authority = "https://localhost:7275";
            options.ClientId = "platformnet6";
            options.ClientSecret = "123456789";
            options.ResponseType = "code";
            options.CallbackPath = "/signin-oidc";
            options.SaveTokens = true;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = false,
                SignatureValidator = delegate(string token, TokenValidationParameters validationParameters)
                {
                    var jwt = new JwtSecurityToken(token);
                    return jwt;
                },
            };
        });
builder.Services.AddControllersWithViews();

 var app = builder.Build();
    app.UseStaticFiles();
    app.UseRouting();


    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
    app.Run();


Enter fullscreen mode Exit fullscreen mode

In the above code I use the AddOpenIdConnect extension that inform ASP.NET Core to redirect the app to the Authorization Server when find any Authorize attribute on any Action Method or Controller. I'm going to use Cookies as Default authentication scheme and OpenIdConnect as Default Challenge Scheme.
Oooooooops we finish our client app, we will back to this application to complete the content of the Index.cshtml page.

The Authorization Server Project

In this section we are going to build our Authorization Server and this section is the mail section in this Post.

Open Visual Studio and create an Empty ASP.NET Core App (see below) named OAuth20.Server (you can give it a name you like)
Please look at the name of the solution is OAuth20

Image description

Choose NET6 (LTS) version (see the pic below)

Image description

Create these folders in the root of our application:

  • Controllers
  • Common
  • Endpoints
  • Models
  • OauthRequest
  • OauthResponse
  • Services
  • Views

The structure of our Authorization Server looks like the below picture

Image description

By using Nuget Packages install this Library:



System.IdentityModel.Tokens.Jwt


Enter fullscreen mode Exit fullscreen mode

If you look at the specification https://www.rfc-editor.org/rfc/rfc6749 the main role of the OAuth 2.0 is the Client and the client here is the Application that used by the Resource Owner and the Resource Owner is the person who used that application. For example, I'm Mohammed Ahmed Hussien and I used the Dev.to to read the latest articles about Software.
Here Dev.to is the Client and Mohammed Ahmed Hussien is the Resource Owner.
Inside Model folder create a new class named Client.cs



using System.Collections.Generic;

namespace OAuth20.Server.Models
{
    public class Client
    {
        public Client()
        {

        }

        public string ClientName { get; set; }
        public string ClientId { get; set; }

        /// <summary>
        /// Client Password
        /// </summary>
        public string ClientSecret { get; set; }

        public IList<string> GrantType { get; set; }

        /// <summary>
        /// by default false
        /// </summary>
        public bool IsActive { get; set; } = false;
        public IList<string> AllowedScopes { get; set; }

        public string ClientUri { get; set; }
        public string RedirectUri { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

The specification says that the RedirectUri must accept an array of the URI, and as you see in our Client.cs class the RedirectUri is accept only string value not Array values. But I will promise you I will fix that later, trust me!

OpenId Connect build on top of the OAuth2.0 protocol and the main purpose of it is to authenticate the user. And if you remember we use the AddOpenIdConnect extension to authenicate our users in the PlatformNet6 application this extension scans for endpoint have name .well-known/openid-configuration and this endpoint shoud return json response with all the information that OpenId Connect needs, consult the OpenId Connect Docs for more details from here: https://openid.net/specs/openid-connect-discovery-1_0.html
Inside Endpoints folder create a new class named DiscoveryResponse.cs and paste the next code inside it:



using System.Collections.Generic;

namespace OAuth20.Server.Endpoints
{
    public class DiscoveryResponse
    {
        public string issuer { get; set; }
        public string authorization_endpoint { get; set; }
        public string token_endpoint { get; set; }
        public IList<string> token_endpoint_auth_methods_supported { get; set; }
        public IList<string> token_endpoint_auth_signing_alg_values_supported { get; set; }
        public string userinfo_endpoint { get; set; }
        public string check_session_iframe { get; set; }
        public string end_session_endpoint { get; set; }
        public string jwks_uri { get; set; }
        public string registration_endpoint { get; set; }
        public IList<string> scopes_supported { get; set; }
        public IList<string> response_types_supported { get; set; }
        public IList<string> acr_values_supported { get; set; }
        public IList<string> subject_types_supported { get; set; }
        public IList<string> userinfo_signing_alg_values_supported { get; set; }
        public IList<string> userinfo_encryption_alg_values_supported   { get; set; }
        public IList<string> userinfo_encryption_enc_values_supported { get; set; }
        public IList<string> id_token_signing_alg_values_supported { get; set; }
        public IList<string> id_token_encryption_alg_values_supported { get; set; }
        public IList<string> id_token_encryption_enc_values_supported { get; set; }
        public IList<string> request_object_signing_alg_values_supported { get; set; }
        public IList<string> display_values_supported { get; set; }
        public IList<string> claim_types_supported { get; set; }
        public IList<string> claims_supported { get; set; }
        public bool claims_parameter_supported { get; set; }
        public string service_documentation { get; set; }
        public IList<string> ui_locales_supported { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

The most important properties of this class is:

  • issuer (is the domain name of the Identity Provider)
  • authorization_endpoint (the endpoint that validate the client)
  • token_endpoint (the return the Identity Token and the Access Token to the client) As I said before the AddOpenIdConnect extension scans for endpoint with name .well-known/openid-configuration, so let us create this endpoint

Inside the Controllers folder create a new controller class named DiscoveryEndpointController.cs paste the code that shown next:



using Microsoft.AspNetCore.Mvc;
using OAuth20.Server.Endpoints;

namespace OAuth20.Server.Controllers
{
    public class DiscoveryEndpointController : Controller
    {
        // .well-known/openid-configuration
        [HttpGet("~/.well-known/openid-configuration")]
        public JsonResult GetConfiguration()
        {
            var response = new DiscoveryResponse
            {
                issuer = "https://localhost:7275",
                authorization_endpoint = "https://localhost:7275/Home/Authorize",
                token_endpoint = "https://localhost:7275/Home/Token",
                token_endpoint_auth_methods_supported = new string[] { "client_secret_basic", "private_key_jwt" },
                token_endpoint_auth_signing_alg_values_supported = new string[] { "RS256", "ES256" },

                acr_values_supported = new string[] {"urn:mace:incommon:iap:silver", "urn:mace:incommon:iap:bronze"},
                response_types_supported = new string[] { "code", "code id_token", "id_token", "token id_token" },
                subject_types_supported = new string[] { "public", "pairwise" },

                userinfo_encryption_enc_values_supported = new string[] { "A128CBC-HS256", "A128GCM" },
                id_token_signing_alg_values_supported = new string[] { "RS256", "ES256", "HS256" },
                id_token_encryption_alg_values_supported = new string[] { "RSA1_5", "A128KW" },
                id_token_encryption_enc_values_supported = new string[] { "A128CBC-HS256", "A128GCM" },
                request_object_signing_alg_values_supported = new string[] { "none", "RS256", "ES256" },
                display_values_supported = new string[] { "page", "popup" },
                claim_types_supported = new string[] { "normal", "distributed" },

                scopes_supported = new string[] { "openid", "profile", "email", "address", "phone", "offline_access" },
                claims_supported = new string[] { "sub", "iss", "auth_time", "acr", "name", "given_name",
                    "family_name", "nickname", "profile", "picture", "website", "email", "email_verified",
                    "locale", "zoneinfo" },
                claims_parameter_supported = true,
                service_documentation = "https://localhost:7275/connect/service_documentation.html",
                ui_locales_supported = new string[] { "en-US", "en-GB", "en-CA", "fr-FR", "fr-CA" }

            };
            return Json(response);
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

The return type of this Action Method is josn result, and the name of this endpoint is decorated inside HttpGet attribute



 [HttpGet("~/.well-known/openid-configuration")]


Enter fullscreen mode Exit fullscreen mode

Be sure to change the Ports of the endpoints to that one Visual Studio created for you.

My own port is https://localhost:7275

To check if everything is works correctly hit a Breakpoint in this endpoint (.well-known/openid-configuration) and run the OAuth20.Server Authorization Server app, after that run the PlatformNet6 application, you will find the breakpoint is triggered.

Be sure of the setting at the PlatformNet6 application



     .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            // this is my Authorization Server Port
            options.Authority = "https://localhost:7275";
            options.ClientId = "platformnet6";
            options.ClientSecret = "123456789";
            options.ResponseType = "code";
            options.CallbackPath = "/signin-oidc";
            options.SaveTokens = true;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = false,
                SignatureValidator = delegate(string token, TokenValidationParameters validationParameters)
                {
                    var jwt = new JwtSecurityToken(token);
                    return jwt;
                },
            };
        });


Enter fullscreen mode Exit fullscreen mode

So far so good at this stage we prepare the Authorization Server to accept calls from any client.
Let us break the ice by creating a common C# extension Method
Inside Common folder create a new class named ExtensionMethods.cs this class has a signature as follow:



using System.ComponentModel;
using System;
using System.Linq;

namespace OAuth20.Server.Common
{
    public static class ExtensionMethods
    {
        public static string GetEnumDescription(this Enum en)
        {
            if (en == null) return null;

            var type = en.GetType();

            var memberInfo = type.GetMember(en.ToString());
            var description = (memberInfo[0].GetCustomAttributes(typeof(DescriptionAttribute),
                false).FirstOrDefault() as DescriptionAttribute)?.Description;

            return description;
        }



        public static bool IsRedirectUriStartWithHttps(this string redirectUri)
        {
            if(redirectUri != null && redirectUri.StartsWith("https")) return true;

            return false;
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Instead of creating a static class with const fields of type string I will use Enum type and any filed inside the Enum I will decorate it with the Description attribute, so If I need a value inside this attribute I will use GetEnumDescription extension method.

Inside Model folder create enum type named AuthorizationGrantTypesEnum.cs



using System.ComponentModel;

namespace OAuth20.Server.Models
{
    internal enum AuthorizationGrantTypesEnum : byte
    {
        [Description("code")]
        Code,

        [Description("Implicit")]
        Implicit,

        [Description("ClientCredentials")]
        ClientCredentials,

        [Description("ResourceOwnerPassword")]
        ResourceOwnerPassword
    }
}


Enter fullscreen mode Exit fullscreen mode

Back to Model folder the create a new class named GrantTypes.cs



using OAuth20.Server.Common;
using System.Collections.Generic;

namespace OAuth20.Server.Models
{
    public class GrantTypes
    {
        public static IList<string> Code =>
            new[] { AuthorizationGrantTypesEnum.Code.GetEnumDescription() };

        public static IList<string> Implicit =>
            new[] { AuthorizationGrantTypesEnum.Implicit.GetEnumDescription() };
        public static IList<string> ClientCredentials =>
            new[] { AuthorizationGrantTypesEnum.ClientCredentials.GetEnumDescription() };
        public static IList<string> ResourceOwnerPassword =>
            new[] { AuthorizationGrantTypesEnum.ResourceOwnerPassword.GetEnumDescription() };
    }
}


Enter fullscreen mode Exit fullscreen mode

Consult the specification for more details about this class from here https://www.rfc-editor.org/rfc/rfc6749#page-23

The second endpoint that the AddOpenIdConnect extension looks for it is the Authorization endpoint and the mission of this endpoint is validate the client. Before going deeper let us create our client store object, this object will hold all clients that would like to use our Authorization Server. Inside Model folder create a new class named ClientStore.cs and the content of this class it looks as follow:



using System.Collections.Generic;

namespace OAuth20.Server.Models
{
    public class ClientStore
    {
        public IEnumerable<Client> Clients = new[]
        {
            new Client
            {
                ClientName = "platformnet .Net 6",
                ClientId = platformnet6",
                ClientSecret = "123456789",
                AllowedScopes = new[]{ "openid", "profile"},
                GrantType = GrantTypes.Code,
                IsActive = true,
                ClientUri = "https://localhost:7026",
                RedirectUri = "https://localhost:7026/signin-oidc"
            }
        };
    }
}


Enter fullscreen mode Exit fullscreen mode

The ClientUri and RedirectUri it should mention the PlatformNet6 application (Client) that we created previously at beginning of this post. Be sure to replace the Port with the one that Visual Studio created for you, (Just the port). the ClientName should match the one that you introduce in the PlatformNet6 as well as ClientId and ClientSecret and GrantType

Please let us take a break and come back to complete this post, I feel tired, and I have to take a cup of tea

So, I'm here, and I'm so glad you are here also with me at this stage!

Let's creating the Authorization endpoint but before that I need to explain how this endpoint works.
If you remember when we configure the .well-known/openid-configuratio endpoint we assign https://localhost:7275/Home/Authorize as value of the authorization_endpoint property, so the OpenIdConnect extension scan for the endpoint with name .well-known/openid-configuratio and it expect to find in the response a location of the authorization_endpoint, like so:



 authorization_endpoint = "https://localhost:7275/Home/Authorize"


Enter fullscreen mode Exit fullscreen mode

The authorization_endpoint is the second place the AddOpenIdConnect looks for, and in that request the AddOpenIdConnect take more than one parameter with the current request (HTTPContext), let's take a look.
Create a new class inside OauthRequest folder named AuthorizationRequest.cs and the content of this class as follow:




namespace OAuth20.Server.OauthRequest
{
    public class AuthorizationRequest
    {
        public AuthorizationRequest() { }
        /// <summary>
        /// Response Type, is required
        /// </summary>
        public string response_type { get; set; }

        /// <summary>
        /// Client Id, is required
        /// </summary>

        public string client_id { get; set; }

        /// <summary>
        /// Redirect Uri, is optional
        /// The redirection endpoint URI MUST be an absolute URI as defined by
        /// [RFC3986] Section 4.3
        /// </summary>

        public string redirect_uri { get; set; }

        /// <summary>
        /// Optional
        /// </summary>
        public string scope { get; set; }

        /// <summary>
        /// Return the state in the result 
        /// if it was present in the client authorization request
        /// </summary>
        public string state { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

So, let's dive in and create the Authorization endpoint.

Inside Controllers folder create a new controller named HomeController.cs, see the content of this controller below:



using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using OAuth20.Server.OauthRequest;


namespace OAuth20.Server.Controllers
{
    public class HomeController : Controller
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        public HomeController(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }


        public IActionResult Authorize(AuthorizationRequest authorizationRequest)
        {
            // The implementation goes here
        }
        public IActionResult Error(string error)
        {
            return View(error);
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

If look carefully I use the IHttpContextAccessor interface that provide access to the current HttpContext's Request.
You have to register this interface at Program.cs file like so:



builder.Services.AddHttpContextAccessor();


Enter fullscreen mode Exit fullscreen mode

Inside HomeController class I defined two Action Method:

  • Authorize
  • Error

The last one is very simple if there is any error has been happening, then redirect the Resource Owner to the Error action method, this Action Method take one parameter of type string, and the name of this errors has standard name, let us create a Enum class take care of all these error name.
Inside OauthResponse folder create class named ErrorTypeEnum.cs as follow:



using System.ComponentModel;
namespace OAuth20.Server.OauthResponse
{
    public enum ErrorTypeEnum : byte
    {
        [Description("invalid_request")]
        InvalidRequest,

        [Description("unauthorized_client")]
        UnAuthoriazedClient,

        [Description("access_denied")]
        AccessDenied,

        [Description("unsupported_response_type")]
        UnSupportedResponseType,

        [Description("invalid_scope")]
        InValidScope,

        [Description("server_error")]
        ServerError,

        [Description("temporarily_unavailable")]
        TemporarilyUnAvailable,

        [Description("invalid_grant")]
        InvalidGrant,

        [Description("invalid_client")]
        InvalidClient
    }
}


Enter fullscreen mode Exit fullscreen mode

Consult the OAuth2.0 specification for more details about the error types
Back to Authorize endpoint (Action Method) in the HomeController class, in this endpoint we have to verify the Client request data that coming inside request with the one that we stored it inside the ClientStore.cs class

By the way you can store the client information at any back store like SQL Server Database, for simplicity I store it inside C# class.

The Client is expected from me a response result if the Authorize* endpoint verifies the Client successfully, for that we need a class take care of the property of that response, so inside OauthResponse folder create a new class named AuthorizeResponse.cs as follow:



using System.Collections.Generic;

namespace OAuth20.Server.OauthResponse
{
    public class AuthorizeResponse
    {
        /// <summary>
        /// code or implicit grant or client creditional 
        /// </summary>
        public string ResponseType { get; set; } 
        public string Code { get; set; }
        /// <summary>
        /// required if it was present in the client authorization request
        /// </summary>
        public string State { get; set; }

        public string RedirectUri { get; set; }
        public IList<string> RequestedScopes { get; set; }
        public string GrantType { get; set; }
        public string Nonce { get; set; }
        public string Error { get; set; } = string.Empty;
        public string ErrorUri { get; set; }
        public string ErrorDescription { get; set; }
        public bool HasError => !string.IsNullOrEmpty(Error);
    }
}


Enter fullscreen mode Exit fullscreen mode

From OpenId Connect specification the authentication response should look similar like so:

HTTP/1.1 302 Found
Location: https://client.example.org/cb?
code=SplxlOBeZQQYbYS6WxSbIA
&state=af0ifjsldkj

The state is coming from the Client request, but the code is s secret key that I have to generate and store it by myself (Authorization Server) in a safe place. But wait, I need also to store all the AuthorizationRequest data after verifying the Client successfully, because I need this data when our Authorization Server issuing the Identity Token and Access Token to the client. So how can I store all these information.

Inside Model folder create a new class named AuthorizationCode.cs as follow:



using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Security.Principal;

namespace OAuth20.Server.Models
{
    public class AuthorizationCode
    {
        public string ClientId { get; set; }
        public string ClientSecret { get; set; }
        public string RedirectUri { get; set; }

        public DateTime CreationTime { get; set; } = DateTime.UtcNow;
        public bool IsOpenId { get; set; }
        public IList<string> RequestedScopes { get; set; }

        public ClaimsPrincipal Subject { get; set; }
        public string Nonce { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

The name of this class is coming from the generation code step, and I will store this information inside C# Concurrent Dictionary this Dictionary expect from me a key that should be unique for any data that I would like to store inside this Dictionary.

Inside Services folder create a new folder named CodeService and inside the CodeService folder create a new class named CodeStoreService.cs the content of this class as follow:
This service is not completed yet we will come back to it again



using Microsoft.AspNetCore.Authentication.Cookies;
using OAuth20.Server.Models;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;

namespace OAuth20.Server.Services.CodeServce
{
    public class CodeStoreService : ICodeStoreService
    {
        private readonly ConcurrentDictionary<string, AuthorizationCode> _codeIssued = new ConcurrentDictionary<string, AuthorizationCode>();
        private readonly ClientStore _clientStore = new ClientStore();

        // Here I genrate the code for authorization, and I will store it 
        // in the Concurrent Dictionary

        public string GenerateAuthorizationCode(string clientId, IList<string> requestedScope)
        {
            var client = _clientStore.Clients.Where(x => x.ClientId == clientId).FirstOrDefault();

            if(client != null)
            {
                var code = Guid.NewGuid().ToString();

                var authoCode = new AuthorizationCode
                {
                    ClientId = clientId,
                    RedirectUri = client.RedirectUri,
                    RequestedScopes = requestedScope,
                };

                // then store the code is the Concurrent Dictionary
                _codeIssued[code] = authoCode;

                return code;
            }
            return null;

        }

        public AuthorizationCode GetClientDataByCode(string key)
        {
            AuthorizationCode authorizationCode;
            if (_codeIssued.TryGetValue(key, out authorizationCode))
            {
                return authorizationCode;
            }
            return null;
        }

        public AuthorizationCode RemoveClientDataByCode(string key)
        {
            AuthorizationCode authorizationCode;
            _codeIssued.TryRemove(key, out authorizationCode);
            return null;
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

This class has three methods one to store the client data, one to get the client data, and the last one to remove the client data from the Concurrent Dictionary

You can use Dictionary instead of Concurrent Dictionary but the Concurrent Dictionary is thread safe, if you want to use Dictionary be sure to lock the method by using C# lock keywork, to save yourself from the Concurrency Issue

Create an interface inside CodeService and name it ICodeStoreService.cs



using OAuth20.Server.Models;
using System.Collections.Generic;

namespace OAuth20.Server.Services.CodeServce
{
    public interface ICodeStoreService
    {
        string GenerateAuthorizationCode(string clientId, IList<string> requestedScope);
        AuthorizationCode GetClientDataByCode(string key);
        AuthorizationCode RemoveClientDataByCode(string key);
    }
}


Enter fullscreen mode Exit fullscreen mode

Update the CodeStoreService.cs class and let it to inheritance from the that interface you created recently



public class CodeStoreService : ICodeStoreService


Enter fullscreen mode Exit fullscreen mode

we need to register this interface inside the program.cs class



builder.Services.AddSingleton<ICodeStoreService, CodeStoreService>();


Enter fullscreen mode Exit fullscreen mode

Inside Model folder create new class named CheckClientResult.cs as follow:



namespace OAuth20.Server.Models
{
    public class CheckClientResult
    {
        public Client Client { get; set; }

        /// <summary>
        /// The clinet is found in my Clients Store
        /// </summary>
        public bool IsSuccess { get; set; }
        public string Error { get; set; }

        public string ErrorDescription { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

Coming bake to the Authorize endpoint we need a new service to take care of verifying the client and issuing a token and store all the information that we need inside the Concurrent Dictionary that created recently. Let's dive in
Inside Services folder create a new class named AuthorizeResultService.cs

I will go very slowly from here to the end, be patient.and I'm going to put all the methods I need to authenticate the user.
This service is not completed yet we will come back to it again



using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Tokens;
using NuGet.Common;
using OAuth20.Server.Common;
using OAuth20.Server.Models;
using OAuth20.Server.OauthRequest;
using OAuth20.Server.OauthResponse;
using OAuth20.Server.Services.CodeServce;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;

namespace OAuth20.Server.Services
{
    public class AuthorizeResultService : IAuthorizeResultService
    {

        private static string keyAlg = "66007d41-6924-49f2-ac0c-e63c4b1a1730";
        private readonly ClientStore _clientStore = new ClientStore();
        private readonly ICodeStoreService _codeStoreService;
        public AuthorizeResultService(ICodeStoreService codeStoreService)
        {
            _codeStoreService = codeStoreService;
        }
        public AuthorizeResponse AuthorizeRequest(IHttpContextAccessor httpContextAccessor, AuthorizationRequest authorizationRequest)
        {
            AuthorizeResponse response = new AuthorizeResponse();

            if (httpContextAccessor == null)
            {
                response.Error = ErrorTypeEnum.ServerError.GetEnumDescription();
                return response;
            }

            var client = VerifyClientById(authorizationRequest.client_id);
            if (!client.IsSuccess)
            {
                response.Error = client.ErrorDescription;
                return response;
            }

            if (string.IsNullOrEmpty(authorizationRequest.response_type) || authorizationRequest.response_type != "code")
            {
                response.Error = ErrorTypeEnum.InvalidRequest.GetEnumDescription();
                response.ErrorDescription = "response_type is required or is not valid";
                return response;
            }

            if (!authorizationRequest.redirect_uri.IsRedirectUriStartWithHttps() && !httpContextAccessor.HttpContext.Request.IsHttps)
            {
                response.Error = ErrorTypeEnum.InvalidRequest.GetEnumDescription();
                response.ErrorDescription = "redirect_url is not secure, MUST be TLS";
                return response;
            }


            // check the return url is match the one that in the client store


            // check the scope in the client store with the
            // one that is comming from the request MUST be matched at leaset one

            var scopes = authorizationRequest.scope.Split(' ');

            var clientScopes = from m in client.Client.AllowedScopes
                               where scopes.Contains(m)
                               select m;

            if (!clientScopes.Any())
            {
                response.Error = ErrorTypeEnum.InValidScope.GetEnumDescription();
                response.ErrorDescription = "scopes are invalids";
                return response;
            }

            string nonce = httpContextAccessor.HttpContext.Request.Query["nonce"].ToString();

            // Verify that a scope parameter is present and contains the openid scope value.
            // (If no openid scope value is present,
            // the request may still be a valid OAuth 2.0 request, but is not an OpenID Connect request.)

            string code = _codeStoreService.GenerateAuthorizationCode(authorizationRequest.client_id, clientScopes.ToList());
            if (code == null)
            {
                response.Error = ErrorTypeEnum.TemporarilyUnAvailable.GetEnumDescription();
                return response;
            }

            response.RedirectUri = client.Client.RedirectUri + "?response_type=code" + "&state=" + authorizationRequest.state;
            response.Code = code;
            response.State = authorizationRequest.state;
            response.RequestedScopes = clientScopes.ToList();
            response.Nonce = nonce;

            return response;

        }




        private CheckClientResult VerifyClientById(string clientId, bool checkWithSecret = false, string clientSecret = null)
        {
            CheckClientResult result = new CheckClientResult() { IsSuccess = false };

            if (!string.IsNullOrWhiteSpace(clientId))
            {
                var client = _clientStore.Clients.Where(x => x.ClientId.Equals(clientId, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();

                if (client != null)
                {
                    if (checkWithSecret && !string.IsNullOrEmpty(clientSecret))
                    {
                        bool hasSamesecretId = client.ClientSecret.Equals(clientSecret, StringComparison.InvariantCulture);
                        if (!hasSamesecretId)
                        {
                            result.Error = ErrorTypeEnum.InvalidClient.GetEnumDescription();
                            return result;
                        }
                    }


                    // check if client is enabled or not

                    if (client.IsActive)
                    {
                        result.IsSuccess = true;
                        result.Client = client;

                        return result;
                    }
                    else
                    {
                        result.ErrorDescription = ErrorTypeEnum.UnAuthoriazedClient.GetEnumDescription();
                        return result;
                    }
                }
            }

            result.ErrorDescription = ErrorTypeEnum.AccessDenied.GetEnumDescription();
            return result;
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Inside Service folder create a new interface name IAuthorizeResultService.cs and the content of this interface as follow:



using Microsoft.AspNetCore.Http;
using OAuth20.Server.OauthRequest;
using OAuth20.Server.OauthResponse;

namespace OAuth20.Server.Services
{
    public interface IAuthorizeResultService
    {
        AuthorizeResponse AuthorizeRequest(IHttpContextAccessor httpContextAccessor, AuthorizationRequest authorizationRequest);
    }
}


Enter fullscreen mode Exit fullscreen mode

we need to register this interface inside the program.cs class



builder.Services.AddScoped<IAuthorizeResultService, AuthorizeResultService>();


Enter fullscreen mode Exit fullscreen mode

After the Client Authenticated successfully, we need away to let the Resource Owner to insert his username and password, so we need a new class named OpenIdConnectLoginRequest.cs created inside OauthRequest folder:



using System.Collections.Generic;

namespace OAuth20.Server.OauthRequest
{
    public class OpenIdConnectLoginRequest
    {
        public string UserName { get; set; }
        public string Password { get; set; }
        public string RedirectUri { get; set; }
        public string Code { get; set; }
        public string Nonce { get; set; }
        public IList<string> RequestedScopes { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

Now update your Authorize endpoint in HomeController.cs class to be like so:



       private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IAuthorizeResultService _authorizeResultService;
        private readonly ICodeStoreService _codeStoreService;
        public HomeController(IHttpContextAccessor httpContextAccessor, IAuthorizeResultService authorizeResultService,
            ICodeStoreService codeStoreService)
        {
            _httpContextAccessor = httpContextAccessor;
            _authorizeResultService = authorizeResultService;
            _codeStoreService = codeStoreService;
        }

     public IActionResult Authorize(AuthorizationRequest authorizationRequest)
        {
            var result = _authorizeResultService.AuthorizeRequest(_httpContextAccessor, authorizationRequest);

            if (result.HasError)
                return RedirectToAction("Error", new { error = result.Error });

            var loginModel = new OpenIdConnectLoginRequest
            {
                RedirectUri = result.RedirectUri,
                Code = result.Code,
                RequestedScopes = result.RequestedScopes,
                Nonce = result.Nonce
            };


            return View("Login", loginModel);
        }


Enter fullscreen mode Exit fullscreen mode

When the AuthorizeRequest method returns a successful response, we returned the user to the Login view

Create a new Action Method inside HomeController.cs named Login



    [HttpGet]
        public IActionResult Login()
        {
            return View();
        }


Enter fullscreen mode Exit fullscreen mode

Inside Views/Home folder create a new Login.cshtml file



@model OAuth20.Server.OauthRequest.OpenIdConnectLoginRequest
@{
    ViewData["Title"] = "Login";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Login - Page</h1>

<div class="row">
    @foreach (var i in Model.RequestedScopes)
    {
        <p>@i</p>
    }


    <div class="col-12">
        <form asp-action="Login" asp-controller="Home" method="post">
            <input type="hidden" asp-for="RedirectUri" />
            <input type="hidden" asp-for="Code" />
            <input type="hidden" asp-for="Nonce" />

            @for (int i = 0; i < Model.RequestedScopes.Count; i++)
            {
                <input type="hidden" asp-for="RequestedScopes[i]" />
            }


            <div class="col-md-6">
                <label>User Name</label>
                <input type="text" asp-for="UserName" class="form-control" />
            </div>

            <div class="col-md-6">
                <label>Password</label>
                <input type="text" asp-for="Password" class="form-control" />
            </div>


            <div class="col-md-6">
                <input type="submit" value="Login" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>


Enter fullscreen mode Exit fullscreen mode

Here the user will insert his username and password, but for that we have to create a POST version of the Login Action Method, so let us create it inside HomController.cs class

but before that we need to update the CodeStoreService.cs service by adding a new method to it, named UpdatedClientDataByCode so open the CodeStoreService.cs and add the follow mwthod, don't forget to update the ICodeStoreService interface as well by adding the name of the method



// TODO
        // Before updated the Concurrent Dictionary I have to Process User Sign In,
        // and check the user credienail first
        // But here I merge this process here inside update Concurrent Dictionary method
        public AuthorizationCode UpdatedClientDataByCode(string key, IList<string> requestdScopes,
            string userName, string password = null, string nonce = null)
        {
            var oldValue = GetClientDataByCode(key);

            if (oldValue != null)
            {
                // check the requested scopes with the one that are stored in the Client Store 
                var client = _clientStore.Clients.Where(x => x.ClientId == oldValue.ClientId).FirstOrDefault();

                if (client != null)
                {
                    var clientScope = (from m in client.AllowedScopes
                                       where requestdScopes.Contains(m)
                                       select m).ToList();

                    if (!clientScope.Any())
                        return null;

                    AuthorizationCode newValue = new AuthorizationCode
                    {
                        ClientId = oldValue.ClientId,
                        CreationTime = oldValue.CreationTime,
                        IsOpenId = requestdScopes.Contains("openId") || requestdScopes.Contains("profile"),
                        RedirectUri = oldValue.RedirectUri,
                        RequestedScopes = requestdScopes,
                        Nonce = nonce
                    };


                    // ------------------ I suppose the user name and password is correct  -----------------
                    var claims = new List<Claim>();



                    if (newValue.IsOpenId)
                    {
                        // TODO
                        // Add more claims to the claims

                    }

                    var claimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
                    newValue.Subject = new ClaimsPrincipal(claimIdentity);
                    // ------------------ -----------------------------------------------  -----------------

                    var result = _codeIssued.TryUpdate(key, newValue, oldValue);

                    if (result)
                        return newValue;
                    return null;
                }
            }
            return null;
        }


Enter fullscreen mode Exit fullscreen mode

The purpose of this method is to update the HttpContext's Request information for the client in Concurrent Dictionary.

Now create the POST Action Method for the Login inside HomeController.cs



        [HttpPost]
        public async Task<IActionResult> Login(OpenIdConnectLoginRequest loginRequest)
        {
            // here I have to check if the username and passowrd is correct
            // and I will show you how to integrate the ASP.NET Core Identity
            // With our framework

            var result = _codeStoreService.UpdatedClientDataByCode(loginRequest.Code, loginRequest.RequestedScopes,
                loginRequest.UserName, nonce: loginRequest.Nonce);
            if (result != null)
            {

                loginRequest.RedirectUri = loginRequest.RedirectUri + "&code=" + loginRequest.Code;
                return Redirect(loginRequest.RedirectUri);
            }
            return RedirectToAction("Error", new { error = "invalid_request" });
        }


Enter fullscreen mode Exit fullscreen mode

Inside Views/Home create a new Error.cshtml file and paste the follow code inside it



@model string
@{
    ViewData["Title"] = "Error";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Error</h1>


Enter fullscreen mode Exit fullscreen mode

After user login successfully we retune him/her to the return URI that we verify it by the one in the ClientStore.cs after this point the AddOpenIdConnect (From Client Application) extension make a soft HTTP call to token endpoint, and you remember where we defend the value of this endpoint, if you check the DiscoveryEndpointController.cs file you will find the value of this endpoint is assigned to the token_endpoint property as follow:



token_endpoint = "https://localhost:7275/Home/Token",


Enter fullscreen mode Exit fullscreen mode

Ok, at this point we complete the process of the Authorization the next step is the TOKEN

Issuing Identity Token and AccessToken

We need new classes to make our journey very easy when we are working with these tokens (id_token & access_token)

Inside OauthRequest folder create a new class named TokenRequest.cs as follow:



namespace OAuth20.Server.OauthRequest
{
    public class TokenRequest
    {
        public string ClientId { get; set; }
        public string ClientSecret { get; set; }
        public string Code { get; set; }
        public string GrantType { get; set; }
        public string RedirectUri { get; set; }
        public string CodeVerifier { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

And then, inside Models folder create a new enum named TokenTypeEnum.cs



using System.ComponentModel;

namespace OAuth20.Server.Models
{
    public enum TokenTypeEnum : byte
    {
        [Description("Bearer")]
        Bearer
    }
}


Enter fullscreen mode Exit fullscreen mode

And last but not least inside OauthResponse folder create a new class named TokenResponse.cs as follow:



using OAuth20.Server.Common;
using OAuth20.Server.Models;

namespace OAuth20.Server.OauthResponse
{
    public class TokenResponse
    {
        /// <summary>
        /// Oauth 2
        /// </summary>
        public string access_token { get; set; }

        /// <summary>
        /// OpenId Connect
        /// </summary>
        public string id_token { get; set; }

        /// <summary>
        /// By default is Bearer
        /// </summary>

        public string token_type { get; set; } = TokenTypeEnum.Bearer.GetEnumDescription();

        /// <summary>
        /// Authorization Code. This is always returned when using the Hybrid Flow.
        /// </summary>
        public string code { get; set; }



        // For Error Details if any

        public string Error { get; set; } = string.Empty;
        public string ErrorUri { get; set; }
        public string ErrorDescription { get; set; }
        public bool HasError => !string.IsNullOrEmpty(Error);
    }
}


Enter fullscreen mode Exit fullscreen mode

Updating the AuthorizeResultService.cs class by adding the following new method:



public TokenResponse GenerateToken(IHttpContextAccessor httpContextAccessor)
        {
            TokenRequest request = new TokenRequest();

            request.CodeVerifier = httpContextAccessor.HttpContext.Request.Form["code_verifier"];
            request.ClientId = httpContextAccessor.HttpContext.Request.Form["client_id"];
            request.ClientSecret = httpContextAccessor.HttpContext.Request.Form["client_secret"];
            request.Code = httpContextAccessor.HttpContext.Request.Form["code"];
            request.GrantType = httpContextAccessor.HttpContext.Request.Form["grant_type"];
            request.RedirectUri = httpContextAccessor.HttpContext.Request.Form["redirect_uri"];

            var checkClientResult = this.VerifyClientById(request.ClientId, true, request.ClientSecret);
            if (!checkClientResult.IsSuccess)
            {
                return new TokenResponse { Error = checkClientResult.Error, ErrorDescription = checkClientResult.ErrorDescription };
            }

            // check code from the Concurrent Dictionary
            var clientCodeChecker = _codeStoreService.GetClientDataByCode(request.Code);
            if (clientCodeChecker == null)
                return new TokenResponse { Error = ErrorTypeEnum.InvalidGrant.GetEnumDescription() };


            // check if the current client who is one made this authentication request

            if (request.ClientId != clientCodeChecker.ClientId)
                return new TokenResponse { Error = ErrorTypeEnum.InvalidGrant.GetEnumDescription() };

            // TODO: 
            // also I have to check the rediret uri 


            // Now here I will Issue the Id_token

            JwtSecurityToken id_token = null;
            if (clientCodeChecker.IsOpenId)
            {
                // Generate Identity Token

                int iat = (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;


                string[] amrs = new string[] { "pwd" };

                var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(keyAlg));
                var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

                var claims = new List<Claim>()
                {
                    new Claim("sub", "856933325856"),
                    new Claim("given_name", "Mohammed Ahmed Hussien"),
                    new Claim("iat", iat.ToString(), ClaimValueTypes.Integer), // time stamp
                    new Claim("nonce", clientCodeChecker.Nonce)
                };
                foreach (var amr in amrs)
                    claims.Add(new Claim("amr", amr));// authentication method reference 

                id_token = new JwtSecurityToken("https://localhost:7275", request.ClientId, claims, signingCredentials: credentials,
                    expires: DateTime.UtcNow.AddMinutes(
                       int.Parse("5")));

            }

            // Here I have to generate access token 
            var key_at = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(keyAlg));
            var credentials_at = new SigningCredentials(key_at, SecurityAlgorithms.HmacSha256);

            var claims_at = new List<Claim>();


            var access_token = new JwtSecurityToken("https://localhost:7275", request.ClientId, claims_at, signingCredentials: credentials_at,
                expires: DateTime.UtcNow.AddMinutes(
                   int.Parse("5")));

            // here remoce the code from the Concurrent Dictionary
            _codeStoreService.RemoveClientDataByCode(request.Code);

            return new TokenResponse
            {
                access_token = new JwtSecurityTokenHandler().WriteToken(access_token),
                id_token = id_token != null ? new JwtSecurityTokenHandler().WriteToken(id_token) : null,
                code = request.Code
            };
        }


Enter fullscreen mode Exit fullscreen mode

The method is straightforward, verify the Client and then create a new id_token and then access_token
Be sure to add the signature this method to the IAuthorizeResultService.cs interface

Back to HomeController.cs class here is the signature of the token endoint



       public JsonResult Token()
        {
            var result = _authorizeResultService.GenerateToken(_httpContextAccessor);

            if (result.HasError)
                return Json("0");

            return Json(result);
        }


Enter fullscreen mode Exit fullscreen mode

Back to our Client Application that we create at the beginning of this post. Inside Views/Home create a new Index.cshtml file and the content of this file as follow:



@using Microsoft.AspNetCore.Authentication

@{
    ViewData["Title"] = "Index";
    Layout = "~/Views/_Layout.cshtml";
}


<div class="col-12">
    <div class="card">
        <div class="card-header">
            <h1>Tokens Resut - For PlatformNet6 Client</h1>
        </div>

        <div class="card-body">
            @if (User.Identity.IsAuthenticated)
            {
                <h2>User is Authenticated</h2>
                <p>
                    <ul>
                        @foreach (var claim in User.Claims)
                        {
                            <li><strong> @claim.Type:</strong> @claim.Value</li>
                        }
                        <li><strong>Access Token: </strong>@await Context.GetTokenAsync("access_token")</li>
                        <li><strong>Identity Token: </strong>@await Context.GetTokenAsync("id_token")</li>

                    </ul>
                </p>
            }
            else
            {
                <h2>ohhhh, u being Unauthenticated</h2>
            }
        </div>
    </div>
</div>


Enter fullscreen mode Exit fullscreen mode

Now run your Authorization Server and then you Client Application
And you will see the result like so:

Image description

Ooooooops we do it, now we need to improve our Authorization Server code and application structure. This is the next step. see you in the next post.
Again, you can download the completed source code from my Github repos from here feel free to add pull request.
Enjoy!

. . . . . . . .
Terabox Video Player