AzureAD / microsoft-identity-web

Helps creating protected web apps and web APIs with Microsoft identity platform and Azure AD B2C
MIT License
682 stars 215 forks source link

[Bug] Silent Authentication in iframe redirects parent window on error #778

Closed keggster101020 closed 3 years ago

keggster101020 commented 3 years ago

Which version of Microsoft Identity Web are you using? Using AAD v2 and ASP.NET Core 3.1

Nuget packages and versions:

Microsoft.AspNetCore.Authentication.OpenIdConnect Version="3.1.10"
Microsoft.Identity.Client Version="4.22.0"
Microsoft.IdentityModel.JsonWebTokens Version="6.8.0"
Microsoft.IdentityModel.Protocols.OpenIdConnect Version="6.8.0"

Where is the issue?

Is this a new or an existing app? New experience and testing experience

Repro I'm calling an auth API on my service by pointing an iframe to an API on my service which calls Challenge() with prompt=none Whenever there is a remote login failure, the identity server responds with a form post and the target attribute is set to _top which is causing the redirect to bust out of the iframe.

This is separate from the successful login flow, which remains contained in the iframe.

Expected behavior All redirects are contained within the iframe.

Actual behavior Identity server errors are busting out of the iframe and are redirecting the parent window.

Possible solution Alter the response form post target away from _top

Additional context / logs / screenshots Add any other context about the problem here, such as logs and screenshots.

jeffreyrivor commented 3 years ago

I'm working on the same service with @keggster101020 and want to provide a screenshot of the form in the response when we make the prompt=none request in an <iframe>.

image

Since we specified prompt=none and on the request in an <iframe>, we were hoping we can silently handle the error callback in the frame instead of navigating the top window.

jmprieur commented 3 years ago

@keggster101020, @jeffreyrivor. I'm a bit confused. Is this an issue with Microsoft.Identity.Web ? which kind of application are you building? a web app or a single page application?

jeffreyrivor commented 3 years ago

@jmprieur I don't think it is an issue with Microsoft.Identity.Web, but I'm also not sure where to send these kinds of issues for AzureAD. We're making an ASP.NET Core web app that uses the OIDC and OAuth2 flow for id_token + code with a POST callback. We want to passively authenticate users using an iframe with a challenge to AAD with prompt=none specified, but the expected error that is returned when there is no user logged into AAD breaks out of the iframe.

jennyf19 commented 3 years ago

@jeffreyrivor can you send no prompt at all, so no value for prompt?

jeffreyrivor commented 3 years ago

@jennyf19 I can't request without prompt=none because that would generate a response from AzureAD with X-Frame-Options: DENY since login prompts are not allowed in iframes.

@jmprieur I believe this is a regression between AADv1 and Microsoft Identity Platform (AADv2) because I just looked at a v1 app and the form response for prompt=none does not force a target="_top".

image

hpsin commented 3 years ago

@jeffreyrivor - are you running this within a sandboxed iframe by chance? We've filed an incident internally to investigate, as you're correct to call out that this should not be occurring.

jennyf19 commented 3 years ago

Thanks @hpsin ...

@jeffreyrivor going to close, as this is not related to identity web, but will update here when we have information about the incident. Thanks for letting us know.

cc: @jmprieur

jeffreyrivor commented 3 years ago

@hpsin We were planning to add the sandbox parameters to the iframe as a workaround, but I figured that it was worth bringing up since v1 apps don't have the same problem. Do you have a suggestion on the set of values that would need to be set if we have to go that route?

hpsin commented 3 years ago

Unfortunately the error still won't make it back to you even with sandboxing, due to our incorrect navigationin respone to interaction_required errors. We wanted to make sure that sandboxing wasn't triggering this. Sandboxing won't really help here, I'm afraid - it'll preserve your app, but you'll need some way to watchdog the iframe to ensure that it hasn't gotten lodged on this error.

iyerusad commented 3 years ago

Seeing similar behavior in SPA app that is using iFrame to refresh authentication token. AAD login page seems to want to bust out and redirect the main app window in some browsers.

Implementation

let iframe = document.createElement("iframe");
iframe.id = "authIFrame";
iframe.style =
  "width: 0; height: 0; border: 0; border: none; position: absolute; visibility: hidden;";
iframe.src = `/.auth/login/aad?prompt=none&domain_hint=<fakeappdomain.com>`;
document.body.appendChild(iframe);

Behavior:

Chrome 90: Properly stays within iframe, successfully refreshing token Firefox 87: AAD login page busts out of iFrame, redirecting parent window. Safari 14.1: Break/hangs with

Unsafe JavaScript attempt to initiate navigation for frame with URL 'https://fakeappdomain.azurewebsites.net' from frame with URL 'https://login.microsoftonline.com/common/oauth2/authorize?response_type=id_token&redirect_uri=https%3A%2F%2Ffakeappdomain.azurewebsites.net%2F.auth%2Flogin%2Faad%2Fcallback&client_id=fake-fake-fake-fake-fake&scope=openid+profile+email&response_mode=form_post&domain_hint=fakeappdomain.com&prompt=none&nonce=blahblahfake&state=redir%3D'. The frame attempting navigation of the top-level window is cross-origin or untrusted and the user has never interacted with the frame. image

iyerusad commented 3 years ago

Troubleshooting further.

Tweaking implementation:

Results:

try/catch was not able to catch the Unsafe Javascript error with the iframe. Didn't think it would but if someone knows how to do it would be helpful for handling the iframe errors (now and in the future).

Chrome 90: Works properly, token refreshed (it worked previously as well)

Firefox 88: With sandbox parameters, busting out of iframe contained, BUT token not refreshed. An error (Uncaught DOMException: The operation is insecure.) was logged in the console pointing to code line 85 of https://login.microsoftonline.com/common/oauth2/authorize

!function(){var e=window,o=e.document,i=e.$Config||{};if(e.self===e.top){o&&o.body&&(o.body.style.display="block")}else if(!i.allowFrame){var s=e.self.location.href,l=s.indexOf("#"),n=-1!==l,t=s.indexOf("?"),f=n?l:s.length,d=-1===t||n&&t>l?"?":"&";s=s.substr(0,f)+d+"iframe-request-id="+i.sessionId+s.substr(f),e.top.location=s}}();

Grabbing the URL from the message and opening it in a new firefox tab -> "You've successfully signed in". Odd because I was expecting an error (as below in safari) - if its working why is it trying to navigate parent window....

Safari 14.01: Breaking but in different manner

  1. Error Unsafe JavaScript attempt to initiate navigation[...] is logged, but now ends with The frame attempting navigation of the top-level window is sandboxed, but the 'allow-top-navigation' flag is not set.. To attempt handling the error I wrapped the iframe creation in try/catch block but that didn't work.
  2. Navigating to the URL provided in the console sanitized: https://login.microsoftonline.com/common/oauth2/authorize?response_type=id_token&redirect_uri=https%3A%2F%2Ffakeapp.azurewebsites.net%2F.auth%2Flogin%2Faad%2Fcallback&client_id=fake&scope=openid+profile+email&response_mode=form_post&domain_hint=fakedomain.com&prompt=none&nonce=blahblahfake&state=redir%3D

Resulted in an error that doesn't make sense:

Request Id: b6791602-aa28-snipped Correlation Id: afa4927d-3bbc-snipped Timestamp: 2021-04-30T00:06:11Z Message: AADSTS50059: No tenant-identifying information found in either the request or implied by any provided credentials.

Some odd conclusions:

  1. Chrome works - don't know why and maybe/probably unsafe to rely on should iframe behavior in later Chrome tightened.
  2. Firefox no longer busted out of iframe but logs error. Launching URL from the error results in "You've successfully signed in" AND token refreshed in both tabs. This is unexpected - my understanding was the reason this whole issue is happening is AAD Login page hits and error and needs to redirect the parent window so you can see the error - when I manually did that it successfully refreshed the token.
  3. Safari is behaving more logically: AAD login iframe (presumably) encounters error, I am able to produce an error navigating to that URL. Now why this error (tenant-identifying information found) is being thrown is a different question - there is some talk of Safari discarding cookies in an iframe, which may explain the why AAD login page seems to be telling me that it doesn't know which tenant because it doesn't have a cookie to refresh. Should the iframe src url provide more than just a domain hint?

What I'm ultimately trying to do is refresh the token/session of an SPA hosted behind Azure Web App Authentication (also sometimes called "EasyAuth"). Initial session launches fine, but once the token expires (~1 hour, 5 minutes in) app starts throwing 401s.

iyerusad commented 3 years ago

Ugly Workaround:

  1. Ensure app state is stored. Add mechanism to reload/rehydrate app state (e.g querystring restoreState=true)

    • EDIT: Query string proved to be problematic when I appending it to the post_login_redirect_url param (maybe/probably a URL encoding issue). I ended up instead setting a flag in localStorage and resulting redirect ended up being: window.location.replace("/.auth/login/aad?post_login_redirect_url=/")
  2. Check browser:

    • If Chromium based, used hidden iframe method (works silently).
    • If not, call AAD login page with post_login_redirect_url parameter that should redirect on login: /.auth/login/aad?post_login_redirect_url=/
  3. App should now successfully refresh token in all cases.

The experience is clearly nicer for chromium based browser, bit more ugly for iOS users but hey, SPA PWA should function now. Not particularly proud of it but something something "perfect enemy of good". It would be nice if cross browser implementation of silent updating of token could be had in this scenario.

refreshAuthToken() {
    //Chrome based browsers work with silent iFrame based token reAuth
    if (this.browserChromium()) {
      //Remove existing iframe (if exists) #trick to not fill up history/back button
      let existingFrame = document.getElementById("authIFrame");
      if (existingFrame) {
        existingFrame.remove();
      }

      //Inject iFrame that will call endpoint to refresh token/cookie
      console.log("Refreshing auth token (quietly)...");
      let iframe = document.createElement("iframe");
      iframe.id = "authIFrame";
      iframe.style =
        "width: 0; height: 0; border: 0; border: none; position: absolute; visibility: hidden;";
      iframe.src = `/.auth/login/aad?prompt=none&domain_hint=fakedomain.com`;
      document.body.appendChild(iframe);

      new Promise(r => setTimeout(r, 2000)); //Hacky method of "waiting" for iframe to finish
    } else {
      console.log("Refreshing auth token (via page reload)...");
      window.location.replace("/.auth/login/aad?post_login_redirect_url=/");
    }
}
hpsin commented 3 years ago

Update here - this is an unfixed bug in the server side, that is still being worked on. All apologies, and we'll update here when it's fixed.