Skip to content

[Minimal Api][Authentication][Delegate?] Could you add details about required additions for oAuth for Minimal Api? #35910

@DevTKSS

Description

@DevTKSS

Description

I would like to use oAuth in my Minimal API project and found no guide to that for the very basic steps of it. Not in the minimal api and not in the detailed ones, like for the RemoteAuthenticationProvider/Handler issued there:

I were looking into this OpenIdDict+Minimal API using Server Sample and that were using direct Minimal API endpoints in the Programm.cs like added below (1. Codeblock), which seemed to work in general, but as I seen in the ToDo Sample of the Minimal API docs, I wanted to put them into .MapGroup("/connect", ) and use TypedResults, but now that RequestDelegate or Delegate on .MapGet("/authorize",Authorize) is somehow making problems 🤔
Thats why I were searching the minimal api docs through and came finally to the authentication docs here, which I hoped maybe could tell me how to do that request, which seems to require that HttpContext but nothing else and by that could show my user then this simplistic html templated page telling him about the success of his login. From that it should be easy going, isnt it?
Possibly I dont even need that context, but I would not know a alternative way to do that for showing my user that for CallbackUri page...
Can you complete the auth docs or/and tell me where I made that mistake?

Sample Code:

    app.MapGet("/connect/authorize", async context =>
    {
        var request = context.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("Invalid request");

        var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, Claims.Name, Claims.Role);
        identity.AddClaim(Claims.Subject, "dummy_user_id");
        identity.AddClaim(Claims.Name, "Test User");

        var principal = new ClaimsPrincipal(identity);
        principal.SetScopes(Scopes.OpenId, Scopes.Profile, Scopes.Email);

        await context.SignInAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, principal);

        context.Response.Clear();

        var template = await File.ReadAllTextAsync("page.html");
        var code = Uri.EscapeDataString(context.GetOpenIddictServerResponse()!.Code!);
        var state = Uri.EscapeDataString(context.GetOpenIddictServerResponse()!.State!);
        var redirect = new UriBuilder(request.RedirectUri!)
        {
            Query = $"code={code}&state={state}"
        }.Uri.ToString();

        var html = string.Format(template, redirect);

        context.Response.ContentType = "text/html; charset=utf-8";
        await context.Response.WriteAsync(html);
    });

    app.MapPost("/connect/token", async context =>
    {
        var request = context.GetOpenIddictServerRequest() ??
                      throw new InvalidOperationException("Invalid request.");

        if (request.IsAuthorizationCodeGrantType())
        {
            // Normally you retrieve the principal associated with the code.
            // For simplicity, here you recreate it – in production, check that the code has not
            // already been consumed and perform all necessary validations.
            var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            identity.AddClaim(Claims.Subject, "dummy_user_id");
            identity.AddClaim(Claims.Name, "Test User");

            var principal = new ClaimsPrincipal(identity);
            principal.SetScopes(request.GetScopes());

            await context.SignInAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, principal);
        }
        else
        {
            // If the grant type is not recognized, trigger a Challenge or
            // return an error.
            await context.ChallengeAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }
    });

    app.MapGet("/connect/userinfo", async context =>
    {
        // Check that the request is authenticated
        var user = context.User;

        if (user?.Identity is null || !user.Identity.IsAuthenticated)
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsync("Unauthorized.");
            return;
        }

        // Create the object to return. You can include more claims if necessary.
        var userInfo = new
        {
            sub = user.FindFirst(Claims.Subject)?.Value,
            name = user.FindFirst(Claims.Name)?.Value,
            email = user.FindFirst(Claims.Email)?.Value
        };

        // Return the JSON with the user's information
        await context.Response.WriteAsJsonAsync(userInfo);
    });

Refactored code

This is the code I came up with, trying my best to apply the learning from the ToDo Sample, but that did not include such setup at all.
And this is the Linting I get which I not know how to act on:

Image

