DEV Community

Cover image for 🌟Implementing OpenID Connect with OpenIddict
Muhammad Naeem
Muhammad Naeem

Posted on

🌟Implementing OpenID Connect with OpenIddict

Recently, I had the opportunity to work with OpenIddict for implementing OpenID Connect. It can be frustrating when you're trying to set up something new without a complete, step-by-step guide. So, I decided to write this article for other developers who might be struggling with the same challenges.

This article will guide you through setting up an authentication server using OpenIddict in a .NET Core Web API project. We'll cover how to implement the Password, Refresh Token, and Client Credentials flows for secure authentication. By the end, you'll have a solid foundation for integrating robust authentication mechanisms into your web applications.

Contents


🛠️ Setting Up the Auth Server

To begin setting up the authentication server using OpenIddict in a .NET Core Web API project, follow these steps:

  • Create a new Web API Project:

    • Open Visual Studio and create a new project using the "ASP.NET Core Web Application" template.
    • Choose the API template and proceed with the project creation.

Create Web API Project

  • Install Required Packages:

    • Open the Package Manager Console (PMC) from Visual Studio.
    • Install the following packages using PMC:
     Install-Package Microsoft.EntityFrameworkCore -Version 8.0.6
     Install-Package Microsoft.EntityFrameworkCore.Design -Version 8.0.6
     Install-Package Microsoft.EntityFrameworkCore.Tools -Version 8.0.6
     Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 8.0.6
     Install-Package Microsoft.AspNetCore.Identity.EntityFrameworkCore -Version 8.0.6
     Install-Package System.Linq.Async -Version 6.0.1
    
     // OpenID Connect packages
     Install-Package OpenIddict -Version 5.7.0
     Install-Package OpenIddict.Core -Version 5.7.0
     Install-Package OpenIddict.Server.AspNetCore -Version 5.7.0
     Install-Package OpenIddict.Abstractions -Version 5.7.0
     Install-Package OpenIddict.EntityFrameworkCore -Version 5.7.0
    
  • Create ApplicationDbContext:

    • Add a new class named ApplicationDbContext.cs under the Data folder of your project.
    • Implement the ApplicationDbContext class as shown below. Ensure to configure the connection string appropriately.
     public class ApplicationDbContext : IdentityDbContext<IdentityUser>
     {
         public ApplicationDbContext()
         {
         }
    
         public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
             : base(options)
         {
         }
    
         protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
             => optionsBuilder.UseSqlServer("Name=ConnectionStrings:DefaultConnection");
    
         protected override void OnModelCreating(ModelBuilder modelBuilder)
         {
             base.OnModelCreating(modelBuilder);
    
             // Customize your identity models here, if needed.
    
             OnModelCreatingPartial(modelBuilder);
         }
    
         partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
     }
    
  • Configure DbContext in Program.cs:

    • Open Program.cs and configure the DbContext in the CreateHostBuilder method as follows:
     public static IHostBuilder CreateHostBuilder(string[] args) =>
         Host.CreateDefaultBuilder(args)
             .ConfigureWebHostDefaults(webBuilder =>
             {
                 webBuilder.UseStartup<Startup>();
             })
             .ConfigureServices((hostContext, services) =>
             {
                 services.AddDbContext<ApplicationDbContext>(options =>
                 {
                     options.UseSqlServer(hostContext.Configuration.GetConnectionString("DefaultConnection"));
                 });
             });
    
  • Configure Identity:

    • Still in Program.cs, configure ASP.NET Core Identity within the ConfigureServices method:
     public static IHostBuilder CreateHostBuilder(string[] args) =>
         Host.CreateDefaultBuilder(args)
             .ConfigureWebHostDefaults(webBuilder =>
             {
                 webBuilder.UseStartup<Startup>();
             })
             .ConfigureServices((hostContext, services) =>
             {
                 services.AddDbContext<ApplicationDbContext>(options =>
                 {
                     options.UseSqlServer(hostContext.Configuration.GetConnectionString("DefaultConnection"));
                 });
    
                 services.AddIdentity<IdentityUser, IdentityRole>()
                     .AddEntityFrameworkStores<ApplicationDbContext>()
                     .AddDefaultTokenProviders();
             });
    
  • Add Initial Migration:

    • Now, add an initial migration to create the necessary Identity models in your database.
     Add-Migration Initial -Context ApplicationDbContext
    
  • Update Database:

    • Update the database to apply the migration.
     Update-Database
    

This will create the necessary tables in your database, including tables for users, roles, and other Identity-related entities.

  • Confirm Database Tables:

    • Verify that the following tables have been created in your database:

    Database Tables

These tables are essential for managing users and roles within your authentication system.


🔒 Add OpenIddict To Auth Server

Now we are ready to configure OpenIddict in our project.

  • Configure OpenIddict in Program.cs

In your Program.cs, add the following configuration to integrate OpenIddict:

   // Cionfigure OpenIddict
   builder.Services.AddOpenIddict()
                  .AddCore(coreOptions =>
                  {
                     coreOptions.UseEntityFrameworkCore()
                                 .UseDbContext<ApplicationDbContext>();
                  })
                  .AddServer(options =>
                  {
                     options.AllowClientCredentialsFlow().AllowRefreshTokenFlow();
                     options.AllowPasswordFlow().AllowRefreshTokenFlow();

                     // Encryption and signing of tokens
                     options
                           .AddDevelopmentEncryptionCertificate()
                           .AddDevelopmentSigningCertificate()
                           .DisableAccessTokenEncryption();

                     // Register the ASP.NET Core host and configure the ASP.NET Core options.
                     options.UseAspNetCore()
                              .EnableTokenEndpointPassthrough()
                              .EnableAuthorizationEndpointPassthrough()
                              .EnableLogoutEndpointPassthrough()
                              .DisableTransportSecurityRequirement();

                  });
