openiddict / openiddict-core

Flexible and versatile OAuth 2.0/OpenID Connect stack for .NET
https://openiddict.com/
Apache License 2.0
4.44k stars 522 forks source link

Implement impersonation/delegation behavior #1489

Closed a-a-k closed 2 years ago

a-a-k commented 2 years ago

Confirm you've already contributed to this project or that you sponsor it

Version

3.x

Question

I was inspired by this comment (I'm still not sure if I need to ask this question there) when was starting to implement something. If I understood correctly, I need to implement a call to token endpoint with specific grant type and custom request parameter.

The main question I have: how to trigger that request, should I do that using http client or initialize a challenge or somehow else? if I do that using http client, how can I preserve related principal? If I can't pass principal with the request, then how should I validate the request? I believe, this is something a quite simple, but I can't grope the answer.

kinosang commented 2 years ago

OAuth requires a "flow" to do so. so a Relying Party or a "Gateway" should do HTTP request for token exchange.

a-a-k commented 2 years ago

@kinosang I know about that OAuth specification, but we don't have to follow this. All the more so, OpenIddict itself has not token_exchange flow implementation yet. Well, we're trying to make something to get push off.

kevinchalet commented 2 years ago

I was inspired by https://github.com/openiddict/openiddict-samples/issues/75#issuecomment-531502199 comment (I'm still not sure if I need to ask this question there) when was starting to implement something. If I understood correctly, I need to implement a call to token endpoint with specific grant type and custom request parameter.

While terms are similar, the impersonation model discussed in that thread has nothing to do with RFC8693: it's just a custom parameter used by a regular code flow client to tell the authorization server that a specific user (the impersonated user) - different from the logged in user (the impersonator) - should be used to create the ClaimsPrincipal that OpenIddict will need to create the tokens (which typically requires a special role/permission). It's very easy to implement as the only thing you need to do is tweak your Authorize action to use the impersonated identity after checking the impersonator is allowed to do so.

Delegation/impersonation as defined in RFC8693 is a much more complex beast that is actually a flow in itself: it uses the regular token endpoint with a specific grant_type with parameters and semantics that are unique to RFC8693. The main use case for delegation/impersonation as defined in RFC8693 is when API A need to communicate with API B on behalf of the first user while preserving the identity of API A so that API B can make appropriate security decisions based on the identity of each party involved.

As I mentioned in one of my emails, implementing RFC8693 is not an easy task: the current OpenIddict bits will only help you with the token request/response handling part but that's pretty much all. All the token validation and generation part will be completely up to you. If you decide to implement that yourself, I'd recommend using OpenIddict 4.0 as the revamped token generation/validation pipeline should make things a bit easier.

a-a-k commented 2 years ago

Sure, I don't mean implementation of protocol, that's why I named the issue 'implementing behavior'. We have no required skills for that and have no time too.

It's just a custom parameter used by a regular code flow client to tell the authorization server that a specific user (the impersonated user) - different from the logged in user (the impersonator) - should be used to create the ClaimsPrincipal that OpenIddict will need to create the tokens (which typically requires a special role/permission). It's very easy to implement as the only thing you need to do is tweak your Authorize action to use the impersonated identity after checking the impersonator is allowed to do so.

Okay, I shouldn't use token endpoint, but authorize. Anyway, I still don't understand, how can I achieve that? The Authorize action fires only twice and both of them at the login process, so how it should be reached within logged user's activity? As I realize, authorization happens on a client side until a user's authentication is valid. Also, how it is supposed to add some 'impersonation' parameter to request properties?

I understand, that may seem too trivial, but that's we stuck on now.

kevinchalet commented 2 years ago

IMHO, before discussing implementation details, you should start by describing your scenario.

a-a-k commented 2 years ago

Our scenario is pretty simple: web client with cookie authentication, user clicks some impersonate button. We have had this implemented using OAuthAuthorizationServerProvider via GrantCustomExtension method before migrated to netcore and OpenIddict. After a user was clicking the button, we were appending a 'target' user id to the request and validated this request. We were preserving impersonator info to be able to make a reverse operation.

Would be better to illustrate this with code. ``` public override async Task GrantCustomExtension(OAuthGrantCustomExtensionContext context) { switch (context.GrantType) { case AuthConstants.ImpersonationGrant: await ImpersonateAsync().ConfigureAwait(false); return; case AuthConstants.RevertImpersonationGrant: await RevertAsync().ConfigureAwait(false); return; default: context.SetError($"Unknown grant_type '{context.GrantType}'!"); return; } async Task ImpersonateAsync() { var auth = context.Request.Context.Authentication; var token = context.Request.Cookies.FirstOrDefault(c => c.Key == AuthConstants.AccessTokenCookie).Value ?? context.Request.Headers.Get(AuthConstants.AuthorizationHeader).Substring(7); var oldTicket = context.Options.AccessTokenFormat.Unprotect(token); if (!oldTicket.Identity.HasClaim(AuthConstants.ModulePermissionClaim, $"{ModuleConstants.Modules.Administration_Companies}/{Permissions.All}") && !oldTicket.Identity.HasClaim(AuthConstants.ModulePermissionClaim, $"{ModuleConstants.Modules.Administration_Users}/{Permissions.All}") && !oldTicket.Identity.HasClaim(AuthConstants.ModulePermissionClaim, $"{ModuleConstants.Modules.Superuser}/{Permissions.All}")) { context.SetError("Impersonation is not allowed!"); return; } var repo = new AuthRepository(context.OwinContext.Get(), context.OwinContext.Get()); var user = await repo.FindUserByIdAsync(context.Parameters["userId"]).ConfigureAwait(false); if (user == null) { context.SetError("invalid_grant", "User with specified ID not found!"); return; } var permissions = repo.GetUserPermissions(user.Id).ToList(); if (!oldTicket.Identity.HasClaim(AuthConstants.ModulePermissionClaim, $"{ModuleConstants.Modules.Administration_Companies}/{Permissions.All}") && !oldTicket.Identity.HasClaim(AuthConstants.ModulePermissionClaim, $"{ModuleConstants.Modules.Superuser}/{Permissions.All}") && permissions.Contains((ModuleConstants.Modules.Administration_Companies, Permissions.All))) { context.SetError("You can't impersonate a user with EA role!"); return; } if (!oldTicket.Identity.HasClaim(AuthConstants.ModulePermissionClaim, $"{ModuleConstants.Modules.Superuser}/{Permissions.All}") && permissions.Contains((ModuleConstants.Modules.Superuser, Permissions.All))) { context.SetError("You can't impersonate a Superuser!"); return; } if (oldTicket.Identity.HasClaim(AuthConstants.ImpersonationClaim, "true")) { context.SetError("You are already impersonated! Please, revert current impersonation before."); return; } var userName = context.Parameters["username"]; var origUserId = oldTicket.Identity.GetUserId(); var allowedOrigins = context.OwinContext.Get("clientAllowedOrigin"); context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", allowedOrigins.Split(',')); var identity = new ClaimsIdentity(DefaultAuthenticationTypes.ExternalCookie); identity.AddClaim(new Claim(ClaimTypes.Name, userName)); identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "OWIN Provider", ClaimValueTypes.String)); identity.AddClaim(new Claim(AuthConstants.ImpersonationClaim, "true")); identity.AddClaim(new Claim(AuthConstants.OriginalUserIdClaim, origUserId)); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); foreach (var (module, permission) in permissions) { identity.AddClaim(new Claim(AuthConstants.ModulePermissionClaim, $"{module}/{permission}")); } var tokenLifetime = int.Parse(ConfigurationManager.AppSettings[AuthConstants.AccessTokenLifetime]); var now = DateTime.UtcNow; var expires = now.AddMinutes(tokenLifetime); var props = new AuthenticationProperties(new Dictionary { { AuthConstants.ClientIdOAuth, context.ClientId ?? string.Empty }, { "username", userName }, { "userId", user.Id }, }) { AllowRefresh = true, IssuedUtc = now, ExpiresUtc = expires, }; var ticket = new AuthenticationTicket(identity, props); var options = new CookieOptions { Expires = expires }; auth.SignIn(props, identity); context.Response.Cookies.Append(AuthConstants.AccessTokenCookie, context.Options.AccessTokenFormat.Protect(ticket), options); context.Validated(ticket); } async Task RevertAsync() { var auth = context.Request.Context.Authentication; var token = context.Request.Cookies.FirstOrDefault(c => c.Key == AuthConstants.AccessTokenCookie).Value ?? context.Request.Headers.Get(AuthConstants.AuthorizationHeader).Substring(7); var oldTicket = context.Options.AccessTokenFormat.Unprotect(token); if (!oldTicket.Identity.HasClaim(AuthConstants.ImpersonationClaim, "true")) { context.SetError("There no any impersonation existed!"); return; } var origUserId = oldTicket.Identity.Claims.FirstOrDefault(c => c.Type == AuthConstants.OriginalUserIdClaim)?.Value; var allowedOrigins = context.OwinContext.Get("clientAllowedOrigin"); context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", allowedOrigins.Split(',')); var identity = new ClaimsIdentity(DefaultAuthenticationTypes.ExternalCookie); identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "OWIN Provider", ClaimValueTypes.String)); var repo = new AuthRepository(context.OwinContext.Get(), context.OwinContext.Get()); var user = await repo.FindUserByIdAsync(origUserId).ConfigureAwait(false); if (user == null) { context.SetError("invalid_grant", "User with specified ID not found!"); return; } var userName = user.UserName; identity.AddClaim(new Claim(ClaimTypes.Name, userName)); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); var permissions = repo.GetUserPermissions(user.Id); foreach (var (module, permission) in permissions) { identity.AddClaim(new Claim(AuthConstants.ModulePermissionClaim, $"{module}/{permission}")); } var tokenLifetime = int.Parse(ConfigurationManager.AppSettings[AuthConstants.AccessTokenLifetime]); var now = DateTime.UtcNow; var expires = now.AddMinutes(tokenLifetime); var props = new AuthenticationProperties(new Dictionary { { AuthConstants.ClientIdOAuth, context.ClientId ?? string.Empty }, { "username", userName }, { "userId", user.Id }, }) { AllowRefresh = true, IssuedUtc = now, ExpiresUtc = expires, }; var ticket = new AuthenticationTicket(identity, props); var options = new CookieOptions { Expires = expires }; auth.SignOut(DefaultAuthenticationTypes.ExternalCookie); auth.SignIn(props, identity); context.Response.Cookies.Append(AuthConstants.AccessTokenCookie, context.Options.AccessTokenFormat.Protect(ticket), options); context.Validated(ticket); } } ```
kevinchalet commented 2 years ago

Good thing I asked about your scenario (which has indeed not much to do with RFC8693 🤣)

You could implement a similar thing in OpenIddict using a custom grant but I wouldn't recommend it at all as your implementation - based on cookies - was sadly flawed and prone to CSRF/session fixation attacks.

Since you're using cookie authentication for that, the approach I mentioned in https://github.com/openiddict/openiddict-samples/issues/75#issuecomment-531502199 is the best option.

The Authorize action fires only twice and both of them at the login process, so how it should be reached within logged user's activity?

An OIDC client can start an authorization code flow at any time. Should the client need to start an authorization code flow with impersonation enforced for a specific user, it could redirect the user agent to the authorization server with the impersonated username in the parameters and if the impersonator was already logged in, it would be a transparent operation.

If you decide to opt for this approach, the implementation will be very similar to what you have in the Velusia sample, with just a tweak when creating the ClaimsIdentity to use the impersonated user claims when impersonation is used. If you use a custom impersonated_username parameter sent from the client, it can be resolved server-side this way:

var impersonatedUsername = (string?) request["impersonated_username"];

As I realize, authorization happens on a client side until a user's authentication is valid.

I'm not sure I'm following. What do you mean exactly?

Also, how it is supposed to add some 'impersonation' parameter to request properties?

It depends on the OIDC client stack you're using. With the new OpenIddict client stack introduced in 4.0, it's as easy as adding a value in AuthenticationProperties.Parameters. Here's an example that sends an identity_provider parameter whose value is set by the client:

https://github.com/openiddict/openiddict-core/blob/00c657340a69d35baba602214f2a173ba014a144/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs#L12-L53

a-a-k commented 2 years ago

An OIDC client can start an authorization code flow at any time. Should the client need to start an authorization code flow with impersonation enforced for a specific user, it could redirect the user agent to the authorization server with the impersonated username in the parameters and if the impersonator was already logged in, it would be a transparent operation.

Well, this is the thing I'm mainly asking for. How are we able to enforce client to start authorization?

The main question I have: how to trigger that request, should I do that using http client or initialize a challenge or somehow else?

kevinchalet commented 2 years ago

It's sadly impossible to give you an answer without having more information on the "web client" you're mentioning: is it an ASP.NET Core application? Do you use the MSFT OIDC handler?

a-a-k commented 2 years ago

Correct - ASP.NET Core 6, MSFT OIDC handler.

kevinchalet commented 2 years ago

You'll need a custom event handler to be able to attach the custom parameter to the authorization request:

services.AddAuthentication().AddOpenIdConnect(options =>
{
    // ...

    options.Events.OnRedirectToIdentityProvider = context =>
    {
        if (context.ProtocolMessage.RequestType is OpenIdConnectRequestType.Authentication)
        {
            // Attach the impersonated username resolved from the authentication
            // properties to the request, if one was specified during the challenge.
            var username = context.Properties.GetParameter<string>("impersonated_username");
            if (!string.IsNullOrEmpty(username))
            {
                context.ProtocolMessage.SetParameter("impersonated_username", username);
            }
        }

        return Task.CompletedTask;
    };
});

Once it's in place, you can easily attach an impersonated_username to the authentication properties to trigger a challenge operation with impersonation:

[HttpGet("~/login")]
public ActionResult LogInWithImpersonation(string username)
{
    var properties = new AuthenticationProperties
    {
        RedirectUri = "/"
    };

    if (!string.IsNullOrEmpty(username))
    {
        properties.SetParameter("impersonated_username", username);
    }

    return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme);
}
a-a-k commented 2 years ago

Okay, seems I got the answer and become much closer to a completion. I've done with your tips, but by some reason the request can't get authenticated at this point. What does that mean?

kevinchalet commented 2 years ago

Do you use ASP.NET Core Identity to handle the cookie authentication process? If not, you'll need to change the constant to use your custom cookie authentication scheme.

a-a-k commented 2 years ago

I've tried to use CookieAuthenticationDefaults.AuthenticationScheme instead of OpenIdConnectDefaults.AuthenticationScheme, but unfortunately with the same result.

kevinchalet commented 2 years ago

Please post your Startup/Program class.

a-a-k commented 2 years ago
Here it goes ``` public class Startup { private readonly IWebHostEnvironment _env; private readonly IConfiguration _configuration; public Startup(IConfiguration configuration, IWebHostEnvironment env) { _env = env; _configuration = configuration; } public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews() .AddHttpExceptions(); services.AddCors(options => options.AddDefaultPolicy(policy => policy.WithOrigins(_configuration.GetRequiredSection("AllowedHosts").Get()) .AllowAnyHeader() .AllowAnyMethod())); services.AddDbContext(options => { if (_env.IsDevelopment()) { options.UseInMemoryDatabase(nameof(AuthDbContext)); } else { options.UseNpgsql(_configuration.GetConnectionString("AuthDbConnection")); } options.UseOpenIddict(); }); services.AddDbContext(options => options.UseNpgsql(_configuration.GetConnectionString("FmsDbConnection"))); services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); services.Configure(options => { // Configure Identity to use the same JWT claims as OpenIddict instead // of the legacy WS-Federation claims it uses by default (ClaimTypes), // which saves you from doing the mapping in your authorization controller. options.ClaimsIdentity.UserNameClaimType = OpenIddictConstants.Claims.Name; options.ClaimsIdentity.UserIdClaimType = OpenIddictConstants.Claims.Subject; options.ClaimsIdentity.RoleClaimType = OpenIddictConstants.Claims.Role; options.ClaimsIdentity.EmailClaimType = OpenIddictConstants.Claims.Email; options.SignIn.RequireConfirmedAccount = false; }); services.AddScoped, CustomClaimsPrincipalFactory>(); services.AddTransient(); services.AddOpenIddict() .AddCore(options => { options.UseEntityFrameworkCore() .UseDbContext(); }) .AddServer(options => { options.SetAccessTokenLifetime(TimeSpan.FromDays(7)); options.AllowClientCredentialsFlow(); options.UseReferenceAccessTokens(); options.AllowAuthorizationCodeFlow() .AllowRefreshTokenFlow() .RequireProofKeyForCodeExchange(); options .SetAuthorizationEndpointUris("/connect/authorize") .SetLogoutEndpointUris("/connect/logout") .SetTokenEndpointUris("/connect/token") .SetIntrospectionEndpointUris("/introspect"); options .AddEphemeralEncryptionKey() .AddEphemeralSigningKey(); options.RegisterScopes("openid", "api", "offline_access", "impersonation", "sync_api"); options .UseAspNetCore() .DisableTransportSecurityRequirement() .EnableTokenEndpointPassthrough() .EnableAuthorizationEndpointPassthrough() .EnableLogoutEndpointPassthrough(); }) .AddValidation(options => { options.UseLocalServer(); options.UseAspNetCore(); }); services.AddAuthorization(); services.Configure>(_configuration.GetRequiredSection("Clients")); services.AddHostedService(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseHttpExceptions(); app.UseCors(); app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedProto, }); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); }); } } ```
kevinchalet commented 2 years ago

I meant the server Startup, where your AuthorizationController is localed 😄

a-a-k commented 2 years ago

Oops, sorry ) I've updated previous comment

kevinchalet commented 2 years ago

I don't see anything suspicious in the Startup configuration. Can you also please share your authorization controller?

a-a-k commented 2 years ago

It's just a copy of mentioned Velusia example AuthorizationController with minimal minor adoptions. Need I post it here anyway?

kevinchalet commented 2 years ago

Yes please. The devil is always in the details so if things don't work as you expect, there's something wrong somewhere.

a-a-k commented 2 years ago

wouldn't be better to give you access to the git repo where the code lies?

kevinchalet commented 2 years ago

Yep, that would work too (assuming your company is fine with that 😅)

a-a-k commented 2 years ago

There no any sensitive information and business logic. Just sent you invitation (Bitbucket repo). Take a look at dev branch, it is most actual.

kevinchalet commented 2 years ago

I see an AccountController in the dev branch. Is it used?

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model)
{
    ViewData["ReturnUrl"] = model.ReturnUrl;

    if (!ModelState.IsValid)
    {
        return View(model);
    }

    var user = await _userManager.FindByNameAsync(model.Username);
    if (user == null)
    {
        return View(model);
    }

    var res = await _signInManager.PasswordSignInAsync(user, model.Password, false, false);
    if (!res.Succeeded)
    {
        return View(model);
    }

    var claims = new List<Claim>
    {
        new(ClaimTypes.Name, user.UserName),
        new(ClaimTypes.NameIdentifier, user.Id),
    };

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

    await HttpContext.SignInAsync(new ClaimsPrincipal(claimsIdentity));

    if (Url.IsLocalUrl(model.ReturnUrl))
    {
        return Redirect(model.ReturnUrl);
    }

    return Redirect("/");
}

