aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.41k stars 2.11k forks source link

Add support for idp_identifier query parameter when going through OAuth #10226

Open jaredready opened 2 years ago

jaredready commented 2 years ago

Is this related to a new or existing framework?

React

Is this related to a new or existing API?

Authentication

Is this related to another service?

No response

Describe the feature you'd like to request

Cognito User Pools federated identities supports using identifiers (typically a domain name) to correctly figure out what IdP a user should be authenticating with based on their email address. This is described in the Cognito documentation here.

You can also choose identifiers for your SAML providers. An identifier uniquely resolves to an identity provider (IdP) associated with your user pool. Typically, each identifier corresponds to an organization domain that the SAML IdP represents. For a multi-tenant app that multiple organizations share, you can use identifiers to redirect users to the correct IdP. Because the same organization can own multiple domains, you can provide multiple identifiers. To sign in your users with an identifier, direct their sessions to the Authorize endpoint for your app client with an idp_identifier parameter.

Following the link, you can see that the /oauth2/authorize endpoint supports an optional idp_identifier query parameter for providing this information.

I would like to be able to use the Auth.federatedSignIn function to be able to utilize this functionality.

Currently Amplify applications utilizing multiple identify providers need to jump through some awkward hoops to get their authentication code working correctly, but Cognito already supports the required functionality.

Describe the solution you'd like

Currently the Auth package provides a federatedSignIn function that has a required provider. I would like to see this function's input type enhanced to something like the following.

export declare type FederatedSignInOptions = {
    provider?: CognitoHostedUIIdentityProvider;
    idpIdentifier?: string;
    customState?: string;
};

Maybe something a little smarter to require either provider or idpIdentifier; my TypeScript isn't that good.

This would allow one to do something like,

Auth.federatedSignIn({ idpIdentifier: "example.com" });

and have the user be directed to their correct IdP to authenticate.

Describe alternatives you've considered

I've tried to see if it would be possible to abuse the current behavior to make this work, but you can see here that there's currently no way to provide additional query parameters to make this request work.

Additional context

The documentation here has a handy graphical overview of the workflow.

Is this something that you'd be interested in working on?

siman4457 commented 2 years ago

I was looking to add exactly this to my Amplify application since I have a multi-tenant app that multiple organizations share. I'd like to see this added to the federatedSignIn function as well.

The current workaround I'm envisioning for this is to create a mapping between the identifier (ex: company.com) and the provider name in a DB.

ncarvajalc commented 1 year ago

Context

Even though there is no official documentation for this feature, it is possible without major workarounds. I am currently implementing federated sign in using AzureAD as my third party provider using OpenID (It is also possible using SAML).

Notice that the provider argument is the identity_provider query parameter that you mentioned in the Authorize endpoint API, so there is no need to create new arguments.

const queryString = Object.entries({
            redirect_uri: redirectSignIn,
            response_type: responseType,
            client_id: clientId,
            identity_provider: provider, // This is already implemented in
                                            Amplify
            scope: scopesString,
            state,
            ...(responseType === 'code' ? { code_challenge } : {}),
            ...(responseType === 'code' ? { code_challenge_method } : {}),
        })
            .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
            .join('&');

        const URL = `https://${domain}/oauth2/authorize?${queryString}`;
        logger.debug(`Redirecting to ${URL}`);
        this._urlOpener(URL, redirectSignIn);

Reproduction steps

CLI

First you need to amplify add auth and select Apply default configuration with Social Provider (Federation). You will be asked for a domain name prefix. You should supply the following information:

When being prompted for the identity providers you want to configure for your user pool press enter without selecting any. This will create a user pool and create a Cognito domain that you will need in your third party auth provider.

amplify push to see your changes in the cloud.

AWS Console

Head to the AWS Console and search for AWS Amplify. Select all apps and click the app you are using. There you can select the Authentication category you just created: Pasted image 20221018124809 Once there, select the View in Cognito option: Pasted image 20221018124658 That will open your Amazon Cognito console. Select Sign-in experience and click in Add identity provider: Pasted image 20221018125012 You can select the third party identifier you are going to use. Be sure to use a provider name you will remember, this will be used as the provider argument in your Auth.federatedSignIn function: Pasted image 20221018125513 Fill in the rest of the information and click in Add identity provider.

After that the identity provider you just created should show up. You should go into App integration and scroll down to the App client list. Select the app that ends in clientWeb (You can follow the same procedure for the _appclient app if needed): Pasted image 20221018130332 From there go to Hosted UI and tap the Edit button: Pasted image 20221018130526 Scroll down to Identity providers and select the one you just created and hit Save changes at the end of the page: Captura de Pantalla 2022-10-18 a la(s) 1 06 56 p m

Third party provider

You should configure your third party provider using the following documentation for SAML and this documentation for OIDC. The redirect URI is key for the flow to work correctly.

Front end

You can use the following React code to test your newly created IdP. Notice that the provider is a string containing the name of the newly created IdP:

import { Auth } from "aws-amplify";
import { useEffect, useState } from "react";

export default function SignInPage() {
  const [user, setUser] = useState(null);

  const signIn = async () => {
    try {
      await Auth.federatedSignIn({ provider: "Azure" });
    } catch (error) {
      console.log("error signing in", error);
    }
  };

  useEffect(() => {
    getUser().then((userData) => setUser(userData));
  }, []);

  function getUser() {
    return Auth.currentAuthenticatedUser()
      .then((userData) => userData)
      .catch(() => console.log("Not signed in"));
  }

  return (
    <div>
      <h1>
        Welcome{" "}
        {user
          ? JSON.stringify(user.attributes.email)
          : "Ups! Not signed in"}
      </h1>
      {user ? (
        <button onClick={() => Auth.signOut()}>Sign Out</button>
      ) : (
        <button onClick={signIn}>Federated Sign In</button>
      )}
    </div>
  );
}

Once you click the button you should be redirected to your IdP and a new user should be created in your Cognito user pool.

Edit: Just realized you were asking about another query parameter. I'll leave the comment as a tutorial for adding third party idps.

igorlg commented 1 year ago

Just to add to this discussion, I faced exactly the same issue described by @ncarvajalc (thanks for the great walkthrough, BTW) and managed to do the UserPool IdP setup from within Amplify using overrides:

  1. (Assuming you already have auth setup in the project) generate a overrides file for Auth:
amplify override auth

This will create a file under amplify/backend/auth/override.ts (details in the docs here).

  1. Use this file to create a CloudFormation resource of type AWS::Cognito::UserPoolIdentityProvider (details here) with your IdP details:
import { AmplifyAuthCognitoStackTemplate, AmplifyProjectInfo } from '@aws-amplify/cli-extensibility-helper';

export function override(resources: AmplifyAuthCognitoStackTemplate, amplifyProjectInfo: AmplifyProjectInfo) {
    resources.addCfnResource(
        {
          type: "AWS::Cognito::UserPoolIdentityProvider",
          properties: {
            AttributeMapping: {
              <your K:V attribute mappings>
            },
            ProviderDetails: {
              client_id: "<your client id>",
              client_secret: "<your client secret>",
              authorize_scopes: "openid",
              oidc_issuer: "<oidc issuer URL>",
              attributes_url_add_attributes: false,
              attributes_request_method: "GET"
            },
            ProviderName: "<your provider name>",
            ProviderType: "OIDC",
            UserPoolId: {
              Ref: "UserPool",
            },
          },
        },
        "oidc-provider"
    );
}
  1. Push changes to Amplify
amplify push

After this point, it is possible to call Auth.federatedSignIn() in your app to redirect the user to the OIDC provider. You do need to specify the provider name - the same used in the CfnResource above - like so:

Auth.federatedSignIn({ provider: "<your provider name>" } as any);

Hope this helps!