Enter fullscreen mode Exit fullscreen mode

This configuration enables both the Client Credentials flow and Password flow, with Refresh Token flow enabled for both to obtain refresh tokens.

  • Add OpenIddict To DbContext Options

Ensure OpenIddict is added to your DbContext options within Program.cs:

   builder.Services.AddDbContext<ApplicationDbContext>(options =>
   {
      options.UseSqlServer(_config.GetConnectionString("DefaultConnection"));

      // Add Openiddict
      options.UseOpenIddict();
   });
Enter fullscreen mode Exit fullscreen mode
  • Add Migration for OpenIddict Tables To incorporate OpenIddict tables into your database, add a migration:
   Add-Migration openIddict -Context ApplicationDbContext
Enter fullscreen mode Exit fullscreen mode
  • Update Database

Update the database to apply the migration:

   Update-Database
Enter fullscreen mode Exit fullscreen mode

Following tables will be added for openiddict

image

In open iddict AddErver options, set token url

   options.SetTokenEndpointUris("connect/token");
Enter fullscreen mode Exit fullscreen mode

📝 Registering New User

To enable user registration and utilize it for token creation, follow these steps:

  • Add ViewModel for User Registration

Create a RegisterViewModel in the ViewModels directory to handle user registration inputs:

   public class RegisterViewModel
   {
      [Required]
      [EmailAddress]
      [Display(Name = "Email")]
      public string Email { get; set; }

      [Required]
      [Display(Name = "UserName")]
      public string UserName { get; set; }

      [Required]
      [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
      [DataType(DataType.Password)]
      [Display(Name = "Password")]
      public string Password { get; set; }
   }
Enter fullscreen mode Exit fullscreen mode
  • Implement RegistrationController

Create a RegistrationController in your Controllers directory to handle user registration:

   namespace AuthServer.Controllers
   {
      [Route("api/[controller]")]
      [ApiController]
      public class RegisterationController : ControllerBase
      {
         private readonly UserManager<IdentityUser> _userManager;
         private readonly ApplicationDbContext _applicationDbContext;
         private static bool _databaseChecked;

         public RegisterationController(
               UserManager<IdentityUser> userManager,
               ApplicationDbContext applicationDbContext)
         {
               _userManager = userManager;
               _applicationDbContext = applicationDbContext;
         }

         //
         // POST: /Account/Register
         [HttpPost]
         [AllowAnonymous]
         public async Task<IActionResult> Register([FromBody] RegisterViewModel model)
         {
               EnsureDatabaseCreated(_applicationDbContext);
               if (ModelState.IsValid)
               {
                  var user = await _userManager.FindByNameAsync(model.Email);
                  if (user != null)
                  {
                     return StatusCode(StatusCodes.Status409Conflict);
                  }

                  user = new IdentityUser { UserName = model.Email, Email = model.Email };
                  var result = await _userManager.CreateAsync(user, model.Password);
                  if (result.Succeeded)
                  {
                     return Ok();
                  }
                  AddErrors(result);
               }

               // If we got this far, something failed.
               return BadRequest(ModelState);
         }

         #region Helpers

         // The following code creates the database and schema if they don't exist.
         // This is a temporary workaround since deploying database through EF migrations is
         // not yet supported in this release.
         // Please see this http://go.microsoft.com/fwlink/?LinkID=615859 for more information on how to do deploy the database
         // when publishing your application.
         private static void EnsureDatabaseCreated(ApplicationDbContext context)
         {
               if (!_databaseChecked)
               {
                  _databaseChecked = true;
                  context.Database.EnsureCreated();
               }
         }

         private void AddErrors(IdentityResult result)
         {
               foreach (var error in result.Errors)
               {
                  ModelState.AddModelError(string.Empty, error.Description);
               }
         }

         #endregion
      }
   }
Enter fullscreen mode Exit fullscreen mode

🔑 Implementing the Password Flow

Now, let's implement the connect/token endpoint in the AuthorizeController to handle the password flow.

  • Create AuthorizeController
   [ApiController]
   public class AuthorizeController : ControllerBase
   {
      private static ClaimsIdentity Identity = new ClaimsIdentity();
      private readonly IOpenIddictApplicationManager _applicationManager;
      private readonly IOpenIddictAuthorizationManager _authorizationManager;
      private readonly IOpenIddictScopeManager _scopeManager;
      private readonly SignInManager<IdentityUser> _signInManager;
      private readonly UserManager<IdentityUser> _userManager;

      public AuthorizeController(IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, IOpenIddictScopeManager scopeManager, SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
      {
         _applicationManager = applicationManager;
         _authorizationManager = authorizationManager;
         _scopeManager = scopeManager;
         _signInManager = signInManager;
         _userManager = userManager;
      }

      [HttpPost]
      [Route("connect/token")]
      public async Task<IActionResult> ConnectToken()
      {
         try
         {
               var openIdConnectRequest = HttpContext.GetOpenIddictServerRequest() ??
                        throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

               Identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, Claims.Name, Claims.Role);
               IdentityUser? user = null;
               AuthenticationProperties properties = new();

               if (openIdConnectRequest.IsClientCredentialsGrantType())
               {
                  throw new NotImplementedException();
               }
               else if (openIdConnectRequest.IsPasswordGrantType())
               {
                  user = await _userManager.FindByNameAsync(openIdConnectRequest.Username);

                  if (user == null)
                  {
                     return BadRequest(new OpenIddictResponse
                     {
                           Error = Errors.InvalidGrant,
                           ErrorDescription = "User does not exist"
                     });
                  }

                  // Check that the user can sign in and is not locked out.
                  // If two-factor authentication is supported, it would also be appropriate to check that 2FA is enabled for the user
                  if (!await _signInManager.CanSignInAsync(user) || (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user)))
                  {
                     // Return bad request is the user can't sign in
                     return BadRequest(new OpenIddictResponse
                     {
                           Error = OpenIddictConstants.Errors.InvalidGrant,
                           ErrorDescription = "The specified user cannot sign in."
                     });
                  }

                  // Validate the username/password parameters and ensure the account is not locked out.
                  var result = await _signInManager.PasswordSignInAsync(user.UserName, openIdConnectRequest.Password, false, lockoutOnFailure: false);
                  if (!result.Succeeded)
                  {
                     if (result.IsNotAllowed)
                     {
                           return BadRequest(new OpenIddictResponse
                           {
                              Error = Errors.InvalidGrant,
                              ErrorDescription = "User not allowed to login. Please confirm your email"
                           });
                     }

                     if (result.RequiresTwoFactor)
                     {
                           return BadRequest(new OpenIddictResponse
                           {
                              Error = Errors.InvalidGrant,
                              ErrorDescription = "User requires 2F authentication"
                           });
                     }

                     if (result.IsLockedOut)
                     {
                           return BadRequest(new OpenIddictResponse
                           {
                              Error = Errors.InvalidGrant,
                              ErrorDescription = "User is locked out"
                           });
                     }
                     else
                     {
                           return BadRequest(new OpenIddictResponse
                           {
                              Error = Errors.InvalidGrant,
                              ErrorDescription = "Username or password is incorrect"
                           });
                     }
                  }

                  // The user is now validated, so reset lockout counts, if necessary
                  if (_userManager.SupportsUserLockout)
                  {
                     await _userManager.ResetAccessFailedCountAsync(user);
                  }

                  //// Getting scopes from user parameters (TokenViewModel) and adding in Identity 
                  Identity.SetScopes(openIdConnectRequest.GetScopes());

                  // Getting scopes from user parameters (TokenViewModel)
                  // Checking in OpenIddictScopes tables for matching resources
                  // Adding in Identity
                  Identity.SetResources(await _scopeManager.ListResourcesAsync(Identity.GetScopes()).ToListAsync());


                  // Add Custom claims
                  // sub claims is mendatory
                  Identity.AddClaim(new Claim(Claims.Subject, user.Id));
                  Identity.AddClaim(new Claim(Claims.Audience, "Resourse"));

                  // Setting destinations of claims i.e. identity token or access token
                  Identity.SetDestinations(GetDestinations);
               }
               else if (openIdConnectRequest.IsRefreshTokenGrantType())
               {
                  throw new NotImplementedException();
               }
               else
               {
                  return BadRequest(new
                  {
                     error = Errors.UnsupportedGrantType,
                     error_description = "The specified grant type is not supported."
                  });
               }

               // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
               var signInResult = SignIn(new ClaimsPrincipal(Identity), properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
               return signInResult;
         }
         catch (Exception ex)
         {
               return BadRequest(new OpenIddictResponse()
               {
                  Error = Errors.ServerError,
                  ErrorDescription = "Invalid login attempt"
               });
         }
      }

      #region Private Methods

      private static IEnumerable<string> GetDestinations(Claim claim)
      {
         // Note: by default, claims are NOT automatically included in the access and identity tokens.
         // To allow OpenIddict to serialize them, you must attach them a destination, that specifies
         // whether they should be included in access tokens, in identity tokens or in both.

         return claim.Type switch
         {
               Claims.Name or
               Claims.Subject
                  => new[] { Destinations.AccessToken, Destinations.IdentityToken },

               _ => new[] { Destinations.AccessToken },
         };
      }

      #endregion

   }
Enter fullscreen mode Exit fullscreen mode

Remember to add subject and audience claim in token. Those are required to cmmunicate to webapi.

  • Generate Token

Start the project and register a test user using following curl:

   curl -X 'POST' \
   'https://localhost:7249/api/Registeration' \
   -H 'accept: */*' \
   -H 'Content-Type: application/json' \
   -d '{
   "email": "user@example.com",
   "userName": "test",
   "password": "@Test.123"
   }'
