How to set up OpenIddict to rely on AzureAd without using Microsoft.AspNetCore.Identity.UI

Viewed 1198

Our roles model is different so we can't use the stock Microsoft identity database model and all UX that goes with it, more's the pity.

All I want to do is

  • use OpenIdDict
  • have AzureAd do authentication
  • put my own claims into the claims principal so they go into the identity token when OpenIdDict creates it

I'm not interested in IdentityServer for assorted reasons.

I worked through a tutorial and had no trouble building all this using cookie based authn handled in an AccountController but I cannot figure out how to switch over to Azure and could really use some help.

Startup looks like this

    public void ConfigureServices(IServiceCollection services)
    {
      services.AddControllersWithViews();

      // services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
      //     .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
      //     {
      //       options.LoginPath = "/account/login";
      //     });

      services.AddAuthentication()
          .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
          // from package `Microsoft.Identity.Web`

      services.AddDbContext<DbContext>(options =>
          {
            // Configure the context to use an in-memory store.
            options.UseInMemoryDatabase(nameof(DbContext));

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

      services.AddHostedService<TestData>();

      var openiddictBuilder = services.AddOpenIddict();

      // Register the OpenIddict core components.
      openiddictBuilder.AddCore(options =>
      {
        // Configure OpenIddict to use the EF Core stores/models.
        options.UseEntityFrameworkCore()
        .UseDbContext<DbContext>();
      });

      // Register the OpenIddict server components.
      openiddictBuilder.AddServer(options =>
      {
        options
        .AllowAuthorizationCodeFlow().RequireProofKeyForCodeExchange()
        .AllowClientCredentialsFlow()
        .AllowRefreshTokenFlow()
        .SetAuthorizationEndpointUris("/connect/authorize")
        .SetTokenEndpointUris("/connect/token")

        // Encryption and signing of tokens
        .AddEphemeralEncryptionKey()
        .AddEphemeralSigningKey()
        .DisableAccessTokenEncryption()

        // Register scopes (permissions)
        .RegisterScopes("api")

        // Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
        .UseAspNetCore()
        .EnableTokenEndpointPassthrough()
        .EnableAuthorizationEndpointPassthrough()
        ;
      });
    }

There's an AuthorizeController with an Authorize method that looks like this

    [HttpGet("~/connect/authorize")]
    [HttpPost("~/connect/authorize")]
    [IgnoreAntiforgeryToken]
    public async Task<IActionResult> Authorize()
    {
      var request = HttpContext.GetOpenIddictServerRequest() ??
          throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

      // Retrieve the user principal stored in the authentication cookie.
      // var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
      var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

      // If the user principal can't be extracted, redirect the user to the login page.
      if (!result.Succeeded)
      {
        var authprops = new AuthenticationProperties
        {
          RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
                    Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
        };
        return Challenge(
            authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
            properties: authprops);
      }

      // Create a new claims principal
      var claims = new List<Claim>
      {
          // 'subject' claim which is required
          new Claim(OpenIddictConstants.Claims.Subject, result.Principal.Identity.Name),
          new Claim(OpenIddictConstants.Claims.Role,"admin").SetDestinations(
            OpenIddictConstants.Destinations.IdentityToken),
          new Claim(OpenIddictConstants.Claims.Role,"gerbil wrangler").SetDestinations(
            OpenIddictConstants.Destinations.IdentityToken)
    };

      var claimsIdentity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

      var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

      // Set requested scopes (this is not done automatically)
      claimsPrincipal.SetScopes(request.GetScopes());

      // Signing in with the OpenIdDict authentiction scheme causes OpenIdDict 
      // to issue a code which can be exchanged for an access token
      return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    }

As I understand the theory of operation, OpenIddict proxies authentication and then issues a new token. That implies that the AzureAd redirect_uri ought to be set to an endpoint provided by OpenIddict, probably something like signin-openiddict and assuming that I'm right about all that, the client talking to OpenIddict will in turn provide a completely distinct and unrelated redirect_uri. But I haven't found any documentation covering this yet, so feel free to educate me.

1 Answers

In OpenIddict (and IdentityServer) the login and token generation are separated.

Those 2 parts are generally:

  1. The user logs in using arbitrary methods and the authentication cookie is created.

  2. The token endpoint reads the authentication cookie and creates tokens.

To use Azure Ad Authentication, you need to alter the first part to generate an authentication cookie using Azure Ad. To achieve this you'll need to implement the ExternalLogin and ExternalLoginCallback as seen in this example: https://github.com/openiddict/openiddict-core/blob/cda55862bcba67bf3de4ba08cf512ee9e2269cf5/samples/Mvc.Server/Controllers/AccountController.cs#L141 Instead of using the SignInManager, you need to create the authentication cookie yourself in the ExternalLoginCallback method. This can be done by using the HttpContext.SignInAsync method.

The second part (token generation) is left unchanged and should work without modification.

Related