There are 2 issues with it:

If this controller is used, try to remove the HttpContext.SignInAsync call to see if it helps.

a-a-k commented 2 years ago

Fixed, but that didn't help :(

kevinchalet commented 2 years ago

I tested it locally with IIS Express - after removing all the references to EyeRide.FMS.Model, adding a new client entry and a fake Identity user - and it worked flawlessly using Postman with PKCE. So either there's an issue with your machine or there's a problem with how you host that application that causes issues with ASP.NET Core Identity.

image

a-a-k commented 2 years ago

Just for sure, are you talking about 'impesonation' or regular authentication?

kevinchalet commented 2 years ago

AFAICT, your controller in your dev branch doesn't contain any logic for impersonation, so it's just "regular authentication". That said, since it's just a simple request parameter, it's unrelated to the cookie problem you described earlier: you should get an authenticated identity here whether you're using impersonation or not.

a-a-k commented 2 years ago

If we are talking about regular authentication, then it undoubtedly works. But the issue is, as I said before, that the request doesn't authenticate at this point when user initiates 'impersonation'. So, I'm a bit confused about the subject of discussion.

kevinchalet commented 2 years ago

But the issue is, as I said before, that the request doesn't authenticate at this point when user initiates 'impersonation'. So, I'm a bit confused about the subject of discussion.

As I said, there's no trace of any impersonation logic in your repo and the client app you're mentioning is not even part of that repo. I can't test code that doesn't exist or help you determine whether the logic is good or not if I can't even see it.

a-a-k commented 2 years ago

I didn't implement any impersonation logic at the server side yet, just because of the mentioned issue. I just can do nothing with an empty authentication result, regardless there exists some logic after or not.

This is how it looks at the client side - a user clicks the button and 'impersonation' request reaches MVC controller action, where challenge performs. After that, OIDC handler handles redirection request and adds the parameter. Right? Next, authorization endpoint hits at the server side, and it is expected that request should be authenticated, but somehow this not happens. Implementations of the handler and MVC controller action are absolutely the same as you posted before. Can share if needed. Did you have a chance to see client's Startup configuration before I replaced it?

kevinchalet commented 2 years ago

This is how it looks at the client side - a user clicks the button and 'impersonation' request reaches MVC controller action, where challenge performs. After that, OIDC handler handles redirection request and adds the parameter. Right? Next, authorization endpoint hits at the server side, and it is expected that request should be authenticated, but somehow this not happens.

Yes, that's how it should work (and it did work when I tested the snippet I shared earlier so 🤷🏻‍♂️)

Can share if needed.

If you need additional assistance, yes. This time, please make sure the solution includes all the needed dependencies so I don't have to remove tons of references on my end 😄

Did you have a chance to see client's Startup configuration before I replaced it?

It looked fine.

a-a-k commented 2 years ago

Well, I definitely need an assistance since I absolutely have no idea what's going wrong. Let me share additional info tomorrow. BTW, here is how it looks now at the break

image image

kevinchalet commented 2 years ago

An AuthenticateResult with None to true indicates that no cookie was found in the request headers, so you'll also want to try capturing a Fiddler trace to see if it's sent by the browser or not.

a-a-k commented 2 years ago

Uhh! Brilliant, I see that there is no cookie in the request to MVC controller and I know why. Digging into this to fix....

a-a-k commented 2 years ago

Is it ok that an OPTIONS request sends to authorize endpoint before GET? This looks weird.

kevinchalet commented 2 years ago

Not really. Is that OPTIONS request a pre-flight CORS request? (the authorization endpoint should NEVER be used with an API client: it's an interactive endpoint meant to be used with 302 redirects).

a-a-k commented 2 years ago

Yeah, exactly pre-flight CORS request. But I have no idea why it appears. Have you? It is definitely not an API client.

UPD Okay, I found the cause.

kevinchalet commented 2 years ago

Okay, I found the cause.

Care to share more details? 😄

a-a-k commented 2 years ago

Later, I'm full in the process :)

a-a-k commented 2 years ago

I figured that out finally. In short - the issue was in the request, it was incorrectly doing via JS service. After getting that fixed, it was really simple to get impersonation to work. Warmest thanks to you for your support! :upside_down_face:

kevinchalet commented 2 years ago

As I said...

The devil is always in the details so if things don't work as you expect, there's something wrong somewhere.

Glad you figured it out 😄

jwillmer commented 2 years ago

@kevinchalet I'm looking at a similar use case and have one question regarding your example in this task:

You propose to add a new parameter to the authentication cookie if the parameter value was defined/transferred by the request. That means each app that supports the impersonation of a user has to implement a check if this parameter exists and then use this information to display the (impersonated) username / validate that this user in fact can execute the requested action?

context.ProtocolMessage.SetParameter("impersonated_username", username);