Logic for Complex IDPs

Background

A "complex" identity provider is one that asserts emails addresses it does not host (such as MySpace asserting a yahoo.com address) and most such IDPs also allow users to change the email address associated with their account. This article describes logic that a relying party can implement to deploy a user self-service mechanism for enabling users to login with such an identity provider, including the ability to connect to an existing account on the site.

The most common scenario is the following:

    1. Sara uses the email sara@yahoo.com

    2. She uses the social network MySpace and has created a MySpace account/password associated with that email address. When she first created that account a few years ago she verified for MySpace that she owned that email address by clicking on a verification link in an email that MySpace mailed to her

    3. Sara also uses the website openstore.com and has created an openstore.com account/password associated with that email address. Similarly to MySpace, she had performed an email address verification step in the past.

    4. openstore.com adds a login button for MySpace

    5. Sara visits the site, notices the button and clicks it. She is redirected to MySpace where she gives consent to share some of her account information with openstore.com, including the email address associated with her account

    6. Sara is then shown an "account linking wizard" by openstore.com that asks her to enter the old password of the openstore.com account associated with that email address to confirm that the same person does in fact control both the openstore.com and MySpace accounts

This flow would have been simpler if Sara had clicked a Yahoo login button on openstore.com because the site could have skipped step 6 since Yahoo is trusted to assert the current owner of sara@yahoo.com.

However the flow can become even more complicated. For example:

    • What if later Sara changes the email address associated with her MySpace account. How should that effect her account at openidstore.com?

    • What if openstore.com had login buttons for both Yahoo and MySpace. Another Yahoo user, such as tom@yahoo.com, might visit openstore.com for the first time and create an account using the Yahoo button. If Tom later clicks the MySpace button and has a MySpace account for tom@yahoo.com, then openstore.com cannot show an account linking wizard that asks for the password of the openstore.com account associated with that email address because there isn't one. Instead the account is only associated with Yahoo

There is more information about the edge case of account linking wizards in this best practices document.

State Diagram

By following the table of different states below, a relying party can detect whether a user is in a special edge case, and if so help guide the user through an appropriate account linking wizard. This is still quite complex, and in the future we will try to further document and clarify the suggested logic for RPs to implement.

The inputs to the table are based on 3 variables:

    • IsEmailTrusted: This variable is set to 1 (TRUE) ONLY if the identity provider that is asserting the email address also hosts that email address.

      • Example: If Yahoo asserts sara@yahoo.com then it is 1 (TRUE), but if MySpace asserts sara@yahoo.com then it is 0 (FALSE)

    • EmailSame: Identity providers assert not just a user's email address, but they also assert a machine generated identifier that represents the user's account at the IDP, such as a number of OpenID URL. The RP should take that IDP identifier and check its local user account database to see if that IDP identifier is already mapped to an account. If there is no match, then the value of this variable is NULL (NOT_FOUND). If there is a match, then the RP should compare the email asserted by the IDP to the email associated with that local account entry, If they are the same then this variable is set to 1 (TRUE) otherwise it is set to 0 (FALSE).

      • Example: In the original 6 step example this variable would be set to NULL at the start of step 6 because the MySpace identifier had not previously been sent by the RP. However if Sara visits the website again later by using the MySpace button then this variable is set to 1 (TRUE) because the MySpace identifier is already mapped to a local account whose email matches what MySpace asserts. However if later Sara changes her email on her MySpace account then this variable would be set to 0 (FALSE) because the MySpace identifier would map to a local accounts whose email no longer matches the one asserted by MySpace.

    • IsEmailInRpDb: This variable is set to 1 (TRUE) only if the RP already has a local account entry associated with the asserted email address, otherwise it is set to 0 (FALSE)

Below is a list of all the possible states based on the values of those variables.

There are then 4 possible actions for the RP to take:

    • [E] Error: Fortunately cases 3 and 7 are impossible because it would require the email to NOT match an existing account, but for their to be an existing account that matches the email asserted by the IDP. If the RP detects this scenario, then there may some corruption in its local account database that should be checked.

    • [L] Log in: This is the simplest case where the RP can just log the user into the existing local account without showing the user an account linking wizard

    • [S] Signup: This is a relatively simple case where the RP needs to help the user create a new local account on the site based on the email asserted by the IDP

    • [C] Change Email: This is a slightly tricky case where the RP needs to find the local account entry that matches the identifier associated by the IDP and then change the email address in that local account to the new value asserted by the identity provider

    • [M] Map: This is the complex case where the user needs to be shown an account linking wizard. There is more detail on this below.

Email Recycling

In addition to handling the states above, there is one other case the RP must handle. Some email providers like Yahoo will recycle email addresses. That means if a user like Sara stops using her Yahoo mail address for a few months, then Yahoo might allow another person to register that same email address. If the new owner logs into the RP, it is a good idea for the RP to detect this case and to disable or delete the old local account entry for that email address if one exists. The standard way identity providers indicate that an email is recycled is to use the exact same IDP identifier but to include a numeric fragment at the end which changes each type the account is recycled. So the RP will need to be able to lookup accounts based on the identifier WITHOUT the fragment, and then check metadata associated with the account to then confirm if the identifier WITH the fragment is a match.

Map Action

There are two states in the above table where the M (Map) action must be performed. The RP should generally store information about the pending account link in either a cookie or in the website's account database, but make sure to note that it is a pending link that has not yet been confirmed. The RP can then ask the user to prove ownership of the local account by either providing its password or by being redirected to another identity provider that is either trusted to assert that email or is already linked to the account. If the user is already logged into that other identity provider then generally that redirection can happen without the user noticing or having to do anything which makes the user experience nice.

The RP should NOT let the start the mapping process on one computer and finish it on another computer unless the RP is willing to ask the user on the 2nd computer to confirm they want to bind the new IDP to their account.

Pseudo code

The following is suggested pseudo code that the RP can implement to handle the necessary logic.

Function: callbackLogic(Email, FederatedId, IsEmailTrusted)

Inputs:

  • Email - the email address asserted in the IDP response

  • FederatedId - the identifier asserted in the IDP response

  • IsEmailTrusted - whether the email is trusted. In other words whether the IDP is the email provider.

Logic:

  • If (IsFederatedIdRecycled(FederatedIdentifier, Email, IsEmailTrusted)

    • recycle(FederatedIdentifier, Email, IsEmailTrusted)

    • return;

  • EmailSame = NULL;

  • OldEmail = NULL;

  • LocalId = Mapping::GetMappingByFederatedId(FederatedId);

  • If (LocalId != NULL)

    • OldEmail = Account::GetAccountByLocalId(LocalId);

    • If (OldEmail == Email) EmailSame = 1; else EmailSame = 0;

  • If (Account::GetAccountByEmail(Email) == NULL) IsEmailInRpDb= 0;

  • else IsEmailInRpDb= 1;

  • If (IsEmailInRpDb == 1)

    • If (IsEmailTrusted == 0 and EmailSame != 1)

      • link(FederatedId, Email, IsEmailTrusted, OldEmail);

    • Else

      • login(FederatedId, Email, IsEmailTrusted, OldEmail);

  • Else

    • If (IsEmailTrusted == 1 and EmailSame == 0)

      • changeEmail(FederatedId, Email, IsEmailTrusted, OldEmail);

    • Else

      • singup(FederatedId, Email, IsEmailTrusted);


Function: recycle(FederatedId, Email, IsEmailTrusted)

  • LocalId = Account::GetAccountByEmail(Email);

  • Account::DeleteAccount(LocalId);

  • Mapping::DeleteMappingsByLocalId(LocalId);

  • signup(FederatedId, Email, IsEmailTrusted, NULL);


Function: link(FederatedId, Email, IsEmailTrusted, OldEmail)

  • Mapping::DeleteMappingByFederatedId(FederatedId);

  • Store the mapping info to the cookie. Show the user the message that he needs to login in the old way to create the mapping. After the users logs in in the old way create the mapping in the cookie.

Function: login(FederatedId, Email, IsEmailTrusted, OldEmail)

  • LocalId = Account::GetAccountByEmail(Email);

  • If (Email != OldEmail)

    • Mapping::DeleteMappingByFederatedId(FederatedId)

    • Mapping::GetMappingsByLocalId and store those mappings in the cookie

    • Mapping::DeleteMappingsByLocalId(LocalId);

    • Mapping::CreateMapping(FederatedId, LocalId);

    • Account::SetFederated(LocalId);

    • Account::SetEmailVerified(LocalId)

  • Logs in the account with the LocalId and ask the user to reaccept the mappings in the cookie.

Function: signup(FederatedId, Email, IsEmailTrusted, OldEmail)

  • Mapping::DeleteMappingByFederatedId(FederatedId)

  • Set the IDP response in the cookie, redirect the user to the account creation page and use the attributes to prefill. After the account is created create the mapping.

Function changeEmail(FederatedId, Email, IsEmailTrusted, OldEmail)

  • LocalId = getLocalIdByEmail(OldEmail);

  • Mapping::DeleteMappingsByLocalId(LocalId);

  • Mapping::CreateMapping(FederatedId, LocalId);

  • Account::SetEmail(LocalId, Email);

  • Account::SetEmailVerified(LocalId)

  • Logs in the account with LocalId. It’d be nice to notify the user that the email has been changed.

Function: IsFederatedIdRecycled(FederatedId, Email, IsEmailTrusted)

  • If (!IsEmailTrusted)

    • Return 0;

  • LocalId = Account::GetAccountByEmail(Email);

  • If (LocalId == NULL)

    • Return 0;

  • FederatedIds = Mapping::GetMappingsByLocalId(LocalId)

  • for (Id in FederatedIds)

    • If (ID and FederatedId only difference in fragment part)

    • Return 1;

  • Return 0;

Interface for the ID mapping storage.

Function: Mapping::DeleteMappingsByLocalId(LocalId)

  • Delete all the mappings associated with LocaiId if there is any.

Function: Mapping::DeleteMappingByFederatedId(FederatedId)

  • Delete the mapping associated with FederatedId.

Function: Mapping::CreateMapping(FederatedId, LocalId)

  • Create the mapping from FederatedId to LocalId.

Function: Mapping::GetMappingByFederatedId(FederatedId)

  • Return the mapping associated with FederatedId.

Fucntion: Mapping::GetMappingsByLocalId(LocalId)

  • Return all the mapping associated with LocalId

Interface for the RP account storage.

Function: Account::SetEmail(LocalId, Emal)

  • Set the email address of the account with LocalId to Email.

Function: Account::GetAccountByEmail(Email)

  • Return the account with the given email.

Function: Account::GetAccountByLocalId(LocalId)

  • Return the account with the given LocalId.

Function: Account::SetFederated(LocalId)

  • Set the account with LocalId to be federated account and clear the password if it’s not yet a federated account.

Function: Account::DeleteAccount(LocalId)

  • Delete or deactivate the account in the table.

OPTIONAL Function: Account::SetEmailVerified(LocalId)

  • Set the email to be verified. The email verified info is not used by the federated login flow. The RP may already use that info to do some interesting stuff. e.g. send notification email.