Closed paolo8417 closed 1 year ago
Hey,
Thanks for sponsoring the project π
At this point, the external site load an iframe calling a special anonymous MVC Frontend controller containing this token (e.g. /Account/Token/{token}). As your opinion, what is the best solution to achieve this?
With browser vendors banning "third-party cookies", anything involving cross-domains iframes and cookies is not going to work well, so if the external site is on a different domain, flowing tokens manually is pretty much your best option.
That said, instead of appending it to the request path, I'd use the standard access_token
query string parameter (and assuming you're using the OpenIddict validation handler with the native ASP.NET Core integration, it's extracted for you without any custom code).
Alternatively, depending on the scenario, you could also provide your partner an HTTP API they could integrate with instead of relying on an iframe.
I wish I had a better option to offer, but anything involving iframes drastically reduces the options these days π
Hi,
thank you for your reply.
The token I'm talking about, it is not the OpendIddict _accesstoken, but a custom token that the Identity Server create to permit the external site to open the MVC Frontend user Resources page with a direct link.
The desired flow is as follows:
The point of my request is to understand if there is, as your opinion, a way to get an MVC Frontend authentication cookie (with _authorizationcode grant), bypassing the user login but using the special token.
I mean, for example:
I hope to be more precise and clear.
Thank you
Thanks for the additional details.
The point of my request is to understand if there is, as your opinion, a way to get an MVC Frontend authentication cookie (with _authorizationcode grant), bypassing the user login but using the special token.
The option you have in mind is technically possible: after all, OpenIddict doesn't care about how your users are authenticated (all it needs is a ClaimsPrincipal
), so you could definitely create an ASP.NET Core IAuthenticationHandler
(or just some custom logic directly in your authorization controller) that would extract your custom token from the request path and create a ClaimsPrincipal
based on it.
But as I said, many browser vendors now block cross-site cookies for "privacy protection", so unless your partner is willing to force their users to disable this option in their browser settings (which is a terrible user experience), your page loaded via the iframe won't see any existing authentication cookie, which would require doing this unusual code flow dance with your special custom token in the authorization request parameters every time the iframe would be loaded from a different domain.
Would I recommend doing that? No. The proper way here would consist in inviting your partner to act as a classical code flow client to request a one-time authorization from your server and get back a refresh token bound to the user who allowed the demand in return, a token that they could use to get new access tokens (via grant_type=refresh_token
) that could be attached to the query string of the page loaded via the iframe (which would basically be considered a resource
like a more classical API).
Hope it's clear π
Would I recommend doing that? No. The proper way here would consist in inviting your partner to act as a classical code flow client to request a one-time authorization from your server and get back a refresh token bound to the user who allowed the demand in return, a token that they could use to get new access tokens (via grant_type=refresh_token) that could be attached to the query string of the page loaded via the iframe (which would basically be considered a resource like a more classical API).
If I understood correctly, you suggest this flow:
By this way, the MVC Frontend receive the access token on the query string. Now, how can I create the authentication cookie with this access token?
If I understood correctly, you suggest this flow:
Yup!
Now, how can I create the authentication cookie with this access token?
Why would you want to do that?
Why would you want to do that?
To view the MVC Frontend user cookie authenticated page on the iframe.
The ultimate goal is to allow the users to enjoy the entire MVC website as if they had logged in with a username and password.
The ultimate goal is to allow the users to enjoy the entire MVC website as if they had logged in with a username and password.
Your initial post seemed to imply it was only a single page, not an entire site π
On MVC Frontend there is a page where an authenticated user can see a list of personal resources. (e.g. /Account/Resources).
I wouldn't recommend at all using iframes to do that as you'll hit many security blockers that will require reducing the security of your application drastically (e.g you'll need to use SameSite=None
for all your cookies and if you have any form, you'll also need to disable all frame-busting mechanisms, including the X-Frame-Options
header added for you by the ASP.NET Core antiforgery stack).
If you're absolutely sure you don't have any better option, I can think of two ways of achieving what you want:
Copying the access token from the query string in all the links and forms of your site so it's persisted across navigated pages (it's basically the equivalent of the cookieless mode in good old ASP.NET 4.x apps). Extreme caution would be necessary to ensure it doesn't leak to third-party websites (and you can't prevent a naΓ―ve user from copying a link with the token appended and sending it to a different person).
Implementing the dance you suggested, with the limitations it has.
(IMHO, iframes offer a terrible user experience in 2023: they should never be used, and many websites will use frame-busting techniques to deliberately block that)
Your initial post seemed to imply it was only a single page, not an entire site π
I apologize for not being precise.
As your opinion, If the external site attach the user _refreshtoken to the query string, can I obtain the authentication cookie in the MVC Frontend using in some way the _refreshtoken grant to Identity Server?
I apologize for not being precise.
No worries π
As your opinion, If the external site attach the user refresh_token to the query string, can I obtain the authentication cookie in the MVC Frontend using in some way the refresh_token grant to Identity Server?
Nah, at least not without adding terrible hacks (flowing the access token in the query string across multiple pages is bad, but doing that with a long-term token like a refresh token is a total abomination π).
If you really want a cookie-based approach, the option you suggested here is pretty much the only practical way to implement that:
I mean, for example:
1. _MVC Frontend_ from the _/Account/Token/{**token**}_ controller pass the **token** on _Challenge_ authorize query parameters with redirect uri _Resources_ page. 2. Identity Server, from the _AuthorizationController /connect/authorize_, if it detects this query parameter, returns _SignInResult_ for the user retrieved by the **token** instead the _Challenge_ (this **token** is stored on _Identity Server_ and it is associated with user). 3. _MVC Frontend_ open user _Resources_ page (from redirect uri) with authentication cookie.
If you wanted to remove the custom token from the equation, you could adopt a variant of this approach by using the standard id_token_hint
authorization request parameter:
id_token
in the token response (grant_type=refresh_token
can be used too).id_token_hint
attached. If you use the OpenIddict client, it should be as easy as:var properties = new AuthenticationProperties(new Dictionary<string, string>
{
// Note: when only one client is registered in the client options,
// setting the issuer property is not required and can be omitted.
[OpenIddictClientAspNetCoreConstants.Properties.Issuer] = issuer,
[OpenIddictClientAspNetCoreConstants.Properties.IdentityTokenHint] = "the identity token resolved from the URI"
})
{
// Only allow local return URLs to prevent open redirect attacks.
RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"
};
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
Your authorization endpoint action (in your identity provider project) would resolve the identity contained in id_token_hint
using var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)
and would build an identity from it without needing an authentication cookie. I'd strongly encourage you to check the result.Properties.IssuedUtc
/result.Properties.ExpiresUtc
properties to manually validate that the identity token was generated very very very recently (since there's no authentication cookie, the user can't control the lifetime of the session, so it's critical to ensure the user logged in quite recently).
The rest of the flow would be identical to a classical code flow.
It's important to note that both your approach and the variant I described are prone to session fixation attacks: a malicious user could extract a custom token/identity token created for his session and use it to build an iframe URI containing this token: if a victim visited a website containing this iframe URI, he would be transparently logged under the attacker's account without necessarily realizing it. If there's sensitive data that can submitted on any page of your embedded site, the victim may be lured into sending sensitive information that would be attached to the attacker's account. Depending on your scenario, this may or may not be a problem.
(again, consider avoiding iframes π )
Hi, just to avoid misunderstandings, here is a sample code of my option:
// MVC Frontend AccountController
[AllowAnonymous]
[HttpGet("[controller]/Token/{token}")]
public IActionResult Token([FromRoute] string token)
{
// This is the endpoint called by external site
var properties = new AuthenticationProperties
{
RedirectUri = string.Format("/Account/Token/{0}/Validate",token)
};
properties.Parameters["token"] = token;
// Force logout
return SignOut(properties,CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme);
}
[AllowAnonymous]
[HttpGet("[controller]/Token/{token}/Validate")]
public IActionResult ValidateToken([FromRoute] string token)
{
// Make sure that only unauthenticated users can go ahead
if ( User.Identity.IsAuthenticated )
{
// Error or signout
return SignOut(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme);
}
var properties = new AuthenticationProperties
{
RedirectUri = "/Resources"
};
properties.Parameters["token"] = token;
return Challenge(properties,OpenIdConnectDefaults.AuthenticationScheme);
}
// Identity Server AuthorizationController
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
var token = (string) request["token"];
ApplicationUser user = null;
// Retrieve the user principal stored in the authentication cookie.
// If it can't be extracted, redirect the user to the login page.
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
// The only difference between the standard flow and my option when I retrive the user by custom token
if ( !string.IsNullOrWhiteSpace(token))
{
if ( result is not null && result.Succeeded )
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.AccessDenied,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Access denied."
}));
}
// Retrieve the profile of the logged in user, like normal code
user = await RetrieveUserFromTokenAsync(token) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
}
else
{
// openiddict code
.....
// Retrieve the profile of the logged in user.
user = await _userManager.GetUserAsync(result.Principal) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
}
// Retrieve the application details from the database.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
....
It's important to note that both your approach and the variant I described are prone to session fixation attacks: a malicious user could extract a custom token/identity token created for his session and use it to build an iframe URI containing this token: if a victim visited a website containing this iframe URI, he would be transparently logged under the attacker's account without necessarily realizing it. If there's sensitive data that can submitted on any page of your embedded site, the victim may be lured into sending sensitive information that would be attached to the attacker's account. Depending on your scenario, this may or may not be a problem.
Thank you for your advice. There are no areas where the user can enter sensitive data, but as a form of protection I will set a life time for the custom token.
Looks good ππ» (as you said, except the custom token part, it's indeed pretty standard π)
Doing some housecleaning, but feel free to reopen if you need additional details π
Confirm you've already contributed to this project or that you sponsor it
Version
4.x
Question
Hi,
I've an enterprise .Net 6 solution which consists of:
The MVC Frontend use the authorization code grant to authenticate the users by cookie.
On MVC Frontend there is a page where an authenticated user can see a list of personal resources. (e.g. /Account/Resources).
I need to integrate the MVC Frontend in an external site.
In particular, I need to authorize the external site to embed the MVC Frontend page on iframe and view a user resources page without user manual authentication (digit username e password). This because the user is already auhtenticated on the external site by another identity provider, but is logically the same user of our Identity server.
The external site (before to open the iframe) get a special token from Identity server (with client credentials grant).
This token is stored on the Identity Server and it is related to user.
At this point, the external site load an iframe calling a special anonymous MVC Frontend controller containing this token (e.g. /Account/Token/{token}).
As your opinion, what is the best solution to achieve this?