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.42k stars 2.12k forks source link

Amplify UI Authenticator not picking up authorisation code from Cognito HostedUI redirect #12983

Closed Pylinho closed 5 months ago

Pylinho commented 8 months ago

Before creating a new issue, please confirm:

On which framework/platform are you having an issue?

React

Which UI component?

Authenticator

How is your app built?

Create React App

What browsers are you seeing the problem on?

Chrome, Firefox, Microsoft Edge

Which region are you seeing the problem in?

eu-west-1

Please describe your bug.

In a React 18 application, I am using aws-amplify 6.0.13 to handle authentication with Cognito. This is working fine for sign ins where a user is inputting there login details themselves, but isn't working for SSO logins managed via Cognito's Hosted UI. Let me describe my setup.

My amplify is configured as so in my React application:

import { Amplify } from 'aws-amplify';
import { defaultStorage } from 'aws-amplify/utils';
import { cognitoUserPoolsTokenProvider } from 'aws-amplify/auth/cognito';

export default function amplifyConfigure() {
  Amplify.configure({
    Auth: {
      Cognito: {
            identityPoolId: myIdPoolId,
            region: myRegion,
            userPoolId: myUserPoolId,
            userPoolClientId: myUserpoolAppClientId,
            loginWith {
                  oauth: {
                      domain: myOAuthDomain,
                      scopes: [ "email", "profile", "openid"],
                      redirectSignIn: "http://localhost:3000/",
                      redirectSignOut: "http://localhost:3000/",
                      responseType: "code",
                  }
            }
      }
    }
  });

  cognitoUserPoolsTokenProvider.setKeyValueStorage(defaultStorage);
}

for which I have anonymised the inputs. amplifyConfigure() is called in my index.js file.

I have a HostedUI in Cognito, which is configured to redirect back to the root directory of my application with an authorisation code grant. When debugging locally, I can see that the user is being authenticated in the HostedUI and the auth code is being attached by the Hosted UI correctly, as I get redirected to http://localhost:3000/?code={code}. However, this code is not then being traded for the authentication tokens with Cognito, as there is no further activity in the developer console in my browser.

I have confirmed that the redirect sign in and sign out URLs are configured identically in both my application code for the Amplify.configure() setup and the Hosted UI, so I have ruled out this as the issue. What could be causing Amplify to not trade the code for the authentication token in my application?

I have tried importing and using Hub to listen for auth events, which has confirmed to me that the code is definitely not being taken and used:

import { Hub } from 'aws-amplify/utils';

Hub.listen('auth', (data) => {
  const { payload } = data;
  this.onAuthEvent(payload);
  console.log(
    'A new auth event has happened: ',
    data.payload.data.username + ' has ' + data.payload.event
  );
});

and can see from my browser dev tools that no calls are being made to Cognito from my application after being redirected.

Just to note, this issues has arisen as we have recently upgraded from amplify v5, where we had our components wrapped with withAuthenticator(), where the SSO for our clients who used it was set up and working just fine through the Cognito HostedUI and aws-amplify setup.

I have raised the issue both here and over at https://github.com/aws-amplify/amplify-js, as I was unsure which specifically the bug would relate to.

What's the expected behaviour?

The Authenticator component takes the authorisation code automatically and uses it to authenticate the user with Cognito and login in our application.

Help us reproduce the bug!

N.A.

Too complex and application specific for me to provide a reproducible example, unfortunately.

Code Snippet

My App.js file returns:

// Put your code below this line.
return (
    <ThemeProvider theme={theme}> 
      <Authenticator components={components}>
        <UserProvider>
          <MyRouting/>
        </UserProvider>
      </Authenticator>
    </ThemeProvider>
  );

where MyRouting contains the routing for my application.

I can provide the theme and components objects if necessary, but components just contains a Header with a logo.

Console log output

No response

Additional information and screenshots

The object I pass into Amplify configure:

{
    Auth: {
      Cognito: {
            identityPoolId: myIdPoolId,
            region: myRegion,
            userPoolId: myUserPoolId,
            userPoolClientId: myUserpoolAppClientId,
            loginWith {
                  oauth: {
                      domain: myOAuthDomain,
                      scopes: [ "email", "profile", "openid"],
                      redirectSignIn: "http://localhost:3000/",
                      redirectSignOut: "http://localhost:3000/",
                      responseType: "code",
                  }
            }
      }
    }
  }
hbuchel commented 8 months ago

Hi @Pylinho while we try to reproduce this, could you try using the latest aws-amplify@6.0.13? There have been a few fixes since 6.0.9 related to Auth and the cognito hosted UI

Pylinho commented 8 months ago

Hi there @hbuchel, thanks for the speedy response. I had already upgraded to aws-amplify@6.0.13 in fact, as well as @aws-amplify/ui-react@6.1.2. To no avail, unfortunately.

Also, for the original issue I raised I had copied across the object we passed to Amplify.configure() from an old version of our code, which we used prior to us upgrading from Amplify v5 to v6. I have edited my message to reflect.

reesscot commented 8 months ago

