Dotnet

OpenIddict - Password Flow [EP.2]

Authorizing with a user's username and password using OpenIddict. The full name of this flow is Resource Owner Password Credentials, though you'll also see it called the Password Flow.
Tech Blogs

Intro

In the last blog we talked about OpenIddict, the Authorization Server and the flow for authorizing with client credentials, including an example of setting up the project and using it with client credentials.

In this blog we'll talk about the flow for authorizing with a user's username and password using OpenIddict. The full name of this flow is Resource Owner Password Credentials, and different places may call it by other names, such as Password Flow, Password Credentials or Resource Owner Password Flow.

Configuring OpenIddict

This blog only shows authorizing with the Password flow. Setting up the project and the environment is the same as for the ClientCredentials flow.

Required Dependencies:

Create and configure project

Create the DbContext
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
}

Add the ConnectionString to appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "DataSource=Database\\app.db"
  }
}

Use EntityFramework in Program.cs

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    // Configure Entity Framework Core to use Microsoft Sqlite.
    options.UseSqlite(connectionString);

    // Register the entity sets needed by OpenIddict.
    options.UseOpenIddict();
});

// Register the Identity services.
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

Run the EF Migrations commands to create the database and tables
dotnet ef migrations add init
dotnet ef database update

Use OpenIddict in Program.cs
builder.Services.AddOpenIddict()
    .AddCore(options =>
    {
        options.UseEntityFrameworkCore()
               .UseDbContext<ApplicationDbContext>();
    })
    .AddServer(options =>
    {
        options.SetTokenEndpointUris("/connect/token");

        options.AllowPasswordFlow();

        options.AcceptAnonymousClients();

        options.AddEphemeralEncryptionKey()
               .AddEphemeralSigningKey();

        options.UseAspNetCore()
               .EnableTokenEndpointPassthrough();
    })
    .AddValidation(options =>
    {
        options.UseLocalServer();

        options.UseAspNetCore();
    });

builder.Services.AddAuthentication().AddJwtBearer();

As you can see, in OpenIddict's server options we only call AllowPasswordFlow() and that's enough to use the password flow. But since this blog is only about the password flow, with no integration with any other flow, and OpenIddict normally asks for client credentials, the next thing we have to declare is AcceptAnonymousClients, so OpenIddict knows it doesn't need client credentials to be sent.


Configuring token endpoint

OpenIddict doesn't care how the application does authentication, so the thing we have to implement ourselves is the authentication part, in the form of token-based authentication. We need to add an endpoint where the client requests an access token. In this blog I'll use ASP.NET Core Identity for authentication, which makes managing user accounts really easy for anyone on .NET.

app.MapPost("/connect/token", async (HttpContext context, SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager) =>
{
    var request = context.GetOpenIddictServerRequest() ?? throw new Exception("User context not found.");
    if (request.IsPasswordGrantType())
    {
        var authenticationSchemes = new List<string>
        {
            OpenIddictServerAspNetCoreDefaults.AuthenticationScheme
        };
        var user = await userManager.FindByNameAsync(request.Username);
        if (user == null)
        {
            var properties = new AuthenticationProperties(new Dictionary<string, string>()
            {
                [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid.",
            });

            return Results.Forbid(properties, authenticationSchemes);
        }

        var result = await signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true);
        if (!result.Succeeded)
        {
            var properties = new AuthenticationProperties(new Dictionary<string, string>
            {
                [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid.",
            });

            return Results.Forbid(properties, authenticationSchemes);
        }

        var identity = new ClaimsIdentity(
            authenticationType: TokenValidationParameters.DefaultAuthenticationType,
            nameType: Claims.Name,
            roleType: Claims.Role);

        identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user))
                .SetClaim(Claims.Email, await userManager.GetEmailAsync(user))
                .SetClaim(Claims.Name, await userManager.GetUserNameAsync(user))
                .SetClaims(Claims.Role, (await userManager.GetRolesAsync(user)).ToImmutableArray());

        identity.SetScopes(new[]
        {
            Scopes.OpenId,
            Scopes.Email,
            Scopes.Profile,
            Scopes.Roles
        }.Intersect(request.GetScopes()));

        identity.SetDestinations(GetDestinations);

        return Results.SignIn(new ClaimsPrincipal(identity), null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    }

    throw new NotImplementedException("The specified grant type is not implemented.");
});

static IEnumerable<string> GetDestinations(Claim claim) => claim.Type switch
{
    Claims.Name or Claims.Email or Claims.Role => new[] { Destinations.AccessToken, Destinations.IdentityToken },
    _ => new[] { Destinations.AccessToken }
};

Seed tester account

Before testing, I'll create a tester account first, by having the app write it into the database before the application starts.


var app = builder.Build();

// application pipeline implementation here 

await SeedTestAccountAsync(app.Services);

async Task SeedTestAccountAsync(IServiceProvider serviceProvider)
{
    using var scope =  serviceProvider.CreateScope();

    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
    var user = await userManager.FindByNameAsync("tester");
    if (user is null)
    {
        user = new IdentityUser { UserName = "tester" };
        await userManager.CreateAsync(user, "P@ssw0rd!");
    }
}

await app.RunAsync();

Get user token

In this step we authenticate with the username and password of the tester account we seeded into the database. The password flow uses an http request in this form:

POST /token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=USERNAME&password=PASSWORD

I'm still using the Thunder client plugin for VS code to make the http request, same as before.

OpenIddict_Password_token.png

That's it, the client can now use the access token.

Conclusion

From the above you can see that the Password flow, which is a standard from OAuth, is pretty similar to JwtBearer (Json Web Token), since the token we get from authorizing with OpenIddict is a Jwt. The difference is that OpenIddict lets you add more security on top, for example by combining it with ClientCredentials or with other Authorization Flows in the OpenID Connect or OAuth standards.

From a development point of view, if all you need is to verify who the user is, with nothing about checking permissions, you may not need OpenIddict at all. JwtBearer alone is enough and much easier to implement.

Pros:

  • Standardization: OpenIddict uses the OpenID Connect framework, a standard built on OAuth 2.0 that is widely accepted across the software industry.
  • Security: OpenIddict gives you better security than plain OAuth 2.0, with a focus on the user's privacy.
  • Token Management: OpenIddict takes care of creating, validating and issuing tokens.
  • Flexibility: OpenIddict is flexible enough to configure to fit what we need.

Cons:

  • Complexity: OpenIddict is more complex than JwtBearer, because it has more moving parts and needs a deeper understanding.
  • Overhead: OpenIddict may add more overhead than JwtBearer when managing tokens.
  • Learning Curve: It takes more time to learn and understand the framework.

You can find the code sample here

checkout: GitHub


Found this useful? You can support the blog with a coffee.

Buy me a coffee