NikiforovAll / keycloak-authorization-services-dotnet

Authentication and Authorization with Keycloak and ASP.NET Core 🔐
https://nikiforovall.github.io/keycloak-authorization-services-dotnet/
MIT License
480 stars 113 forks source link

Protected Resource Builder + MVC #104

Open NikiforovAll opened 6 months ago

NikiforovAll commented 6 months ago

Currently, header propagation middleware assumes the presence of an access token in a header. This is not true for cookie-based applications

luylucas10 commented 4 months ago

Is there a way, even not official from the library, to do that? I'm building a mvc app and I really want to use this keycloak resource. I've tryied on some ways, but didn't work.

I used the source directly, trying to put the tokens on cookies and recover, but no success.

NikiforovAll commented 4 months ago

Hi @luylucas10 , it should be possible. The only issue is to find a valid token for exchange. Have you tried exchaing id_token?

luylucas10 commented 4 months ago

EDIT: Nop, It doesn't work, I've tested more now and have a problem to check permission because ideed it needs the user token, not just api access.

I could find a way.

I put "DisableHeaderPropagation": true in the keycloak section in appsettings.cs, configuring the Program.cs like this:

builder
    .Services
    .AddAuthorization()
    .AddKeycloakAuthorization()
    .AddAuthorizationServer(builder.Configuration)
    .ConfigureHttpClient(async (provider, client) =>
    {
        var tokenResponse = await client.RequestTokenAsync(new TokenRequest()
        {
            Address = builder.Configuration["Keycloak:auth-server-url"]+ "realms/" + builder.Configuration["Keycloak:realm"] + "/protocol/openid-connect/token",
            GrantType = "client_credentials",
            ClientId = builder.Configuration["Keycloak:resource"],
            ClientSecret = builder.Configuration["Keycloak:credentials:secret"],
        });
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(tokenResponse.TokenType, tokenResponse.AccessToken);
    });

// Add services to the container.
builder.Services.AddControllersWithViews(a => a.AddProtectedResources());

I think that isn't the best way, but works. I'd try to use Duende Token Management but had problems with DI. Now in the action I can use [ProtectedResource("resource", "scope")] in the MVC controllers.

If you could provide a better solution I'll really appreciate that. I can commit this example, I've created a new project at samples directory of the repo.---

luylucas10 commented 4 months ago

Finally I have a solution.

This was the reference that I used: https://github.com/mderriey/aspnet-core-token-renewal/blob/master/src/MvcClient/Startup.cs

We still need the "DisableHeaderPropagation": true in the appsettings and our program.cs like this:

builder
    .Services
    .AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddKeycloakWebApp(builder.Configuration.GetSection(KeycloakAuthenticationOptions.Section),
        configureOpenIdConnectOptions: options =>
        {
            options.SaveTokens = true;
            options.ResponseType = OpenIdConnectResponseType.Code;
            options.GetClaimsFromUserInfoEndpoint = true;
            options.MapInboundClaims = false;
            options.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Name;
            options.TokenValidationParameters.RoleClaimType = ClaimTypes.Role;
            options.Events = new OpenIdConnectEvents
            {
                OnSignedOutCallbackRedirect = context =>
                {
                    context.Response.Redirect("/");
                    context.HandleResponse();
                    return Task.CompletedTask;
                },
                // guess this is the needed configurationn
                OnTokenResponseReceived = context =>
                {
                    var identity = context?.Principal?.Identity as ClaimsIdentity;
                    identity?.AddClaims(new[]
                    {
                        new Claim("access_token", context.TokenEndpointResponse.AccessToken),
                        new Claim("id_token", context.TokenEndpointResponse.IdToken)
                    });
                   // makes cookie and token with same expiration
                    context.Properties.IsPersistent = true;
                    context.Properties.ExpiresUtc = new JwtSecurityToken(context.TokenEndpointResponse.AccessToken).ValidTo;
                    return Task.CompletedTask;
                }
            };
        });

builder
    .Services
    .AddAuthorization()
    .AddKeycloakAuthorization()
    .AddAuthorizationServer(builder.Configuration)
    .ConfigureHttpClient(async (provider, client) =>
    {
        // get the token from user cookie and put in the header
        var ctx = provider?.GetService<IHttpContextAccessor>()?.HttpContext;
        var header = new AuthenticationHeaderValue("Bearer", await ctx?.GetTokenAsync("access_token"));
        client.DefaultRequestHeaders.Authorization = header;
    });

Now I have the [ProtectedResource] working. Thank you for working on this integration for dotnet, I'll contribute, but for now, I have to speed up my personal project.

NikiforovAll commented 4 months ago

Awesome, I'm looking forward to your contribution. Looks good to me!