Closed a-a-k closed 2 years ago
OAuth requires a "flow" to do so. so a Relying Party or a "Gateway" should do HTTP request for token exchange.
@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.
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.
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.
IMHO, before discussing implementation details, you should start by describing your scenario.
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.
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:
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?
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?
Correct - ASP.NET Core 6, MSFT OIDC handler.
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);
}
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?
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.
I've tried to use CookieAuthenticationDefaults.AuthenticationScheme
instead of OpenIdConnectDefaults.AuthenticationScheme
, but unfortunately with the same result.
Please post your Startup
/Program
class.
I meant the server Startup
, where your AuthorizationController
is localed 😄
Oops, sorry ) I've updated previous comment
I don't see anything suspicious in the Startup
configuration. Can you also please share your authorization controller?
It's just a copy of mentioned Velusia example AuthorizationController with minimal minor adoptions. Need I post it here anyway?
Yes please. The devil is always in the details so if things don't work as you expect, there's something wrong somewhere.
wouldn't be better to give you access to the git repo where the code lies?
Yep, that would work too (assuming your company is fine with that 😅)
There no any sensitive information and business logic. Just sent you invitation (Bitbucket repo). Take a look at dev
branch, it is most actual.
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:
CookieAuthenticationDefaults.AuthenticationScheme
) that doesn't seem to be registered in the Startup
config.PasswordSignInAsync()
already creates and returns a cookie for you, so you're essentially trying to return 2 different authentication cookies as part of the same call, which may hit cookie size limits depending on the browser you're using.If this controller is used, try to remove the HttpContext.SignInAsync
call to see if it helps.
Fixed, but that didn't help :(
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.
Just for sure, are you talking about 'impesonation' or regular authentication?
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.
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.
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.
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?
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.
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
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.
Uhh! Brilliant, I see that there is no cookie in the request to MVC controller and I know why. Digging into this to fix....
Is it ok that an OPTIONS
request sends to authorize
endpoint before GET
? This looks weird.
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).
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.
Okay, I found the cause.
Care to share more details? 😄
Later, I'm full in the process :)
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:
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 😄
@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);
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.