@Pylinho How are you initiating the SSO flow? Is the user clicking a button from your application, or are you simply redirecting them to the Cognito Hosted UI if they are not authenticated?

For Amplify to consume the code, you have to initiate the OAuth flow from your application. If you do this from the Authenticator there is a specific set of Social Providers that are supported: https://ui.docs.amplify.aws/react/connected-components/authenticator/configuration#social-providers

If you're using a custom provider, this is not supported by the Authenticator UI Component and you will want to create your own button which uses the signInWithRedirect function available from the aws-amplify JS library.

Pylinho commented 8 months ago

@reesscot You're correct with your diagnosis, it's not from within our application, no.

Some of our clients point to our Cognito HostedUI from their own applications, which in turn redirects them to our app with the auth code. This worked with our previous implementation old versions of amplify (4.2.10) and aws-amplify-react (5.0.16).

My understanding of the usage of signInWithRedirect is that it would be used within our application to send that request to Cognito HostedUI, which doesn't work with our auth flow. Our external client's implementation is to send a request to HostedUI directly, which then brings the user to our application with the auth code, which we would ideally like to use to automatically exchange for tokens with Cognito.

What is the reason for the Authenticator component being able to detect the code if using one of the supported social providers / signInWithRedirect, but not when receiving direct from HostedUI as it was previously?

Cheers again for your time and reply.

Pylinho commented 8 months ago

Hello again,

Just to give you a bit more to go off, we reverted back to using @aws-amplify/ui-react 5.2.0 and aws-amplify 5.3.15, which are both compatible with our React 18 upgrade, and wrapped our App in withAuthenticator().

This is still working in the same way that v4 was, in that our clients who authenticate direct with Cognito Hosted UI and are then redirected to our site are successfully logged in automatically with the authorisation code that is passed from Cognito to our application.

It seems that it's the Authenticator component specifically, or the new restructuring of the object that is passed to configureAmplify(), with the oauth object being nested with the loginWith object, that has broken the authorisation code being picked up from Cognito Hosted UI when the request is not initiated from within the application.

import { withAuthenticator } from '@aws-amplify/ui-react';
...
const theme = {...}
const App = () => {
...
};

export default withAuthenticator(App,
  false,
  [],
  null,
  theme,
  {hide: true});

with our amplifyConfigure function being called in our index.js file:

import { Amplify } from 'aws-amplify';

export default function configureAmplify() {
  Amplify.configure({
    Auth: {
        identityPoolId: myIdentityPoolId,
        region: myRegion,
        userPoolId: myUserPoolId,
        userPoolWebClientId: myUserpoolAppClientId,
        oauth: {
            domain: myOAuthDomain,
            scope: [ 'email', 'profile', 'openid'],
            redirectSignIn: myRedirectSignIn,
            redirectSignOut: myRedirectSignOut,
            responseType: 'code',
        }
    },
    Storage:{
        AWSS3: {
            identityPoolId: myIdentityPoolId,
            region: myRegion,
            bucket: myS3BucketName
        }
    }
  });
} 

Let us know if you find anything / have any updates.

Cheers.

reesscot commented 8 months ago

@Pylinho The way that the authorization code is consumed was changed in aws-amplify@6 which is why you see the issue go away when downgrading both libraries to v5. I'm transferring this over to the JS repo for further triage.

nadetastic commented 8 months ago

Hi @Pylinho, I'd like to provide some clarity on this issue. It looks like the app where the request to Hosted UI is made from is on a different domain than where the user is finally redirected to - in this case, that won’t work (for security reasons). In short, the flow has to be initiated from same app, or at least the same domain.

However a possible work around is for you to potentially set up a proxy page for this purpose, with a flow like the following:

  1. Initial App A
  2. App B’s proxy page with signInWithRedirect()
  3. Hosted UI
  4. App B with authenticated user session
Pylinho commented 8 months ago

Hi @nadetastic,

Cheers again for the information.

We did consider implementing a proxy on our end as suggested. However, this would involve some dev work on our clients end, who have set up their dev and prod environments to be configured to point straight at Cognito HostedUI to allow for SSO between their respective sites and ours.

We want to avoid this, as it is not a good look for us to get back to them to say we are moving away from what is a pretty fundamental and commonly implemented authentication flow, and requiring them to put some developer time into it as a result.

What are the security concerns surrounding yourselves supporting ingesting the authentication code from Cognito when the request was not initiated from within the application? The authorisation code has to be exchanged for access tokens with Cognito before a user is authenticated anyway, so if a bad actor was to try and send an invalid authorisation code to gain access to our system, the automatic pickup and transfer of the code for the tokens with Cognito would be rejected and fail anyway. Correct me if I'm wrong, of course.

If there is no plan to add back in support for this authentication flow in v6, we would likely look at moving away from aws-amplify and implementing our own custom authentication solution, unfortunately. It feels like this functionality shouldn't have been removed between v5 and v6 from our perspective.

Thanks again for your time and continued support.

sankaran85 commented 7 months ago

Facing same issue with v6 , after login with congnito hosted ui when i try to access the getCurrentUser . Im getting error like