Enter fullscreen mode Exit fullscreen mode

Now to get token , hit this curl

   curl --location 'https://localhost:7249/connect/token' \
   --header 'Content-Type: application/x-www-form-urlencoded' \
   --data-urlencode 'grant_type=password' \
   --data-urlencode 'username=test' \
   --data-urlencode 'password=@Test.123'
Enter fullscreen mode Exit fullscreen mode

Response:

   {
      "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkMwMzE4N0NGOERDOTMxQzQ5QjQ5RTg3MzlFODQ4RDU2MzlEM0Y1NTYiLCJ4NXQiOiJ3REdIejQzSk1jU2JTZWh6bm9TTlZqblQ5VlkiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MjQ5LyIsImV4cCI6MTcxOTkwNTQxNywiaWF0IjoxNzE5OTAxODE3LCJqdGkiOiI5ZWIzNmZjMS02ZDY1LTQ3OWQtOWJmZC1hZGZjNWVhNzc3Y2EiLCJzdWIiOiI2YmE0ODk1OC0yZjBhLTQ5ZTktYTg2OC1iZTMzNGQ4N2UxYjgiLCJhdWQiOiJSZXNvdXJzZSIsIm9pX3Rrbl9pZCI6ImJmNzA1M2YyLWZjZTgtNDg5YS1hYTgyLTliZmFmOGNlNGUxOSJ9.SYbM8ltyiftgqj6AFABJA_zbiXzlQtLJR2E4xt4W2y85AIlACSBzC3i4Ppg5nsMzgscFGbcO8MOYM3gB6EvViKugdV4BOL26x9UVWnEzOV_BC9p-TYx58EA0Ewx2m3KEqXlJfeLNaAg8H9MyXwIQkc9mbE89MDKhZ3udlI2qElWH2-JbF39mXjgpPHjiMP1UV2Dvp0slewNYeTlj04YY7iSnuEkawDrPAqfWVQPePEuefXVuS139eGLeNdnDxSa16l1tv6V08JcqrSRrRJpDo0yldt07WKCPz8e9lyCts6oiUvNSPSKnf5RE3RSl8jKoa2JnaxfAVyG106JyXacm2g",
      "token_type": "Bearer",
      "expires_in": 3599
   }
