ASP.NET Core OpenIdConnect and admin consent on the same callback path

Viewed 396

I have implemented an OpenIdConnect with Azure. The code is approximately like this:

            var options = new OpenIdConnectOptions
            {
                SignInScheme = PersistentSchemeName,
                CallbackPath = "/oauth2office2",
                ClientId = pubConf.ApplicationId,
                Authority = $"https://login.microsoftonline.com/{configuration.TenantId}"
            };

It works perfectly. But I also need admin consent and I don't want my users to add two CallbackPaths into my app. So I crafted admin consent url manually. And added a redirect so it won't conflict with a OpenId middleware:

        app.UseRewriter(new RewriteOptions().Add(context =>
        {
            var request = context.HttpContext.Request;
            if (request.Path.StartsWithSegments("/oauth2office2") && request.Method == HttpMethods.Get)
            {
                request.Path = "/oauth2office";
            }
        }));

Now i have a controller at /oauth2office that does some extra stuff for me (actually gets tenant id).

Question - is there a way I can achieve it with OpenIdConnect middleware? While still being on the same callback path. Because adding two paths is an extra i want to avoid. I'm not even sure I can make OpenIdConnect work with admin consent actually.

2 Answers

One option is to add two AddOpenIDConnect(...) instances with different schema names and different callback endpoints?

You can only have one endpoint per authentication handler.

Also, do be aware that the callback request to the openidconnect handler is done using HTTP POST, like

POST /signin-oidc HTTP/1.1

In your code you are looking for a GET

if (request.Path.StartsWithSegments("/oauth2office2") && request.Method == HttpMethods.Get)

This can be done with a single OpenIdConnect handler by overriding the events RedirectToIdentityProvider and MessageReceived.

    public override async Task RedirectToIdentityProvider(RedirectContext context)
    {
        if (!context.Properties.Items.TryGetValue("AzureTenantId", out var azureTenantId))
            azureTenantId = "organizations";

        if (context.Properties.Items.TryGetValue("AdminConsent", out var adminConsent) && adminConsent == "true")
        {
            if (context.Properties.Items.TryGetValue("AdminConsentScope", out var scope))
                context.ProtocolMessage.Scope = scope;
            context.ProtocolMessage.IssuerAddress =
                $"https://login.microsoftonline.com/{azureTenantId}/v2.0/adminconsent";
        }

        await base.RedirectToIdentityProvider(context);
    }

    public override async Task MessageReceived(MessageReceivedContext context)
    {
        // Handle admin consent endpoint response.
        if (context.Properties.Items.TryGetValue("AdminConsent", out var adminConsent) && adminConsent == "true")
        {
            if (!context.ProtocolMessage.Parameters.ContainsKey("admin_consent"))
                throw new InvalidOperationException("Expected admin_consent parameter");

            var redirectUri = context.Properties.RedirectUri;
            var parameters = context.ProtocolMessage.Parameters.ToQueryString();
            redirectUri += redirectUri.IndexOf('?') == -1
                ? "?" + parameters
                : "&" + parameters;
            context.Response.Redirect(redirectUri);
            context.HandleResponse();
            return;
        }

        await base.MessageReceived(context);
    }

Then when you need to do admin consent, craft a challenge with the correct properties:

    public IActionResult Register()
    {
        var redirectUrl = Url.Action("RegisterResponse");
        var properties = new OpenIdConnectChallengeProperties
        {
            RedirectUri = redirectUrl,
            Items =
            {
                { "AdminConsent", "true" },
                { "AdminConsentScope", "https://graph.microsoft.com/.default" }
            }
        };

        return Challenge(properties, "AzureAd");
    }

    public IActionResult RegisterResponse(
        bool admin_consent,
        string tenant,
        string scope)
    {
        _logger.LogInformation("Admin Consent for tenant {tenant}: {admin_consent} {scope}", tenant, admin_consent,
            scope);
        return Ok();
    }
Related