Below I added comments on each of the Lines where I get the linting

    public static RouteGroupBuilder MapAuthenticationEndpoints(this IEndpointRouteBuilder routes)
    {
        // Map the authentication endpoints
        var group = routes.MapGroup("/connect")
                              .WithTags("Authentication");

        group.MapGet("/authorize", Authorize) // <-- Linted 
                .WithName("Authorize")
                .WithSummary("Authorize user")
                .WithDescription("Authorize a user and return an HTML page with the authorization code")
                .AllowAnonymous();

        group.MapPost("/token", Token) // <-- Linted
                .RequireAuthorization()
                .WithName("Token")
                .WithSummary("Exchange authorization code for access token")
                .WithDescription("Exchange an authorization code for an access token");

        group.MapGet("/userinfo", UserInfo) // <-- Linted
                .RequireAuthorization()
                .WithName("UserInfo")
                .WithSummary("Get user information")
                .WithDescription("Retrieve user information based on the authenticated user's claims");

        return group;
    }

    private static async Task<Results<ContentHttpResult, BadRequest>> Authorize(HttpContext context)
    {
        var request = context.GetOpenIddictServerRequest();
        if (request is null)
            return TypedResults.BadRequest();

        var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, Claims.Name, Claims.Role);
        identity.AddClaim(Claims.Subject, "dummy_user_id");
        identity.AddClaim(Claims.Name, "Test User");

        var principal = new ClaimsPrincipal(identity);
        principal.SetScopes(Scopes.OpenId, Scopes.Profile, Scopes.Email);

        await context.SignInAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, principal);

        context.Response.Clear();

        var template = await File.ReadAllTextAsync("page.html");
        var code = Uri.EscapeDataString(context.GetOpenIddictServerResponse()!.Code!);
        var state = Uri.EscapeDataString(context.GetOpenIddictServerResponse()!.State!);
        var redirect = new UriBuilder(request.RedirectUri!)
        {
            Query = $"code={code}&state={state}"
        }.Uri.ToString();

        var html = string.Format(template, redirect);

        return TypedResults.Content(html, MediaTypeNames.Text.Html, Encoding.UTF8, StatusCodes.Status200OK);
    }

    private static async Task<Results<Ok, UnauthorizedHttpResult, BadRequest>> Token(HttpContext context)
    {
        var request = context.GetOpenIddictServerRequest();
        if (request is null)
            return TypedResults.BadRequest();

        if (request.IsAuthorizationCodeGrantType())
        {
            // Normally you retrieve the principal associated with the code.
            // For simplicity, here you recreate it – in production, check that the code has not
            // already been consumed and perform all necessary validations.
            var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            identity.AddClaim(Claims.Subject, "dummy_user_id");
            identity.AddClaim(Claims.Name, "Test User");

            var principal = new ClaimsPrincipal(identity);
            principal.SetScopes(request.GetScopes());

            await context.SignInAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, principal);
            return TypedResults.Ok();
        }
        else
        {
            // If the grant type is not recognized, trigger a Challenge or
            // return an error.
            await context.ChallengeAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            return TypedResults.Unauthorized();
        }
    }

    private static Task<Results<Ok<object>, UnauthorizedHttpResult>> UserInfo(HttpContext context)
    {
        // Check that the request is authenticated
        var user = context.User;

        if (user?.Identity is null || !user.Identity.IsAuthenticated)
        {
            return Task.FromResult<Results<Ok<object>, UnauthorizedHttpResult>>(TypedResults.Unauthorized());
        }

        // Create the object to return. You can include more claims if necessary.
        var userInfo = new
        {
            sub = user.FindFirst(Claims.Subject)?.Value,
            name = user.FindFirst(Claims.Name)?.Value,
            email = user.FindFirst(Claims.Email)?.Value
        };

        return Task.FromResult<Results<Ok<object>, UnauthorizedHttpResult>>(TypedResults.Ok((object)userInfo));
    }
}

my Repo where I trying to implement the shown api:
https://github.com/DevTKSS/DevTKSS.MyManufacturerERP/blob/master/src/WebApi/WebApi/Endpoints/Authentication/AuthenticationEndpoints.cs

Related Issue

First mentioned but unanswered as maybe it requires a seperate issue it now got:

Page URL

https://learn.microsoft.com/de-de/aspnet/core/fundamentals/minimal-apis/security?view=aspnetcore-9.0

Content source URL

https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/security.md

Document ID

3a9d7eb8-6c1f-4619-00fa-9b69dbe3dcea

Platform Id

d905a302-a7a8-ce53-f765-5ab6bef661e7

Article author

@captainsafia

Metadata

  • ID: 3a9d7eb8-6c1f-4619-00fa-9b69dbe3dcea
  • PlatformId: d905a302-a7a8-ce53-f765-5ab6bef661e7
  • Service: aspnet-core
  • Sub-service: fundamentals

Related Issues

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions