w3c-fedid / FedCM

A privacy preserving identity exchange Web API
https://w3c-fedid.github.io/FedCM/
Other
376 stars 72 forks source link

Users may be confused after showing intent to sign in but the sign-in is failed #488

Open yi-gu opened 1 year ago

yi-gu commented 1 year ago

Currently after a user has granted permission to sign in to an RP with IdP (e.g. by clicking on the "Continue as" button on the account UI), the browser will send a request to the id_assertion_endpoint to fetch an IdentityProviderToken. If a token is issued, the browser will hand it over to the API caller and the user will continue their journey on the website.

However, the IdP could refuse to issue an token. To name a few:

  1. the account's admin has blocked federated sign-in on that RP (or even globally)
  2. the RP isn't properly configured on the IdP side for federation
  3. signing in with the account needs extra work. e.g. the account belongs to a minor and parental control must be involved to proceed

Today when the IdP refuses to issue an token, FedCM API will reject the promise and fail the request silently.

From user's perspective, after they have granted permission to sign in, they may be confused because they are neither signed in nor informed about the failure. It would be helpful if there's a way for keep users in the loop for better user experience.

philsmart commented 1 year ago

Agreed. In addition, the RP might want to force an authentication, or the authentication level of assurance is not sufficient (both of which might trigger a re-authentication).

yi-gu commented 1 year ago

Problem Statement

The FedCM API currently lacks a dedicated API surface that enables an IdP to return information about what went wrong and where to go from there to the RP or the browser. The only option is to overload the IdentityCredential.token parameter and rely on the API caller (IdP SDK, RP, or 3P library) to decode and display error messages. Unfortunately, this creates two problems:

  1. IdPs have no guarantees that their errors will be handled as they expect by their callers.
  2. RPs have to special-case each IdP to parse and display the IdP-specific error.

A dedicated API surface would address these problems by providing a standard way for IdPs to return error information to RPs and/or users. This would allow IdPs to be confident that their errors would be handled correctly, and it would eliminate the need for RPs to special-case each IdP.

This is a proposal to introduce an API to inform the user agent about errors and handle them consistently across IdPs.

Proposal

In this proposal, to guarantee to IdPs that errors are shown to inform users that their sign-in attempt has failed, the browser can support displaying error dialogs. To start with, a general purpose error dialog can cover a lot of cases and guarantee that users are notified in a consistent manner across RPs and IdPs. For example:

While the general purpose error dialog is better than the status quo, there are known error cases where a more purpose-specific dialog could give users more specific information. Therefore, it would be better if the browser can show some specific error dialogs to give the user more context. For example:

The enumeration of specific errors could be insufficient and it’s possible that users would want to learn more about the error and potentially some next steps to fix the error. Therefore the browser can provide some affordance to achieve that, e.g. via a new “More details” button:

When the user clicks the “More details” button, the browser can open a pop-up window to show a IdP controlled page with detailed information about the error.

Proposed API

In this proposal, to support the error dialogs described above, the browser introduces error codes and error urls that can be used whenever the IdP cannot produce a token.

The IdP HTTP API

In the id_assertion_endpoint, currently the IdP can return a token to the browser if it can be issued upon request. In this proposal, in case a token cannot be issued, the IdP can return an “error” response, which has two new fields:

  1. code
  2. url

code

OPTIONAL. The IdP can use the “code” field to specify one of the known errors from [invalid_request,unauthorized_client, access_denied, server_error and temporarily_unavailable]. e.g.

// id_assertion_endpoint response
{
  "error" : {
     "code": "access_denied"
  }
}

The list is based on the OAuth 2.0 error response table to cover common errors across IdPs.

If a valid code is included in the response of the token request, the browser can trigger a native UI with proper strings to notify users that their sign-in attempt was failed due to the corresponding error. See considered alternatives for other options to show errors.

If an error is provided but the code is not on the pre-defined list, we propose to pass the string to the API caller and show the uncustomized generic error UI above.

url

OPTIONAL. A URL identifying a human-readable web page with information about the error, used to provide additional information about the error to users. The uri must be of the same-origin as the IdP configURL.

// id_assertion_endpoint response
{
  "error": {
    "url" : "https://idp.example/error?type=foo"
  }
}

This field is useful to users because browsers cannot provide rich error messages on a native UI. e.g. links for next steps, customer service contact information etc.. If a user wants to learn more about the error details and how to fix it, they could visit the provided page from the browser UI for more details.

[!NOTE]

  • Both code and url are optional in case of token request failures. The browser should provide a fallback UI to keep users aware if both are missing.
    • The new browser error UI may conflict with existing error UI rendered by the RP if any. That said, the risk is extremely low based on how we see current IdPs implementing FedCM.
    • We believe that the browser should be opinionated to render a fallback error UI to make sure that users are informed when their sign-in attempt has failed.
  • It’s possible that there’s no response returned from the id_assertion_endpoint in which case the browser cannot be sure that an error occurred. Without a time-out mechanism, the browser won’t show any UI in this case.

The Client JS API

To give more context to the API caller such that they could provide more sign-in options to users, the browser can pass over the error (code and url) by failing the promise:

try {
  await avigator.credentials.get({
    identity: {
      // ...
    }
  });
} catch (e) {
  // Acquire {code, url} from an `IdentityException`
}

Privacy Considerations

The new Error Response API is only invoked post user permission to allow RP/IdP communication. e.g. the user is aware that they are “signing in to RP with IdP”. In addition, the IdP has already possessed both the RP information and the user cookie from the id_assertion_endpoint. Therefore we believe that there’s no change in the privacy threat model with the new API.

Security Considerations

When the user clicks the “More details” button, we open a popup (same UI and web platform properties as what one would get with window.open(url,””,”popup,noopener,noreferrer”)) that loads the error.url. Note that no communication between the website and this pop-up is allowed (e.g. no postMessage, no window.opener).

Phising

The primary threat is a phishing attack, where the attacker (who controls - or colludes with - both the RP as well as the IdP) can provide a fake “error.url” (that impersonates a real IdP) for the browser to display via the pop-up window, and trick the user to enter their (real IdP) password there. e.g. upon user clicking the “More details” button, the browser will open a page that looks like a genuine “Sign in to IdP” website. Then the user “may” be tricked into entering their IdP credentials on that website.

Because of that, the pop-up window has the following properties:

As such, the attack has to rely on the fact that the user misses the displayed origin/site in both steps and on safe browsing not knowing about the site.

In addition to that, the attacker may already be able to do this by opening a phishing pop-up window and there’s no browser UI involved compared to this proposal.

Considered Alternatives

API caller handles all errors

The browser could delegate handling the errors to the API callers (RP or IdP SDK or FedCM library owned neither by RP nor IdP).

When the browser receives the errors from the token request, it rejects the promise with the errors. Once the API caller receives the error, the caller can inform users accordingly. e.g. the caller can render an iframe to show proper information to users.

Pros

Cons

Appendix

OAuth 2.0 Error Response Error Description Included in this proposal
invalid_request The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Yes
unauthorized_client The client is not authorized to request an authorization code using this method. Yes
access_denied The resource owner or authorization server denied the request. Yes
server_error The authorization server encountered an unexpected condition that prevented it from fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client via an HTTP redirect.) Yes
temporarily_unavailable The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server. (This error code is needed because a 503 Service Unavailable HTTP status code cannot be returned to the client via an HTTP redirect.) Yes
unsupported_response_type The authorization server does not support obtaining an authorization code using this method. No. FedCM is authentication focused at the moment and we can add this type in the future.
invalid_scope The requested scope is invalid, unknown, or malformed. No. FedCM is authentication focused at the moment and we can add this type in the future.
antosart commented 1 year ago

Since the error is anyway exposed to the RP ("The Client JS API"), wouldn't it make more sense to let the RP take care of showing error messages (which it could do without a popup), and let this just be a proposal about exposing the error type to the RP?

yi-gu commented 1 year ago

Having the API caller rendering the UI does have benefits as mentioned in the considered alternatives section. That said, there's no guarantee that the API callers would do it and the UX would be bad if they don't. In addition, when it comes to multi-IdPs, it would be challenging for them to implement a proper UI affordance to show error messages.

achimschloss commented 1 year ago

As noted in the call:

  1. The IDP primarily also needs to be able to simply directly interact with the user in case of an error. This can be achieved using _continueon and then being able to do something like IdentityProvider.reject('{error:.....}')
  2. In case of an additional browser provided UI as an option (a discussed this might be helpful on mobile compared to a custom tab), why not simply let the IDP provide error and _errordescription and render it? Don't see any additional benefit to move error details handling into the browser (matching certain errors etc.) and also letting custom text be provided
yi-gu commented 1 year ago

Thanks Achim for following up!

This can be achieved using continue_on and then being able to do something like IdentityProvider.reject('{error:.....}')

This is indeed a possible solution and we started with it when developing this feature. It's worth noting that this may lead to suboptimal UX. In Chrome, the error page will be displayed in a pop-up window on desktop and in a CCT (ChromeCustomTab) on Android. For UI that's as loud as a pop-up or CCT, it should provide as much value as possible. In the AuthZ API (name TBD), user can grant calendar access to finish the sign-in flow; in IdPLoginStatus API, user can sign in to IdP via the pop-up. However, for the error page, it's unlikely to be call-to-action. Rather, it's likely to just provide some information about the error itself. Therefore we believe that it should be gated by a quieter UI and only be displayed if user wants to "learn more". Ideally the browser UI can provide meaningful strings such that the user doesn't need to open the loud UI.

why not simply let the IDP provide error and error_description and render it?

Unfortunately IdP doesn't have a reliable way to do it. If FedCM API is called by RP directly (or via 3P library) without using IdP SDK, then the error may not be (properly) handled. This could be concerning to IdPs. Even if an IdP SDK is used, when it comes to multiple IdP, it's possible that different IdPs handle errors differently which could cause inconsistent UX on the same RP.

achimschloss commented 1 year ago

... Ideally the browser UI can provide meaningful strings such that the user doesn't need to open the loud UI.

I'm good with having both options - both paths should be generally possible though.

Unfortunately IdP doesn't have a reliable way to do it. If FedCM API is called by RP directly (or via 3P library) without using IdP SDK, then the error may not be (properly) handled. This could be concerning to IdPs. Even if an IdP SDK is used, when it comes to multiple IdP, it's possible that different IdPs handle errors differently which could cause inconsistent UX on the same RP.

That's not what I meant - I was referring to this:

If a valid code is included in the response of the token request, the browser can trigger a native UI with proper strings to notify users that their sign-in attempt was failed due to the corresponding error. See considered alternatives for other options to show errors.

If an error is provided but the code is not on the pre-defined list, we propose to pass the string to the API caller and show the uncustomized generic error UI above.

Given this error UI is shown post consent why not simply define

// id_assertion_endpoint response
{
  "error": {
     "error_code":   "OIDC or SAML standard".        --- not used in UI, but for SDKs and RPs to have aligned error handling between FedCM based on Redirect flows.
     "error_short" : "Short description",            --- Headline for UI above - if not given default error text
     "error_description": "Long Descritton".         --- Detailed Text  for UI - if not given default error text
     "url": "https://"
  }
}

IDP can use the strings they'd want from any standard they want to use, if this a IDP controlled UX

This should also be passed back to the RP in case they'd want to react to this.

yi-gu commented 1 year ago

Hi Achim, allowing customized error description occurred to us as well. We should have mentioned it in the "Considered Alternatives" section. Sorry about that!

The question is: whether / how much should the browser allow random strings on its native UI. The conclusion we have at least in Chrome is that it should be "little to none" from security's perspective. We shared the same principle when designing the RP context API where we only allow 4 predefined strings (sign in, sign out, user, continue) instead of accepting customized ones.

From extensibility's perspective, we allow random error string in code so if an IdP prefers to show customized error UI with their SDK (or with proper developer documentation such that RP can handle the error properly by themselves), they could send "code":"foo" back to the browser and and the browser will hand it over to the API caller. The API caller can then parse the error and handle it accordingly. The only difference is that a native browser UI will show up with a generic error string (see mock above). We believe that A browser UI in this case is necessary because there's no guarantee that the API caller will handle "code":"foo" (properly) therefore the browser should close the authentication flow by itself.

WDYT?

achimschloss commented 1 year ago

We shared the same principle when designing the RP context API where we only allow 4 predefined strings (sign in, sign out, user, continue) instead of accepting customized ones.

From a thread model perspective this seems a different case given this is shown in the account selector during the main mediation. Whereas the error is an alternative to passing a token to the RP post user interaction.

From extensibility's perspective, we allow random error string in code so if an IdP prefers to show customized error UI with their SDK (or with proper developer documentation such that RP can handle the error properly by themselves), they could send "code":"foo" back to the browser and and the browser will hand it over to the API caller. The API caller can then parse the error and handle it accordingly. The only difference is that a native browser UI will show up with a generic error string (see mock above). We believe that A browser UI in this case is necessary because there's no guarantee that the API caller will handle "code":"foo" (properly) therefore the browser should close the authentication flow by itself.

If the generic error UI is mandatory than no IDP or RP would mind showing a second error honestly, the error JSON passed to the RP is then likely only used for failure analysis. Its an additional complexity for browsers and IDP (supported error codes + no ability for a bespoke description (which is left open to the IDP in OAuth/OpenID) - not a deal breaker but makes continue_on even more pressing as a different option.

npm1 commented 1 year ago

@asr-enid can you clarify what is your preferred behavior? In particular, how should errors be handled if the IDP id assertion endpoint has some network error?

yi-gu commented 1 year ago

If the generic error UI is mandatory than no IDP or RP would mind showing a second error honestly

Alternatively the IdP can provide an error URL without a predefined code. The browser can then construct the UI with generic error string + a "More details" button. If the user is interested, they can definitely learn about the detailed error from the pop-up window. If they are not interested, we don't need to show the loud UI which also respects the user's choice (not interested).

achimschloss commented 1 year ago

@asr-enid can you clarify what is your preferred behavior? In particular, how should errors be handled if the IDP id assertion endpoint has some network error?

In this case a generic error UX seems sensible as the browser won't have any information available (an error JSON) and the browser mediated flow just fails technically. Would something be passed back to the caller in this case?

Overall the approach suggested is doable I guess, but as noted managing the supported codes to avoid having the IDP provide the content directly can become a burden. I made the case for continue_on already. If these things are optional / available for the IDP they can decide which path to follow given the scenario at hand

achimschloss commented 1 year ago

continue_on would additionally adress cases where the login needs extra work or re-authentication as noted by @philsmart as the error can be avoided if the user is able to take the necessary action.

yi-gu commented 1 year ago

continue_on would additionally adress cases where the login needs extra work or re-authentication as noted by @philsmart as the error can be avoided if the user is able to take the necessary action.

continue_on is definitely useful (e.g. handle parental control as mentioned in this issue) and it's not mutually exclusive with this proposal :)

achimschloss commented 1 year ago

continue_on is definitely useful (e.g. handle parental control as mentioned in this issue) and it's not mutually exclusive with this proposal :)

Agreed

achimschloss commented 1 year ago

@samuelgoto seems this should be a proposal under https://github.com/fedidcg/FedCM/tree/main/proposals ?