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
- Add OpenIddict To Auth Server
- Registering New User
- Implementing the Password Flow
- Creating a Web API Project as a Resource Server
- Testing Out Generated Tokens
- Implementing the Refresh Token Flow
- Implementing the Client Credentials Flow
- Conclusion
🛠️ 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.
-
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 theData
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); }
- Add a new class named
-
Configure DbContext in Program.cs:
- Open
Program.cs
and configure the DbContext in theCreateHostBuilder
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")); }); });
- Open
-
Configure Identity:
- Still in
Program.cs
, configure ASP.NET Core Identity within theConfigureServices
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(); });
- Still in
-
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.
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();
});
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();
});
- Add Migration for OpenIddict Tables To incorporate OpenIddict tables into your database, add a migration:
Add-Migration openIddict -Context ApplicationDbContext
- Update Database
Update the database to apply the migration:
Update-Database
Following tables will be added for openiddict
In open iddict AddErver options, set token url
options.SetTokenEndpointUris("connect/token");
📝 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; }
}
- 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
}
}
🔑 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
}
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"
}'
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'
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkMwMzE4N0NGOERDOTMxQzQ5QjQ5RTg3MzlFODQ4RDU2MzlEM0Y1NTYiLCJ4NXQiOiJ3REdIejQzSk1jU2JTZWh6bm9TTlZqblQ5VlkiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MjQ5LyIsImV4cCI6MTcxOTkwNTQxNywiaWF0IjoxNzE5OTAxODE3LCJqdGkiOiI5ZWIzNmZjMS02ZDY1LTQ3OWQtOWJmZC1hZGZjNWVhNzc3Y2EiLCJzdWIiOiI2YmE0ODk1OC0yZjBhLTQ5ZTktYTg2OC1iZTMzNGQ4N2UxYjgiLCJhdWQiOiJSZXNvdXJzZSIsIm9pX3Rrbl9pZCI6ImJmNzA1M2YyLWZjZTgtNDg5YS1hYTgyLTliZmFmOGNlNGUxOSJ9.SYbM8ltyiftgqj6AFABJA_zbiXzlQtLJR2E4xt4W2y85AIlACSBzC3i4Ppg5nsMzgscFGbcO8MOYM3gB6EvViKugdV4BOL26x9UVWnEzOV_BC9p-TYx58EA0Ewx2m3KEqXlJfeLNaAg8H9MyXwIQkc9mbE89MDKhZ3udlI2qElWH2-JbF39mXjgpPHjiMP1UV2Dvp0slewNYeTlj04YY7iSnuEkawDrPAqfWVQPePEuefXVuS139eGLeNdnDxSa16l1tv6V08JcqrSRrRJpDo0yldt07WKCPz8e9lyCts6oiUvNSPSKnf5RE3RSl8jKoa2JnaxfAVyG106JyXacm2g",
"token_type": "Bearer",
"expires_in": 3599
}
- 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"
}
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
- Open Visual Studio and navigate to your existing solution.
- 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
- 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();
- 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();
}
🧪 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.
-
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
});
}
}
- 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);
- 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'
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"
}
- 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'
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;
}
- 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());
}
}
- 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"
}'
- 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);
}
- 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'
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)