Enter fullscreen mode Exit fullscreen mode
  • Debugging Token

To debug the generated token, visit https://token.dev/ and paste your token. The decoded token will appear as follows:

   {
   "iss": "https://localhost:7249/",
   "exp": 1719905417,
   "iat": 1719901817,
   "jti": "9eb36fc1-6d65-479d-9bfd-adfc5ea777ca",
   "sub": "6ba48958-2f0a-49e9-a868-be334d87e1b8",
   "aud": "Resourse",
   "oi_tkn_id": "bf7053f2-fce8-489a-aa82-9bfaf8ce4e19"
   }
Enter fullscreen mode Exit fullscreen mode

All good till now, we are able to get an access token to use over our resourse controller.
To separate the resource server from the authentication server, create a separate API project to hold your resources. Use the access token generated by your authentication server to authenticate requests to this resource server. This ensures secure communication between the two servers.


💼 Creating a Web API Project as a Resource Server

To set up a resource server using JWT authentication in Visual Studio, follow these steps:

  • Step 1: Create Web API Project
  1. Open Visual Studio and navigate to your existing solution.
  2. Add a new project to the solution:
    • Select File -> New -> Project.
    • Choose ASP.NET Core Web API as the project template.
  • Step 2: Install JWT Authentication Package

Install the Microsoft.AspNetCore.Authentication.JwtBearer package using the NuGet Package Manager Console:

   NuGet\Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 8.0.6
Enter fullscreen mode Exit fullscreen mode
  • Step 3: Configure Authentication in Program.cs

In the Program.cs file of your Web API project, configure authentication settings:

   using Microsoft.AspNetCore.Authentication.JwtBearer;

   var builder = WebApplication.CreateBuilder(args);

   // Add services to the container.

   builder.Services.AddControllers();
   // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
   builder.Services.AddEndpointsApiExplorer();
   builder.Services.AddSwaggerGen();

   builder.Services.AddAuthentication(options =>
   {
      options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
      options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
   })
   .AddJwtBearer(options =>
   {
      // base-address of Auth Server
      options.Authority = "https://localhost:7249/";

      // name of the API resource
      options.Audience = "Resourse";

      options.RequireHttpsMetadata = false;

      // Check preferred_username claim exists in the token. If it exists, .NET Core framework sets it to currently logged-in user name i-e User.Identity.Name
      options.TokenValidationParameters.NameClaimType = "preferred_username";
      options.TokenValidationParameters.RoleClaimType = System.Security.Claims.ClaimTypes.Role;// "role";
   })
   ;

   var app = builder.Build();

   // Configure the HTTP request pipeline.
   if (app.Environment.IsDevelopment())
   {
      app.UseSwagger();
      app.UseSwaggerUI();
   }

   app.UseHttpsRedirection();

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

   app.MapControllers();

   app.Run();

Enter fullscreen mode Exit fullscreen mode
  • Step 4: Secure Weather Controller with Authorization

Ensure that access to the WeatherForecast endpoint requires authentication by adding [Authorize] attribute:

   [Authorize]
   [HttpGet(Name = "GetWeatherForecast")]
   public IEnumerable<WeatherForecast> Get()
   {
      return Enumerable.Range(1, 5).Select(index => new WeatherForecast
      {
         Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
         TemperatureC = Random.Shared.Next(-20, 55),
         Summary = Summaries[Random.Shared.Next(Summaries.Length)]
      })
      .ToArray();
   }
Enter fullscreen mode Exit fullscreen mode

🧪 Testing Out Generated Tokens

To access the secured endpoint in Postman:

  • Step 1: Import the following curl command into Postman:

      curl --location 'https://localhost:7023/WeatherForecast' \
      --header 'accept: text/plain' \
      --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkMwMzE4N0NGOERDOTMxQzQ5QjQ5RTg3MzlFODQ4RDU2MzlEM0Y1NTYiLCJ4NXQiOiJ3REdIejQzSk1jU2JTZWh6bm9TTlZqblQ5VlkiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MjQ5LyIsImV4cCI6MTcxOTkwNTQxNywiaWF0IjoxNzE5OTAxODE3LCJqdGkiOiI5ZWIzNmZjMS02ZDY1LTQ3OWQtOWJmZC1hZGZjNWVhNzc3Y2EiLCJzdWIiOiI2YmE0ODk1OC0yZjBhLTQ5ZTktYTg2OC1iZTMzNGQ4N2UxYjgiLCJhdWQiOiJSZXNvdXJzZSIsIm9pX3Rrbl9pZCI6ImJmNzA1M2YyLWZjZTgtNDg5YS1hYTgyLTliZmFmOGNlNGUxOSJ9.SYbM8ltyiftgqj6AFABJA_zbiXzlQtLJR2E4xt4W2y85AIlACSBzC3i4Ppg5nsMzgscFGbcO8MOYM3gB6EvViKugdV4BOL26x9UVWnEzOV_BC9p-TYx58EA0Ewx2m3KEqXlJfeLNaAg8H9MyXwIQkc9mbE89MDKhZ3udlI2qElWH2-JbF39mXjgpPHjiMP1UV2Dvp0slewNYeTlj04YY7iSnuEkawDrPAqfWVQPePEuefXVuS139eGLeNdnDxSa16l1tv6V08JcqrSRrRJpDo0yldt07WKCPz8e9lyCts6oiUvNSPSKnf5RE3RSl8jKoa2JnaxfAVyG106JyXacm2g' \
      --header 'Cookie: .AspNetCore.Identity.Application=CfDJ8JZb4jmttB5Fm2_VcJT682HD7u6NCaZ1Bsx8zeyVvdGELKkP4V0bm7gmUbj8vZje-KQsz7deOYJV0B3_HOZyxEPWVIxm-U8TBoGzJ7tr8p94NZd7Xm8Wrw1wwNd2RO8NiyiAie6ChKACzBeBzfHV8VinyMZfi4-b84Y0gg0hAqHjsJr-AB4dcRz2JUxzElPasqYjGELZHzlm--l7NSUBVh6BR_E5iKc3VCM94s6Xpzz-k05NwRrcjR-s6gARucAFWWsCgrI0aynw9LMzuvXxY0_6K7sgl0WezreSwlfvYdcqb8QivcDqrdKvQODdKIBEymlSmZfa2w0xz5ej9kS33rXFMRbRx4Zh5Ear1VrZARINVHspXsq7q7N65IORq3BzsNxxVkc76y6G22ug4oUZCW-KRBxU1SiffNP2XRPBmpnoQNk1lQ51iCnMDmjcmMTzQ3xbScyAh8WZfPBZoNSdmp8LORSprI2x6RpKDah4_y8_YE57TEPeSPZoFRbHxb0_tHBTVOcJJQ-VXXGj4JGpB4tilWeqtuuSTQuqIEWdpCB01QExwEsSM3iIkV8Sy4Yb8e-0sQl5RYp6PaAx-aPkNOxFf8aIj0SlzQODWlbJzZDhfSRaQFifJEGwZdicNpaS2lpI27rycBSoqkvbAmxCjp63ewlCLCVxg1lGrt8Ze-rvJ5Td0Yh1Ozb_1KlSCyfYid2tCiEI7V3HEB5vYMafMH4'
    
  • Step-2: In the Authorization tab of Postman, select Bearer Token and paste your token into the token field.

    image

  • Step-3: Hit the request to access the secured WeatherForecast endpoint.

    This setup allows you to authenticate requests to the resource server using a bearer token issued by the authentication server. Adjust the endpoint URL (https://localhost:7023/WeatherForecast) and the token as per your specific configuration.

Till now , we are able to generate our token via password flow without client from our auth server and use this token to access the resource of webapi which is our resourse server.
Now its a basic flow for password. The token generated will be valid for one hour as mentioned in response like 3600 sec. You can get new token again or you can add refresh token flow to generate the refresh token and then use this token to get token again.


🔄 Implementing the Refresh Token Flow

To implement the refresh token flow, follow these steps:

  • Step 1: Handle the Refresh Token Grant Type:

Update the code to handle the refresh token grant type within your authentication logic.

   else if (openIdConnectRequest.IsRefreshTokenGrantType())
   {
      // Retrieve the claims principal stored in the authorization code/refresh token.
      var authenticateResult = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

      if (authenticateResult.Succeeded && authenticateResult.Principal != null)
      {
         // Retrieve the user profile corresponding to the authorization code/refresh token.
         user = await _userManager.FindByIdAsync(authenticateResult.Principal.GetClaim(Claims.Subject));
         if (user is null)
         {
               return BadRequest(new OpenIddictResponse
               {
                  Error = Errors.InvalidGrant,
                  ErrorDescription = "The token is no longer valid."
               });
         }

         // You have to grant the 'offline_access' scope to allow
         // OpenIddict to return a refresh token to the caller.
         Identity.SetScopes(OpenIddictConstants.Scopes.OfflineAccess);

         Identity.AddClaim(new Claim(Claims.Subject, user.Id));
         Identity.AddClaim(new Claim(Claims.Audience, "Resourse"));

         // Getting scopes from user parameters (TokenViewModel)
         // Checking in OpenIddictScopes tables for matching resources
         // Adding in Identity
         Identity.SetResources(await _scopeManager.ListResourcesAsync(Identity.GetScopes()).ToListAsync());

         // Setting destinations of claims i.e. identity token or access token
         Identity.SetDestinations(GetDestinations);
      }
      else if (authenticateResult.Failure is not null)
      {
         var failureMessage = authenticateResult.Failure.Message;
         var failureException = authenticateResult.Failure.InnerException;
         return BadRequest(new OpenIddictResponse
         {
               Error = Errors.InvalidRequest,
               ErrorDescription = failureMessage + failureException
         });
      }
   }
Enter fullscreen mode Exit fullscreen mode
  • Step 2: Grant Offline Access Scope in Password Flow:

Ensure the 'offline_access' scope is granted when generating tokens using the password flow.

   //// Getting scopes from user parameters (TokenViewModel) and adding in Identity 
   Identity.SetScopes(openIdConnectRequest.GetScopes());

   //// You have to grant the 'offline_access' scope to allow
   //// OpenIddict to return a refresh token to the caller.
   if (!openIdConnectRequest.Scope.IsNullOrEmpty() && openIdConnectRequest.Scope.Split(' ').Contains(OpenIddictConstants.Scopes.OfflineAccess))
      Identity.SetScopes(OpenIddictConstants.Scopes.OfflineAccess);
Enter fullscreen mode Exit fullscreen mode
  • Step 3: Request a Token with Offline Access:

When requesting a token, include the 'offline_access' scope to receive a refresh token.

   curl --location 'https://localhost:7249/connect/token' \
   --header 'Content-Type: application/x-www-form-urlencoded' \
   --data-urlencode 'grant_type=password' \
   --data-urlencode 'username=test' \
   --data-urlencode 'password=@Test.123' \
   --data-urlencode 'scope=offline_access'
Enter fullscreen mode Exit fullscreen mode

The response will contain both the access token and the refresh token.

   {
      "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkMwMzE4N0NGOERDOTMxQzQ5QjQ5RTg3MzlFODQ4RDU2MzlEM0Y1NTYiLCJ4NXQiOiJ3REdIejQzSk1jU2JTZWh6bm9TTlZqblQ5VlkiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MjQ5LyIsImV4cCI6MTcxOTkwNjQxMCwiaWF0IjoxNzE5OTAyODEwLCJzY29wZSI6Im9mZmxpbmVfYWNjZXNzIiwianRpIjoiMWIwMTBlNzktNGIwOC00MTc2LTg2NjktZDExNjZkOGNjZTA2Iiwic3ViIjoiNmJhNDg5NTgtMmYwYS00OWU5LWE4NjgtYmUzMzRkODdlMWI4IiwiYXVkIjoiUmVzb3Vyc2UiLCJvaV9hdV9pZCI6IjRmYTc5YzdlLWRhZjEtNDMxMC04OWE1LWU0ZDMxMzRjNmI4NyIsIm9pX3Rrbl9pZCI6Ijc2ZjdlZjVjLTJlMjUtNDBkMC1hZDg0LTEyMmJiYTAxOWU0MSJ9.G175Tp4nIO0VqUdXXxB3iP55KarQe9JXGGlCF9us7uW-6337JbTfy6jQqcYZMxWDGFDFJTE5i7jLbchLt465r8NJPK8kHvgm5zA_ayBC12-q-BN9qjdiSovEmTBshnISsGr1-ix-WdcFjwdFxQKl-yNC9BZ1seyIbe7WUMhTAbXDguAH9Y1uhq1HUAF8xLDEe3_0hslZzyTuK4L-FHERPosy0hV5ekk3DGQFUGTnwawAZK9JyiBr0iXdnt4Hyt8dm0KZuacjE1G7ml7ECyVHnQQuESLbLfmGYTQl9qP1mqTxiZ1g_z5T_Mlr6W-PDSALWJsXzQ6XfoTFIgU2fYJUug",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiI1RUI3RUUwMTgzNTU2NENDRjhEQzhCMTQwRkQ0Rjg0QzczQzFERDM5IiwidHlwIjoib2lfcmVmdCtqd3QiLCJjdHkiOiJKV1QifQ.IGpB4Ncj3uBhpb3S8p7xctY8v7G6VOkG29108cYOO-R3BM4mwdO8jNnqgcYdIXoTJLxBEnlK7nGApyNf6oF8I3DVec1XDUncbaKcMKtGmH29xVatuYEGFOeAPmLQF74SLxaQ2lGWxOcsmR71eR3i4PswweQ7vaHwcIGr_URjJ7Yqw__iPmzyqZoxThCCLP9D69H_YiuGHZsrn-wGiBvy1XBGJiscpA331MtPrevxugpvrmfq5W05SNQb5GNQ6VNv4MO9POv4oQp08fMxedg2DWDh8yEVI08yn9TuyAxnUMvWPdnvSgjdEqqKXj-GKp09U8Nfz5663g3M6pbk_k0e2g.-Ytx-sYMH6S95Q5G8cr8TA.qTxPLELewogr3PBnYF8fxGD8QjOmTM-jqgxio2-Ju6_6r_1c71bjylwaCfQYycTNjLMAHLwM6pC6POSr5YX1tz3Rwgp5j22Fk0ybXXHCKnfnKUuMA9_DHPYS5icgvEnGG-l67Orpqcoli5gjmVSpXss3_luvDWj9rsyxENHrgnw47oyQQ_KurFHtBKnPDkWeIe7ZIPjz2SCR0RQ1vH3dzAhs9GnmwGhNDdovQifIfeQ04ShDW9__EKbVxRPSFoamyqTVBppx42IHPM30geFP5XUm6K5K9yKEWQgoJW-aOvxCKO9xgzQaqzyo3NP_djau6P8mp_FpAR9I6B-naeIHQgM9c9TudSoP8a4VLZ3HoUsdmqMv6itZVSA-hwURTNgyjMDyOakJzgm9O-ZNE4qMXHbyxQoH4ObkAAlN_xx1RLW_YRJalRnMZZLiT5bqe-aP0oWpZ17MymaOiA8kWW2429rS586NAX80JkKAlk1qqgc5KS8shOOv6hia8Zlh76tjH_cWZkQeQoB_wmUpHvr2j50D5mgHYNOmK6bAiJHcDrQ5J1mxn3_Mx6B3UKFUXMokETfueHiF36ehB9rgoqEPgB2KUr93v_I5ZPygtpbFnSHUUFEehAPGxZZLcc4lwtkn8i7Rk7JNjGhTVTcnTMHOfiVN5LOMOMETusjV6-ZDtUH1ktRsFweAWOC7MiEECilwMO6posZOpaNggX3YKrMsKJ0Eq83Glpkn3LyRADQG4XumK8XbSiO75Ucy3aZaxeVgovjRjhJqWTjRW5TjNpHv-GfLl4soJEQgwVzEtJKkZaUcMZIp10szRqROrdU9iqo_QAnDqW9cf6QMjxWEKSRKqM1eA6q_c2Bi_vhZa5Vm7gOQQNpK9PhENRpUoJ7kHcpgTY89qwyMQcpKeefHBg62cZKK9vGbuoabjNkYx7BI4UhDczhjWQ0Azhp3i05GNHCB2Ts9RAxwlPDZfVhJNnFfN7djJ4wSzjSxRwQUHylNl1hrtvwKnEx5hYr9D-cYSkLuLcsU6WOeYsFJupRXxKsaACRx_2a7-RFD876H1tDYlv7ZNlhsS8z9VlFTY-29_cl4LNxYNvSdekz3lvd1baKztvJgbXQYRLzszpN2rP7zi0gY3fbg5gTN97ZxCifCN4o09IGSU5u_rDNa5l2dnvk9lgnDC45FOlM3T4gb1o06pcztuJVynBM1ZTK2doZi43a_2kbtVcdxz963QJdbAY__YuilKEg5GwDqBk0YRIrVtMexUI3a6fCzNSKYcW2T2NvXxwKEG2jA5H7qtPFNHYaDTg2LdzH8FBDoa4U16gtfW1k.c4FGZ57S5sVqMupJItF7PmixWyqfwV5vqCNCezuJAb8"
   }
Enter fullscreen mode Exit fullscreen mode
  • Step 4: Refreshing the Token:

When the token expires, you can use the refresh token to obtain a new token by executing the following command:

   curl --location 'https://localhost:7249/connect/token' \
   --header 'Content-Type: application/x-www-form-urlencoded' \
   --data-urlencode 'grant_type=refresh_token' \
   --data-urlencode 'refresh_token=eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiI1RUI3RUUwMTgzNTU2NENDRjhEQzhCMTQwRkQ0Rjg0QzczQzFERDM5IiwidHlwIjoib2lfcmVmdCtqd3QiLCJjdHkiOiJKV1QifQ.IGpB4Ncj3uBhpb3S8p7xctY8v7G6VOkG29108cYOO-R3BM4mwdO8jNnqgcYdIXoTJLxBEnlK7nGApyNf6oF8I3DVec1XDUncbaKcMKtGmH29xVatuYEGFOeAPmLQF74SLxaQ2lGWxOcsmR71eR3i4PswweQ7vaHwcIGr_URjJ7Yqw__iPmzyqZoxThCCLP9D69H_YiuGHZsrn-wGiBvy1XBGJiscpA331MtPrevxugpvrmfq5W05SNQb5GNQ6VNv4MO9POv4oQp08fMxedg2DWDh8yEVI08yn9TuyAxnUMvWPdnvSgjdEqqKXj-GKp09U8Nfz5663g3M6pbk_k0e2g.-Ytx-sYMH6S95Q5G8cr8TA.qTxPLELewogr3PBnYF8fxGD8QjOmTM-jqgxio2-Ju6_6r_1c71bjylwaCfQYycTNjLMAHLwM6pC6POSr5YX1tz3Rwgp5j22Fk0ybXXHCKnfnKUuMA9_DHPYS5icgvEnGG-l67Orpqcoli5gjmVSpXss3_luvDWj9rsyxENHrgnw47oyQQ_KurFHtBKnPDkWeIe7ZIPjz2SCR0RQ1vH3dzAhs9GnmwGhNDdovQifIfeQ04ShDW9__EKbVxRPSFoamyqTVBppx42IHPM30geFP5XUm6K5K9yKEWQgoJW-aOvxCKO9xgzQaqzyo3NP_djau6P8mp_FpAR9I6B-naeIHQgM9c9TudSoP8a4VLZ3HoUsdmqMv6itZVSA-hwURTNgyjMDyOakJzgm9O-ZNE4qMXHbyxQoH4ObkAAlN_xx1RLW_YRJalRnMZZLiT5bqe-aP0oWpZ17MymaOiA8kWW2429rS586NAX80JkKAlk1qqgc5KS8shOOv6hia8Zlh76tjH_cWZkQeQoB_wmUpHvr2j50D5mgHYNOmK6bAiJHcDrQ5J1mxn3_Mx6B3UKFUXMokETfueHiF36ehB9rgoqEPgB2KUr93v_I5ZPygtpbFnSHUUFEehAPGxZZLcc4lwtkn8i7Rk7JNjGhTVTcnTMHOfiVN5LOMOMETusjV6-ZDtUH1ktRsFweAWOC7MiEECilwMO6posZOpaNggX3YKrMsKJ0Eq83Glpkn3LyRADQG4XumK8XbSiO75Ucy3aZaxeVgovjRjhJqWTjRW5TjNpHv-GfLl4soJEQgwVzEtJKkZaUcMZIp10szRqROrdU9iqo_QAnDqW9cf6QMjxWEKSRKqM1eA6q_c2Bi_vhZa5Vm7gOQQNpK9PhENRpUoJ7kHcpgTY89qwyMQcpKeefHBg62cZKK9vGbuoabjNkYx7BI4UhDczhjWQ0Azhp3i05GNHCB2Ts9RAxwlPDZfVhJNnFfN7djJ4wSzjSxRwQUHylNl1hrtvwKnEx5hYr9D-cYSkLuLcsU6WOeYsFJupRXxKsaACRx_2a7-RFD876H1tDYlv7ZNlhsS8z9VlFTY-29_cl4LNxYNvSdekz3lvd1baKztvJgbXQYRLzszpN2rP7zi0gY3fbg5gTN97ZxCifCN4o09IGSU5u_rDNa5l2dnvk9lgnDC45FOlM3T4gb1o06pcztuJVynBM1ZTK2doZi43a_2kbtVcdxz963QJdbAY__YuilKEg5GwDqBk0YRIrVtMexUI3a6fCzNSKYcW2T2NvXxwKEG2jA5H7qtPFNHYaDTg2LdzH8FBDoa4U16gtfW1k.c4FGZ57S5sVqMupJItF7PmixWyqfwV5vqCNCezuJAb8'
Enter fullscreen mode Exit fullscreen mode

Replace the refresh_token value with your actual refresh token. This request will return a new access token and a new refresh token without needing to provide the username and password again.

If you only need to support the password flow, the existing implementation is sufficient. However, if your requirements include setting up the client credentials flow as well, follow the steps below to implement it. Before proceeding, we need to register a new client.


🔐 Implementing the Client Credentials Flow

If you only need to support the password flow, the existing implementation is sufficient. However, if your requirements include setting up the client credentials flow as well, follow the steps below to implement it. Before proceeding, we need to register a new client.

  • Step 1: Modify the DI Tree to Inject OpenIddict Application Manager

In the RegistrationController, modify the dependency injection (DI) tree to inject the OpenIddictApplicationManager:

   private readonly UserManager<IdentityUser> _userManager;
   private readonly IOpenIddictApplicationManager _applicationManager;
   private readonly ApplicationDbContext _applicationDbContext;
   private static bool _databaseChecked;

   public RegisterationController(
      UserManager<IdentityUser> userManager,
      ApplicationDbContext applicationDbContext,
      IOpenIddictApplicationManager applicationManager)
   {
      _userManager = userManager;
      _applicationDbContext = applicationDbContext;
      _applicationManager = applicationManager;
   }
Enter fullscreen mode Exit fullscreen mode
  • Step 2: Add API to Register Client

Add the following API to register a new client:

   [HttpPost]
   [Route("RegisterClient")]
   [AllowAnonymous]
   public async Task<IActionResult> RegisterClient([FromBody] ClientRegistrationModel model)
   {
      try
      {
         EnsureDatabaseCreated(_applicationDbContext);

         await _applicationManager.CreateAsync(new OpenIddictApplicationDescriptor
         {
               ClientId = model.ClientId,
               ClientSecret = model.ClientSecret,
               DisplayName = model.ClientId,
               Permissions =
         {
               Permissions.Endpoints.Token,
               Permissions.Endpoints.Authorization,

               Permissions.GrantTypes.ClientCredentials,
               Permissions.GrantTypes.RefreshToken,

               Permissions.Prefixes.Scope + "Resourse",
               Permissions.Prefixes.Scope + Scopes.OfflineAccess,

         }
         });

         return  Ok(model);
      }
      catch (Exception ex)
      {
         return BadRequest(ex.ToString());
      }
   }

Enter fullscreen mode Exit fullscreen mode
  • Step 3: Register a New Client

Use the following curl command to register a new client:

   curl -X 'POST' \
   'https://localhost:7249/api/Registeration/RegisterClient' \
   -H 'accept: */*' \
   -H 'Content-Type: application/json' \
   -d '{
   "clientId": "webClient",
   "clientSecret": "supersecret"
   }'
Enter fullscreen mode Exit fullscreen mode
  • Step 4: Implement Client Credentials Flow in AuthorizeController

In the AuthorizeController, add the following code to handle the client credentials flow:

   if (openIdConnectRequest.IsClientCredentialsGrantType())
   {
      Identity.SetScopes(openIdConnectRequest.GetScopes());
      Identity.SetResources(await _scopeManager.ListResourcesAsync(Identity.GetScopes()).ToListAsync());

      // Add mandatory Claims
      Identity.AddClaim(new Claim(Claims.Subject, openIdConnectRequest.ClientId));
      Identity.AddClaim(new Claim(Claims.Audience, "Resourse"));

      Identity.SetDestinations(GetDestinations);
   }
Enter fullscreen mode Exit fullscreen mode
  • Step 5: Generate Token via Client Credentials Flow

Use the following curl command to generate a token using the client credentials flow:

   curl --location 'https://localhost:7249/connect/token' \
   --header 'Content-Type: application/x-www-form-urlencoded' \
   --data-urlencode 'grant_type=client_credentials' \
   --data-urlencode 'client_id=webClient' \
   --data-urlencode 'client_secret=supersecret' \
   --data-urlencode 'scope=offline_access'
Enter fullscreen mode Exit fullscreen mode

Awesome, now the Auth server is pretty mature to handle token generation for password flow, refesh token flow nad client credentials flow also.


💡 Conclusion

By following these steps, you have successfully enhanced your authentication server to support various authentication flows, including the password flow, refresh token flow, and client credentials flow. This setup ensures robust security and flexibility, catering to a wide range of authentication requirements.

I will be posting more about OpenIddict to cover other flows, customizations, custom grants, and scopes. Stay tuned for more updates!

Top comments (0)