UserUnAuthenticatedException: User needs to be authenticated to call this API. at assertAuthTokens (webpack-internal:///./node_modules/@aws-amplify/auth/dist/esm/providers/cognito/utils/types.mjs:29:15) at getCurrentUser (webpack-internal:///./node_modules/@aws-amplify/auth/dist/esm/providers/cognito/apis/internal/getCurrentUser.mjs:16:71)

No call to cognito made from amplify on page load. Please help us in solving this issue.

nadetastic commented 7 months ago

HI @Pylinho following up here as there is another alternative that can work for you.

Instead of setting a proxy page between the starting app (A) and the final app (B), you can have a user go directly to the final app (B), and check if a session exists> If it doesn't then you could automatically trigger a signIn to Hosted UI which would complete successfully on the redirect since the request originated from app B

  1. Start at app A
  2. From app A, user is sent to app B
  3. On app B, if there is no session call signInWithRedirect()
  4. After completing authentication the user is redirected back to app B successfully
nadetastic commented 7 months ago

@Pylinho following up here - wanted to see if you still have any questions with this issue.

dimo121 commented 7 months ago

So with v6 we need to wrap with the Authenticator component and it should pick up the code and set the session in Amplify once authenticated, then we can call through amplify to get user details?

Pylinho commented 6 months ago

Hi @nadetastic,

The other solution you have proposed does not solve our problem. We don't want our clients to have to implement a workaround, when a request direct to Cognito HostedUI redirecting their users to our application with an authorisation code should suffice.

Where you previously mentioned:

It looks like the app where the request to Hosted UI is made from is on a different domain than where the user is finally redirected to - in this case, that won’t work (for security reasons).

What are the security concerns preventing your Authenticator component from picking up the authorisation code when it is returned from HostedUI when the request doesn't originate from the application itself?

From my understanding, a user who is unauthorised will be blocked by Cognito HostedUI irrespective of where the request originates from, hence not returning an authorisation code to the application to complete the auth process.

Therefore, the request originating from outside of the application should not make a difference i.e. if the Authenticator component receives an authorisation code, it knows the user is authorised to retrieve the login credentials from Cognito to grant access to the application.

If there is a good reason for the Authenticator not handling this, then we would be willing to hear it and implement a workaround solution on our end. Otherwise, we would expect this to be an auth pattern that should be handled, and will unfortunately be looking to move away and implement our own authentication solution.

Thanks again for your replies!

cwomack commented 6 months ago

Hey, @Pylinho 👋. Was there additional context or questions here that were missed that caused you to reopen this issue?

As for the "why" behind this being not supported, this RFC on OAuth 2.0 security best practices will provide some insight and reasoning around Cross-Site Request Forgery.

Deanfei commented 6 months ago

Same issue here. It stucked there when using signInWithRedirect in the application. It's so frustrating. Cognito v6 is making my life a lot harder.

Pylinho commented 6 months ago

Morning @cwomack. No additional context, I just came back to check up on the issue and realised I had hit "Close with comment" rather than just "Comment" when submitting last time around.

Thanks for the CSRF link. However, I'm still not sure how a potential attacker injecting their own authorisation code between Cognito HostedUI and the Authenticator component receiving this poses a security risk.

Given the object we are passing to Amplify.configure() would be as so for v6:

{
    Auth: {
      Cognito: {
            identityPoolId: myIdPoolId,
            region: myRegion,
            userPoolId: myUserPoolId,
            userPoolClientId: myUserpoolAppClientId,
            loginWith {
                  oauth: {
                      domain: myOAuthDomain,
                      scopes: [ "email", "profile", "openid"],
                      redirectSignIn: myRedirectSignInURL,
                      redirectSignOut: myRedirectSignOutURL,
                      responseType: "code",
                  }
            }
      }
    }
  }

surely this configuration prevents an authorisation code injected into the redirect URI by a bad actor from being used maliciously? The Authenticator component would try and exchange the authorisation code for a token with our Cognito domain/user pool, which would be rejected, correct me if I'm wrong?

Unless there is something that is not secure about how the Amplify configuration is set up/used in the Authenticator component, meaning that a bad actor could take the authorisation code from the outgoing request and redirect it to their own Cognito domain?

Cheers again for your time.

bbdev9805 commented 5 months ago

Is there any update on this issue? I am experiencing a similar problem. Since upgrading to Amplify V6, SSO via SAML works for SP-initiated but not for IdP-initiated SSO. I am redirected from the Idp to https://www.example.com/?code=[Authorization code] but cannot obtain the authentication token. This needs to be resolved immediately if IdP-initiated SSO is to be supported. https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-SAML-session-initiation-idp-initiation.html

Deanfei commented 5 months ago

I believe it's caused by this: https://github.com/aws-amplify/amplify-js/issues/13306

cwomack commented 5 months ago

@Pylinho and others on this issue, thank you for the additional context around what's being asked for in this issue. This looks to be a duplicate of the feature request detailed within #13343. We'll close this issue as a duplicate and encourage anyone on this issue to give upvotes, comments, and any further context on #13343.