Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Auth Enhancements - Easier Federation with Cognito User Pools and Hosted UI #2716

Closed
undefobj opened this issue Feb 15, 2019 · 35 comments
Closed
Assignees
Labels
Auth Related to Auth components/category
Milestone

Comments

@undefobj
Copy link
Contributor

undefobj commented Feb 15, 2019

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:
#1316
#2644
#2543
#2525
#2518
#2512
#2503
#2456
#2423
#2283
#2168
#2156
#2115
#1585
#1521
#1392
#1386
#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 aws-amplify/amplify-cli#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?

@undefobj undefobj pinned this issue Feb 15, 2019
@undefobj undefobj added this to the AdminAuth milestone Feb 15, 2019
@undefobj undefobj added the Auth Related to Auth components/category label Feb 15, 2019
@baffleinc
Copy link

Cannot +100 this enough 🙏

@khola
Copy link

khola commented Feb 16, 2019

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

@undefobj
Copy link
Contributor Author

Potentially include scopes per: #297

@rcbrown
Copy link

rcbrown commented Feb 18, 2019

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
Copy link

rcbrown commented Feb 18, 2019

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()?

@undefobj
Copy link
Contributor Author

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.

@ddennis
Copy link

ddennis commented Apr 3, 2019

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

@serartmar
Copy link

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

@ceich
Copy link
Contributor

ceich commented Apr 3, 2019

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

@undefobj
Copy link
Contributor Author

undefobj commented Apr 4, 2019

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: #3005 (comment)

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

@ddennis
Copy link

ddennis commented Apr 4, 2019

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.

@undefobj
Copy link
Contributor Author

undefobj commented Apr 4, 2019

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.

@ddennis
Copy link

ddennis commented Apr 4, 2019

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?

@undefobj
Copy link
Contributor Author

undefobj commented Apr 4, 2019

@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.

@ddennis
Copy link

ddennis commented Apr 4, 2019

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

@ddennis
Copy link

ddennis commented Apr 4, 2019

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

@undefobj
Copy link
Contributor Author

undefobj commented Apr 4, 2019

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);
        })
      }
    );

@justingrant
Copy link
Contributor

@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);
}

@undefobj
Copy link
Contributor Author

undefobj commented Apr 4, 2019

@justingrant no reason you can do that too.

@ddennis
Copy link

ddennis commented Apr 4, 2019

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!!!

@undefobj
Copy link
Contributor Author

undefobj commented Apr 6, 2019

@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

@ddennis
Copy link

ddennis commented Apr 8, 2019

@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.

@undefobj
Copy link
Contributor Author

undefobj commented Apr 8, 2019

@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

@ghost ghost removed the review label Apr 8, 2019
@undefobj
Copy link
Contributor Author

undefobj commented Apr 9, 2019

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

@ghost
Copy link

ghost commented May 17, 2019

@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.

@davidfarinha
Copy link

davidfarinha commented May 21, 2019

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

@undefobj
Copy link
Contributor Author

@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.

@aws-amplify aws-amplify locked as resolved and limited conversation to collaborators May 22, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Auth Related to Auth components/category
Projects
None yet
Development

No branches or pull requests