Initiating User Registration via OpenID Connect with Duende Identity Server

Β· 760 words Β· 4 minutes to read

There is a new proposal for an extension to OpenID Connect Authentication Framework, called Initiating User Registration via OpenID Connect. It went into public review just last week, which is expected to close later this year.

This very useful extension defines how a client application can indicate to the OpenID Provider that a new user account should be created, rather than triggering the typical login procedure.

In this post we will look at how to support it with Duende Identity Server.

Use case πŸ”—

The main use case for initiating user registration via OIDC is to allow clients to execute account creation workflows directly on the IDP, instead of embedding a registration form locally into the app and letting that interact with a given user API. At the end of the registration workflow the user is redirected back to the client.

In other words, similarly to how a client may initiate e.g. an authorization code flow with the goal of authenticating a user, and then exchange the obtained code to complete the authentication, the client could now trigger a code flow based registration, and ultimately also use the code to obtain the necessary tokens of this new user.

Overall, such approach has plenty of benefits:

  • the registration workflow can be modified at any point without changing anything in the clients
  • users interact only with the IDP so the password can set safely
  • user can be automatically authenticated in at the end of the procedure without an explicit login step

The obvious drawback is that the user experience may suffer to some degree, as the process plays itself out in the browser, which for mobile clients would mean an in-app browser.

Duende IDP implementation πŸ”—

The proposal envisions using a new prompt=create parameter when calling the authorize endpoint of the IDP to trigger user registration. Out of the box, Duende Identity Server supports a fixed set of prompt mode values only - none, login, consent and select_account. Thankfully, it also exposes well defined extensibility points that make supporting a new prompt mode quite easy.

First, we need to implement our own ICustomAuthorizeRequestValidator. The default authorize request validator will only allow the supported built-in prompt modes, and it will strip away any custom ones, like our create. Registering an implementation of ICustomAuthorizeRequestValidator will not replace the built-in default validator (which is good, since it does a lot of work), but it allows us to run a re-validation after the default one completes its main chunk of work. With that, we are able to explicitly restore the create prompt mode that would be stripped away by the default validator. This is shown next.

public class CreateCustomAuthorizeRequestValidator : ICustomAuthorizeRequestValidator
{
    public Task ValidateAsync(CustomAuthorizeRequestValidationContext context)
    {
        var prompt = context.Result.ValidatedRequest.Raw.Get("prompt");
        if (!string.IsNullOrWhiteSpace(prompt) && 
            prompt.Equals("create", StringComparison.OrdinalIgnoreCase))
        {
            context.Result.ValidatedRequest.PromptModes = new[] { "create" };
        }
        
        return Task.CompletedTask;
    }
}

With this in place, we need to instruct Duende Identity Server to perform a custom interaction upon encountering this prompt parameter in the validated authorization request. This can be done by subclassing AuthorizeInteractionResponseGenerator and overriding ProcessInteractionAsync. Should we encounter no create prompt mode, we will let the base class continue as it would, but if we find it, we shall redirect the user to our registration page. This is shown below.

public class CreateAuthorizeInteractionResponseGenerator : AuthorizeInteractionResponseGenerator
{
    public CreateAuthorizeInteractionResponseGenerator(IdentityServerOptions options, 
        ISystemClock clock, 
        ILogger<AuthorizeInteractionResponseGenerator> logger, 
        IConsentService consent, 
        IProfileService profile)
     : base(options, clock, logger, consent, profile)
    {
    }

    public override async Task<InteractionResponse> ProcessInteractionAsync(
        ValidatedAuthorizeRequest request, ConsentResponse consent = null)
    {
        if (!request.PromptModes.Contains("create"))
        {
            return await base.ProcessInteractionAsync(request, consent);
        }
        
        request.Raw.Remove("prompt");
        var response = new InteractionResponse
        {
            RedirectUrl = "/account/register"
        };
        
        return response;
    }
}

In this sample, we just assume that there is a registration page in the IDP under the /account/register path. How it is implemented is highly specific to the given IDP setup - how the user store is managed, whether MFA is in place, what are the rules for registering, how to confirm an email or phone number, is it passwordless and so on. It is also irrelevant to this discussion.

The important thing is that upon handling the registration, we would still be in scope of the authorization context interaction similar to how it is shown in all login samples and using the same methodology, once the account is created and confirmed, we could redirect back to the client’s return URL.

The only thing left to make it work, is to make sure our services are properly registered in the DI container, which can be done immediately after calling AddIdentityServer(…) at server startup:

builder.Services.
    AddScoped<IAuthorizeInteractionResponseGenerator, CreateAuthorizeInteractionResponseGenerator>();
builder.Services.
    AddScoped<ICustomAuthorizeRequestValidator, CreateCustomAuthorizeRequestValidator>();

About


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github and on Mastodon.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP