Amplify-js: RFC: Auth Enhancements - Easier Federation with Cognito User Pools and Hosted UI

Created on 15 Feb 2019  ·  35Comments  ·  Source: aws-amplify/amplify-js

Overview: Based on feedback there is a fair amount of confusion when using User Pools for Social Provider Federation, especially when building a custom UI and bypassing the default Cognito Hosted UI. While Amplify documentation gives an overview of using redirects within the OAuth flow, this is cumbersome, error prone, and seen as difficult by customers who are unfamiliar with these flows. Therefore we wish to build this into Amplify's Auth category via a simple, declarative API. There are several phases and potential future options with different trade-offs which are outlined below.

Please reply with a +1 or detailed comment on a feature if you have specific thoughts around how it should work.

Related Issues:
https://github.com/aws-amplify/amplify-js/issues/1316
https://github.com/aws-amplify/amplify-js/issues/2644
https://github.com/aws-amplify/amplify-js/issues/2543
https://github.com/aws-amplify/amplify-js/issues/2525
https://github.com/aws-amplify/amplify-js/issues/2518
https://github.com/aws-amplify/amplify-js/issues/2512
https://github.com/aws-amplify/amplify-js/issues/2503
https://github.com/aws-amplify/amplify-js/issues/2456
https://github.com/aws-amplify/amplify-js/issues/2423
https://github.com/aws-amplify/amplify-js/issues/2283
https://github.com/aws-amplify/amplify-js/issues/2168
https://github.com/aws-amplify/amplify-js/issues/2156
https://github.com/aws-amplify/amplify-js/issues/2115
https://github.com/aws-amplify/amplify-js/issues/1585
https://github.com/aws-amplify/amplify-js/issues/1521
https://github.com/aws-amplify/amplify-js/issues/1392
https://github.com/aws-amplify/amplify-js/issues/1386
https://github.com/aws-amplify/amplify-js/issues/1281

Phase 1: Add User Pool Federation to federatedSignIn()

Amplify has an Auth.federatedSignIn() method today which allows customers to pass tokens to Cognito Identity and retrieve AWS credentials, which are then used to sign requests (with Signature Version 4) to AWS services. Request signing happens automatically when using other Amplify categories.

UPDATE: A new value of federationTarget will not be needed to the config. Instead in the design we are inferring the behavior from the existing configuration values.

Depending on the presence of Cognito User Pool or Identity Pool value passed to Amplify.configure(), the call to federatedSignIn() will use User Pools or Identity Pools as appropriate passing along the social provider token. In the case where both are present in your configuration, the social provider federation will go to User Pools, and the returned JWT token will be sent to Identity Pools thus federating the User Pool with that Identity Pool and providing AWS credentials to the caller automatically as well.

The existing behavior when only using an Identity Pool remains unchanged, and the only required arguments are the provider and token.

Auth.federatedSignIn(
  'provider',               //Facebook, Google, etc.
  'token',                  //OAuth token from provider
  'user',                   //Optional user attributes {username, phone}
  'expiresIn',              //Optional time to invoke refresh of provider OAuth token
  'IdentityID'              //Optional
);

UPDATED: When using User Pools, there are no arguments required and provider is optional. If a provider is not given the User Pool Hosted UI will be displayed. If a provider is given the page will redirect to the Hosted UI, but we'll add a query string to signal an immediate redirect to the social provider's login page (Facebook, G+, etc.). This allows you to build your own UI but still leverage the User Pool federation. In either case after the user signs in with that provider an account will be created in User Pools and a JWT token returned.

The user object contains information for you to use when calling Auth.currentSession() and returns a CognitoUserSession object. If you do not pass this object we will attempt to automatically populate the name, email, and phone_number attributes from the social provider based on known mechanisms (for example - Facebook requires a call to '/me' to retrieve details after authenticating) but if we are unable to retrieve the attributes we'll populate them as UNDEF.

The expiresIn value controls when Amplify will attempt to refresh the token from the social provider, and when the time comes do so automatically behind the scenes. This value was _previously required and is now optional_ as we will default by default attempt to retrieve the attribute from the social token, and if it does not exist we will set it to 1 hour. If you provide a value then Amplify will honor your provided time.

The IdentityID value is strictly for Cognito Identity Pools and allows you to set an Identity ID to retrieve credentials for a specific Cognito Identity ID: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityCredentials.html#identityId-property

_Implementation_

Identity Pool federation will stay as-is today, with the only changes being automatic generation of expiresIn logic. For User Pools Amplify.configure() will require domain, redirectSignIn, redirectSignOut, and responseType from https://aws-amplify.github.io/docs/js/authentication#configuring-the-hosted-ui. The scope will be optional (for discussion - should this move into the options object?). Note that in the future the Amplify CLI will set all this up see https://github.com/aws-amplify/amplify-cli/issues/766. Under the covers, we will manage the OAuth redirects on behalf of the developer such as the code below, which must be manually called today:

const { 
    domain,  
    redirectSignIn, 
    redirectSignOut,
    responseType } = config.oauth;

const clientId = config.userPoolWebClientId;
// The url of the Cognito Hosted UI
const url = 'https://' + domain + '/login?redirect_uri=' + redirectSignIn + '&response_type=' + responseType + '&client_id=' + clientId;
// If you only want to log your users in with Google or Facebook, you can construct the url like:
const url_to_google = 'https://' + domain + '/oauth2/authorize?redirect_uri=' + redirectSignIn + '&response_type=' + responseType + '&client_id=' + clientId + '&identity_provider=Google';
const url_to_facebook = 'https://' + domain + '/oauth2/authorize?redirect_uri=' + redirectSignIn + '&response_type=' + responseType + '&client_id=' + clientId + '&identity_provider=Facebook';

// Launch hosted UI
window.location.assign(url);

// Launch Google/Facebook login page
window.location.assign(url_to_google);
window.location.assign(url_to_facebook);

We will provide a config option (or another mechanism) for customers to override what "opening a url" means in the different platforms (e.g. using Expo's WebBrowser in React Native).

Potential future Option: Add Federation to signIn() and deprecate federatedSignIn()
_We are not doing this as part of this feature work but are including in the RFC to get early opinions for the future_

One future option is to take all of the implementation and functionality as above, however we would move this into Auth.signIn() instead of Auth.federatedSignIn().

PROS: Less APIs to learn, potentially simpler code that is standardized for many situations. Auth codebase could also have a “cascading rule set” depending on config leading to less logic branches.

CONS: This will take a larger effort as it introduces more code complexity into the existing signIn() method. There is also the fundamental question of whether or not the action of “federation” is actually a “sign-in” action (more below in Option 3). We would also need to deprecate the federatedSignIn() method requiring customers to update their code in the future if they upgrade versions. Perhaps the biggest issue though is that there is “no free lunch” and that while the code may become simpler from a customer perspective, you must push the logic somewhere and naturally that would go into Amplify.configure() to know what actions to take. This ultimately means that finding the appropriate API setting will be less intuitive.

Potential future Option: Introduce a new API for Federation
_We are not doing this as part of this feature work but are including in the RFC to get early opinions for the future_

All of the implementation and functionality as above, however we introduce a new API in Auth - Auth.XXX.

The reason for this discussion is should federation be an action tied to Login? The Wikipedia definition of federation (https://en.wikipedia.org/wiki/Federation_(information_technology)) has no terms around authentication but rather a “joining” or “association” of separate systems. In the Amplify case that can be either the joining of separate Identity Providers or it can be joining them to a single “logical identity”, ultimately all in order to provide authorization to resources (via OIDC tokens or AWS Credentials). This begs the question is there a more descriptive API such as simply Auth.federate() and, if one exists, is the name so much better that it makes performing these actions that much easier for customers?

Auth

Most helpful comment

Cannot +100 this enough 🙏

All 35 comments

Cannot +100 this enough 🙏

Yes yes yes! This sounds awesome and will be really helpful. 😍😍😍

You can't deliver this soon enough!

Is this correct?

When using User Pools, the only required argument is token. If a provider is not given the User Pool Hosted UI will be displayed. If a provider is given the page will redirect to the Hosted UI, but we'll add a query string to signal an immediate redirect to the social provider's login page (Facebook, G+, etc.).

Seems that if the call to federatedSignIn can redirect to the hosted UI, then token should not be required (because it is a return value from the hosted UI, whether it be through user pool username/password or social IdP). It should only be required if a custom UI has already obtained it from Auth.signIn() to the user pool or from a social IdP.

Might also be more orthogonal if provider were instead passed as cognito_userpool or such, instead of having Cognito user pool inferred from the absence of provider. Would it still then be required at Auth.configure()?

You can't deliver this soon enough!

Is this correct?

When using User Pools, the only required argument is token. If a provider is not given the User Pool Hosted UI will be displayed. If a provider is given the page will redirect to the Hosted UI, but we'll add a query string to signal an immediate redirect to the social provider's login page (Facebook, G+, etc.).

Seems that if the call to federatedSignIn can redirect to the hosted UI, then token should not be required (because it is a return value from the hosted UI, whether it be through user pool username/password or social IdP). It should only be required if a custom UI has already obtained it from Auth.signIn() to the user pool or from a social IdP.

@rcbrown Good catch, I was actually commenting more with respect to the implementation but yes you are correct that token isn't required here as the IdP will pass it back. I'll update the posting.

Might also be more orthogonal if provider were instead passed as cognito_userpool or such, instead of having Cognito user pool inferred from the absence of provider. Would it still then be required at Auth.configure()?

As a design principle Amplify uses configuration for making these decisions which allows tooling, such as the Amplify CLI, to scaffold things out. The goals are to reduce the need for Frontend engineers needing to learn all of the backend implementations and by pushing it into config we can keep a nice abstraction.

This all looks very useful, any idea on when it will be implemented? Trying to figure out whether to put a workaround in place for Social Sign-in users receiving JWT tokens or to just wait for the feature.

@donaldev it's a pretty complex piece of work and the goal is to have it work across JS and React Native, so there are some particulars to work through. Additionally there are some security related topics such as PKCE that customers have asked for to be included that I need some design around. I have a rough skeleton functioning in my local environment but it could be mid/late April depending on a few things before we could publish. I'll try to get something sooner in a beta branch that you might be able to use but want to be transparent.

Potential future Option: Add Federation to signIn() and deprecate federatedSignIn()

Today we're using federatedSignIn() with Cognito Identity Pools without Cognito User Pools. It's great to make federation easier for the User Pools case, but please don't break our existing use-case of being able to login to Facebook and to use Amplify to make calls to API Gateway without a User Pool!

Below is the TypeScript code that we're (successfully) using to enable login through Facebook that can safely call into API Gateway without a User Pool. It took a while to get this code right, so hopefully pasting it here will help others.

UPDATE: simplified the code sample below (thanks @undefobj!)

public onFacebookLoginResponse = async (response: ReactFacebookLoginInfo) => {
  if (response.name && response.id) {
    const user = {
      name: response.name,
      pictureUrl: `https://graph.facebook.com/${response.id}/picture?type=normal`,
      email: response.email,
      firstName: response.first_name,
      lastName: response.last_name,
      middleName: response.middle_name,
      shortName: response.short_name,
      nameFormat: response.name_format,
      facebookUserId: response.id
    };
    const federatedUser: FederatedUser = {name: user.name, email: user.email};

    try {
      await Auth.federatedSignIn( 
        'facebook', // TODO: federated sign-in with Google identity provider
        {
          token: response.accessToken,
          expires_at: response.expiresIn
        },
        federatedUser  
      );
      console.log ("Cognito login success!")
    }
    catch (e) {
      console.log(`Cognito login error: ${e}`);
      return;
    }

    // The user is authenticated, so we can call AWS services like API Gateway. 
    const initialUserInfo : CreateUserInput = {
      name: user.name,
      lastName: user.lastName,
      firstName: user.firstName,
      middleName: user.middleName ? user.middleName : null,
      shortName: user.shortName,
      nameFormat: user.nameFormat,
      email: user.email,
      pictureUrl: user.pictureUrl,
      facebookUserId: user.facebookUserId, 
      phone: null,
    };

    // Call AWS API Gateway to get this user (or create if the user doesn't exist)
    await this.getOrCreateCurrentUser(initialUserInfo);
  }
}

@justingrant thanks for the feedback here. If we ever change APIs we would do it in major version releases so that you could upgrade accordingly.

That being said, your code above mixes both Amplify's Auth category with the AWS SDK. Anything in that SDK, such as AWS.config.getCredentials(), isn't controlled by the Amplify library. However, you don't actually need to be making those calls to AWS.CognitoIdentityCredentials if you're using Auth.federatedSignIn() with Amplify since that happens under the covers for you. So if you're trying to use IAM credentials with API Gateway or other services without User Pools and instead a Social Provider as your IdP you can get AWS credentials by passing those tokens to Auth.federatedSignIn() today and Amplify will sign the requests for you.

If we ever change APIs we would do it in major version releases so that you could upgrade accordingly.

Thanks @undefobj - API changes are OK with us as long as the new API will support the same capabilities as the current API for logging in with a social identify using an Identity Pool but without a User Pool. From reading the RFC it looks like this is already the plan, which is great.

However, you don't actually need to be making those calls to AWS.CognitoIdentityCredentials if you're using Auth.federatedSignIn() with Amplify since that happens under the covers for you.

Cool. I was able to remove the non-Amplify code and Facebook login still worked. This is an ancient part of our app that's filled with copypasta, so thanks for helping me find redundant code!I I updated the original code sample above accordingly.

BTW the rest of this RFC looks great. Thanks for improving Auth!

Currently I use Cognito Hosted UI but Amplify CLI doesn't support it (at least last I looked ... wishing for Release Notes for Amplify CLI because it's hard to keep up). I wrap my app using withAuthenticator but the only authComp is a little redirector that calls props.OAuthSignIn after a delay.

As I understand the RFC, I'd be able to use the standard authComps, not Hosted UI, and still have federated logins reflected in my User Pool. If true, then Yes Please!

Currently I use Cognito Hosted UI but Amplify CLI doesn't support it

@ceich we're about to release a new version with this support. You can see this here: https://github.com/aws-amplify/amplify-cli/tree/hosted-ui-and-crud
This is the first stage of us delivering on the following RFC: https://github.com/aws-amplify/amplify-cli/issues/766

As I understand the RFC, I'd be able to use the standard authComps, not Hosted UI, and still have federated logins reflected in my User Pool. If true, then Yes Please!

The idea is that yes you'll be able to leverage the OAuth endpoints that are created when you setup the Hosted UI, but you don't actually need to "render" the Hosted UI (unless you want to) in your SPA. We'll use the configuration to define how federatedSignIn operates (and the CLI will create this config) and the Social Provider accounts will be represented in your User Pool.

Really looking forward to this.
Any timeline on when this will be released? should we expect weeks or months?

@ddennis as @undefobj specified here it could mid/late April

See also this amplify-cli RFC thread comment for their first release.

Hello everyone - we have a PR for this and published to our beta tag on NPM. While I've done a lot of testing myself and with team members if the community could run some tests in the next couple days it would help us have confidence to release sooner. You'll see the instructions here: https://github.com/aws-amplify/amplify-js/pull/3005#issuecomment-479737451

Note that through our design reviews and implementation we've been able to infer the setup and operations via the config in aws-exports which means that the difference from the initial RFC is an explicit key of federationTarget is not actually needed. We believe this simplifies things even further.

@ceich @serartmar @ddennis @justingrant @donaldev @rcbrown @khola @baffleinc

Hi

I have tested. Notice i have not used the CLI.

I don't want to use the hosted-ui - and i would prefer to manually send the facebook token.
like this:

Auth.federatedSignIn('facebook', {token, expires_at:expires}, {username:authResponse.userID, name:"test"})

My test - using authorization code grant

package.json

"aws-amplify": "1.1.24-beta.2",
"aws-amplify-react": "2.3.4-beta.1",

This is my config:

    Auth: {
        region: "eu-central-1",
        userPoolId: "eu-central-1_tXqk9YBgf",
        userPoolWebClientId:"41doioqalvkuqpn8sjvldvmija",
        mandatorySignIn: true,
        identityPoolRegion: 'eu-central-1',
        identityPoolId: 'eu-central-1:569e9408-fe0c-4a40-b0ea-2444e644a1ee',
        authenticationFlowType: 'USER_PASSWORD_AUTH',

        oauth: {
            domain: '*****-testing.auth.eu-central-1.amazoncognito.com',
            //scope: ['aws.cognito.signin.user.admin'],
            redirectSignIn:  'http://localhost:3000',
            redirectSignOut: 'http://localhost:3000',
            scope : ['profile', 'openid','aws.cognito.signin.user.admin'],
            responseType: 'code'
        },
    }

Notice that the scope exist multiple times. This is copy pasted from you example in the #3005 (comment) i uncommented one of them.

The user is created in my User Pool, but Auth.handleAuthResponse(). is always undefined.

Auth.currentCredentials() and currentAuthenticatedUser returns the same object as always when using federatedSignIn. But does not contain the idToken.

I tried loggin in multiple times and i took me through facebook login flow.
This has now stopped and it now redirects me back to http://localhost:3000/?code=*****-1e88-44c9-9ce1-bfc19369d5c7&state=*****dAeWGjQ9ozbLKiV.yrLAKTG_S. when calling Auth.federatedSignIn({provider: 'Facebook'})

I getting a 400 after calling Auth.handleOAuthResponse to this url: https://**-testing.auth.eu-central-1.amazoncognito.com/oauth2/token

I might have change something, but not really sure.

I tried deleting the user in my User Pool , it does not recreate the user.

Any ideas on why its not working?

I am expecting Auth.handleOAuthResponse() to return an idToken i can decode on my server.

Thanks much for testing @ddennis .

The user is created in my User Pool, but Auth.handleAuthResponse(). is always undefined.

If you are using the older version of federatedSignIn() where you're passing in tokens from Facebook, etc., then you don't call handleAuthResponse(). Note though that passing in the tokens manually from Facebook is not possible with Cognito User Pools and only possible with Identity Pools. handleAuthResponse() is only needed when using the newer functionality of OAuth flows with User Pools. The testing instructions were for the new flow but we'll make this clear in the final documentation.

I am expecting Auth.handleOAuthResponse() to return an idToken i can decode on my server.

If you want to do this with the new flow then don't pass in tokens to federatedSignIn() and after the redirect back and completing the process with handleAuthResponse() you can get them with Auth.currentSession() like so: https://aws-amplify.github.io/docs/js/authentication#retrieve-current-session.

Yes as stated i am using your example. For reference, the full code is here:

As shown in the previous post, this is package.json

"aws-amplify": "1.1.24-beta.2",
"aws-amplify-react": "2.3.4-beta.1",

Auth.federatedSignIn({provider: 'Facebook'})
I am asking for the following permissions: public_profile, email, user_friends, pages_show_list

I am experiencing the same as before:

I am taken through the facebook login flow.

Then redirected back to http://localhost:3000/?code=-1e88-44c9-9ce1-bfc19369d5c7&state=dAeWGjQ9ozbLKiV.yrLAKTG_S.

I getting a 400 after calling Auth.handleOAuthResponse to this url: https://**-testing.auth.eu-central-1.amazoncognito.com/oauth2/token

The user is created in my User Pool, but Auth.handleAuthResponse(). is always undefined.

Auth.currentSession() throws "No current user"

Nothing is set in localstorage?

So my questions:

  • is the version correct in my package.json
  • why does Auth.handleOAuthResponse causes a 400
  • why is localstorage not populated, when redirected back.
  • Do you have any idea why this is not working?

@ddennis did you enable "Authorization code grant" check box for the "userPoolWebClientId" that you have configured on that User Pool? It's under App Integration -> App Client Settings in the Cognito User Pool console.

Note the new Amplify CLI does this automatically however if you're using an existing User Pool you'll need to manually do this.

Yes - my settings is as below.

Enabled Identity Providers
[X] FacebookCognito [X] User Pool

Allowed OAuth Flows
[X]Authorization code grant [ ]Implicit grant [ ]Client credentials

Allowed OAuth Scopes
[ ]phone [X] email [X]openid [X]aws.cognito.signin.user.admin [X]profile

Made a video showing my experience: https://www.dropbox.com/s/0ohk2de9gwshv9t/cognito.webm?dl=0
Maybe that helps!

Both handleAuthResponse() and currentSession() are promises so if you want the second to happen after the first could you try calling them like so:

    Auth.handleAuthResponse().then(
      (res) => {
        console.log(" App.js > AuthResponse = ", res);
        Auth.currentSession().then((res) => {
          console.log(" App.js > session = ", res);
        }).catch((err) => {
          console.log(" App.js > err = ", err);
        })
      }
    );

@undefobj - is there a reason to avoid async/await? Seems clearer to do something like this:

const res = await Auth.handleAuthResponse();
try {
  const session = await Auth.currentSession();
  console.log(" App.js > session = ", session);
} catch (err) {
  console.log(" App.js > err = ", err);
}

@justingrant no reason you can do that too.

But should't there be data in localstorage? As shown in the video, everything is empty except the query string.

And whats causing the Auth.handleOAuthResponse() to throw a 400

It does not make any sense!!!

@ddennis Thanks for sending your gist. There are a couple things going on that we were able to simplify the beta codebase as well as clarify in our docs:

  1. You won't need to call handleAuthResponse now. We do that automatically for you and have made it private.
  2. Since federatedSignIn() will redirect to either the Hosted UI or your social provider (depending on the arguments passed) the promise will never resolve. To listen for logged in state you'll need to use the standard Hub.listen() methods that Amplify provides.
  3. We were able to use your config for the Facebook flow however your app client's redirect URI in the Cognito console looks to be incorrect so that we couldn't test. It was giving us a "RedirectUri is not registered with the client". I'd suggest either manually fixing it per the docs or setting up a new User Pool with the newly released Amplify CLI updates that do this for you.

Here is a CodeSandbox with your endpoint that works: https://codesandbox.io/s/jnn33xzwy5?fontsize=14

@undefobj Thanks alot for your time and for helping me with this.

I never manged to get the first version to work.
But setting up a new project using the amplify CLI made it work.

I am really curious about why the first version did not work, could it be because the hosted-ui was not working? or did i not enable it?

This should be the url of the hosted UI:
https://finewires-testing.auth.eu-central-1.amazoncognito.com/login?response_type=code&client_id=41doioqalvkuqpn8sjvldvmija

Thanks again.

@ddennis excellent glad you have it working. Not sure about the first one, could have been a typo in the redirect URIs inside the console.

Also we're working on a big doc update with a concepts section and easier explanation & navigation. If you or anyone else in this thread have any thoughts in the next 24 hours that would be great. Here they are on my fork:
https://undefobj.github.io/docs/js/authentication#concepts
https://undefobj.github.io/docs/js/authentication#social-providers-and-federation
https://undefobj.github.io/docs/js/authentication#oauth-and-hosted-ui

Hello everyone - We have merged this feature and published it to NPM. The new documentation will be going live tomorrow.

@undefobj

  1. Since federatedSignIn() will redirect to either the Hosted UI or your social provider (depending on the arguments passed) the promise will never resolve. To listen for logged in state you'll need to use the standard Hub.listen() methods that Amplify provides.

Does the same apply for React Native? Ive implement the same as above and Im navigating out of the app to verify with facebook Auth.federatedSignIn({ provider: 'Facebook' });. Once authenticated I'm navigated back to my app (redirect url is my app://).

Hub.listen is not hit in this scenario. I do also have a login button on screen that authenticated with my user pool directly. This works fine and Hub.listen is always triggered in this instance.

Me too, seems OAuth is still broken with react-native (without the hosted UI).

@chriscraiclabs @davidfarinha the solution works with React Native as well. Please open up a new issue for a team member to help you with troubleshooting as this issue is closed.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

callmekatootie picture callmekatootie  ·  3Comments

DougWoodCDS picture DougWoodCDS  ·  3Comments

rygo6 picture rygo6  ·  3Comments

guanzo picture guanzo  ·  3Comments

epicfaace picture epicfaace  ·  3Comments