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

Ability to initiate Auth sessions and set cookies server side with Next.js #11598

Open davidjulakidze opened 1 year ago

davidjulakidze commented 1 year ago

Before opening, please confirm:

JavaScript Framework

Next.js

Amplify APIs

Authentication

Amplify Categories

auth

Environment information

``` # Put output below this line System: OS: Windows 10 10.0.19044 CPU: (16) x64 AMD Ryzen 7 3700X 8-Core Processor Memory: 34.96 GB / 47.93 GB Binaries: Node: 18.15.0 - C:\Program Files\nodejs\node.EXE Yarn: 1.22.5 - C:\Program Files (x86)\Yarn\bin\yarn.CMD npm: 9.5.0 - C:\Program Files\nodejs\npm.CMD Browsers: Chrome: 114.0.5735.199 Edge: Spartan (44.19041.1266.0), Chromium (114.0.1823.67) Internet Explorer: 11.0.19041.1566 npmPackages: @ampproject/toolbox-optimizer: undefined () @babel/core: undefined () @babel/runtime: 7.15.4 @edge-runtime/cookies: 3.2.1 @edge-runtime/ponyfill: 2.3.0 @edge-runtime/primitives: 3.0.1 @emotion/react: ^11.11.1 => 11.11.1 @emotion/server: ^11.11.0 => 11.11.0 @hapi/accept: undefined () @mantine/core: ^6.0.16 => 6.0.16 @mantine/hooks: ^6.0.16 => 6.0.16 @mantine/next: ^6.0.14 => 6.0.16 @napi-rs/triples: undefined () @next/font: undefined () @next/react-dev-overlay: undefined () @opentelemetry/api: undefined () @prisma/client: ^4.15.0 => 4.16.2 @segment/ajv-human-errors: undefined () @tabler/icons-react: ^2.23.0 => 2.24.0 @types/crypto-js: ^4.1.1 => 4.1.1 @types/node: 20.2.5 => 20.2.5 (16.18.38) @types/react: 18.2.8 => 18.2.8 (18.2.14) @types/react-dom: 18.2.4 => 18.2.4 @vercel/nft: undefined () @vercel/og: undefined () acorn: undefined () amphtml-validator: undefined () anser: undefined () arg: undefined () assert: undefined () async-retry: undefined () async-sema: undefined () autoprefixer: 10.4.14 => 10.4.14 aws-amplify: ^5.2.7 => 5.3.3 axios: ^1.4.0 => 1.4.0 (0.26.0) babel-packages: undefined () browserify-zlib: undefined () browserslist: undefined () buffer: undefined () bytes: undefined () chalk: undefined () ci-info: undefined () cli-select: undefined () client-only: 0.0.1 comment-json: undefined () compression: undefined () conf: undefined () constants-browserify: undefined () content-disposition: undefined () content-type: undefined () cookie: undefined () cookies-next: ^2.1.2 => 2.1.2 cross-spawn: undefined () crypto-browserify: undefined () crypto-js: ^4.1.1 => 4.1.1 css.escape: undefined () data-uri-to-buffer: undefined () debug: undefined () devalue: undefined () domain-browser: undefined () edge-runtime: undefined () encoding: ^0.1.13 => 0.1.13 eslint: 8.42.0 => 8.42.0 eslint-config-next: ^13.4.9 => 13.4.9 events: undefined () find-cache-dir: undefined () find-up: undefined () fresh: undefined () get-orientation: undefined () glob: undefined () gzip-size: undefined () http-proxy: undefined () http-proxy-agent: undefined () https-browserify: undefined () https-proxy-agent: undefined () icss-utils: undefined () ignore-loader: undefined () image-size: undefined () is-animated: undefined () is-docker: undefined () is-wsl: undefined () jest-worker: undefined () json5: undefined () jsonwebtoken: undefined () loader-runner: undefined () loader-utils: undefined () lodash.curry: undefined () lru-cache: undefined () micromatch: undefined () mini-css-extract-plugin: undefined () nanoid: undefined () native-url: undefined () neo-async: undefined () next: ^13.4.9 => 13.4.9 node-fetch: undefined () node-html-parser: undefined () ora: undefined () os-browserify: undefined () p-limit: undefined () path-browserify: undefined () platform: undefined () postcss: 8.4.24 => 8.4.24 (8.4.14, 8.4.25) postcss-flexbugs-fixes: undefined () postcss-modules-extract-imports: undefined () postcss-modules-local-by-default: undefined () postcss-modules-scope: undefined () postcss-modules-values: undefined () postcss-preset-env: undefined () postcss-safe-parser: undefined () postcss-scss: undefined () postcss-value-parser: undefined () prisma: ^4.15.0 => 4.16.2 process: undefined () punycode: undefined () querystring-es3: undefined () raw-body: undefined () react: ^18.2.0 => 18.2.0 react-builtin: undefined () react-dom: ^18.2.0 => 18.2.0 react-dom-builtin: undefined () react-dom-experimental-builtin: undefined () react-experimental-builtin: undefined () react-infinite-scroll-component: ^6.1.0 => 6.1.0 react-is: 18.2.0 react-plaid-link: ^3.4.0 => 3.4.0 react-refresh: 0.12.0 react-server-dom-webpack-builtin: undefined () react-server-dom-webpack-experimental-builtin: undefined () regenerator-runtime: 0.13.4 sass-loader: undefined () scheduler-builtin: undefined () scheduler-experimental-builtin: undefined () schema-utils: undefined () semver: undefined () send: undefined () server-only: 0.0.1 setimmediate: undefined () shell-quote: undefined () source-map: undefined () stacktrace-parser: undefined () stream-browserify: undefined () stream-http: undefined () string-hash: undefined () string_decoder: undefined () strip-ansi: undefined () tailwindcss: 3.3.2 => 3.3.2 tar: undefined () terser: undefined () text-table: undefined () timers-browserify: undefined () ts-loader: ^9.4.3 => 9.4.4 tty-browserify: undefined () typescript: 5.1.3 => 5.1.3 ua-parser-js: undefined () undici: undefined () unistore: undefined () util: undefined () vm-browserify: undefined () watchpack: undefined () web-vitals: undefined () webpack: undefined () webpack-sources: undefined () ws: undefined () zod: undefined () npmGlobalPackages: create-react-app: 3.4.1 jscrambler: 5.5.14 jshint: 2.11.0 lerna: 4.0.0 serve: 11.3.2 typescript: 5.1.3 vector-ui: 0.1.10 vsts-npm-auth: 0.42.1 yarn: 1.22.19 ```

Describe the bug

I'm attempting to configure amplify in a client component then make a request to the server with the SSR context, but the cookies are not set after calling configure.

from official docs: By providing ssr: true, Amplify persists credentials on the client in cookies so that subsequent requests to the server have access to them.

Expected behavior

cookies to be populated after Amplify.configure is called client side.

Reproduction steps

  1. next dev
  2. visit /

Code Snippet

src/app/page.tsx

// Put your code below this line.
"use client";
import { Amplify } from "aws-amplify";
import awsconfig from "../aws-exports";

export default function Home() {
  Amplify.configure({...awsconfig, ssr: true});
  return <>Home</>;
}

Log output

``` // Put your logs below this line - event compiled client and server successfully in 189 ms (20 modules) - wait compiling /page (client and server)... - warn ./node_modules/@aws-sdk/client-lex-runtime-service/node_modules/@aws-sdk/util-user-agent-node/dist-cjs/is-crt-available.js Module not found: Can't resolve 'aws-crt' in 'F:\projects\x\node_modules\@aws-sdk\client-lex-runtime-service\node_modules\@aws-sdk\util-user-agent-node\dist-cjs' Import trace for requested module: ./node_modules/@aws-sdk/client-lex-runtime-service/node_modules/@aws-sdk/util-user-agent-node/dist-cjs/is-crt-available.js ./node_modules/@aws-sdk/client-lex-runtime-service/node_modules/@aws-sdk/util-user-agent-node/dist-cjs/index.js ./node_modules/@aws-sdk/client-lex-runtime-service/dist-cjs/runtimeConfig.js ./node_modules/@aws-sdk/client-lex-runtime-service/dist-cjs/LexRuntimeServiceClient.js ./node_modules/@aws-sdk/client-lex-runtime-service/dist-cjs/index.js ./node_modules/@aws-amplify/interactions/lib/Providers/AWSLexProvider.js ./node_modules/@aws-amplify/interactions/lib/index.js ./node_modules/aws-amplify/lib/index.js ./src/app/page.tsx ``` this error is related to #11030 and the page still renders so the warning can be ignored however the cookies are not set when visiting the page.

aws-exports.js

/* eslint-disable */
// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

const awsmobile = {
    "aws_project_region": "us-east-1",
    "aws_cognito_identity_pool_id": "",
    "aws_cognito_region": "us-east-1",
    "aws_user_pools_id": "",
    "aws_user_pools_web_client_id": "",
    "oauth": {},
    "aws_cognito_username_attributes": [
        "EMAIL"
    ],
    "aws_cognito_social_providers": [],
    "aws_cognito_signup_attributes": [
        "EMAIL"
    ],
    "aws_cognito_mfa_configuration": "OFF",
    "aws_cognito_mfa_types": [
        "SMS"
    ],
    "aws_cognito_password_protection_settings": {
        "passwordPolicyMinLength": 12,
        "passwordPolicyCharacters": [
            "REQUIRES_LOWERCASE",
            "REQUIRES_NUMBERS",
            "REQUIRES_SYMBOLS",
            "REQUIRES_UPPERCASE"
        ]
    },
    "aws_cognito_verification_mechanisms": [
        "EMAIL"
    ]
};

export default awsmobile;

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

screenshot

cwomack commented 1 year ago

Hello, @davidjulakidze 👋 and thanks for opening this issue. Is there a reason why you've got your Amplify.configure() being called within the Home component? While I don't think moving it outside your component will resolve the cookie issue, it should help to ensure that it's not being called on every render.

Calling Amplify.configure({...awsconfig, ssr:true}) enables SSR support within Amplify, but it doesn't handle the cookie management. Since you're using ssr: true, it should be called on the server side. This might have been why you placed the configure call in your Home component, but the only way to ensure that it's only being called on the client side would be to call it within a useEffect hook.

Is there any particular reason why you're looking to manage cookies on the client side?

davidjulakidze commented 1 year ago

So to use amplify client side I would need to configure it using useEffect but how about if I want everything server side like so:

for example have an src/app/api set up with routes like: /configureAmplify <- Amplify.configure({}) /signIn <- SSR.Auth.signIn /getcurrentAuthenticatedUser <- SSR.Auth.currentAuthenticatedUser()

if I call /api/configureAmplify -> /api/signIn -> /api/getcurrentAuthenticatedUser I get "The user is not authenticated"

both signIn and getcurrentAuthenticatedUser amplify calls worked which means its configured correctly but yet getcurrentAuthenticatedUser returned user not authenticated even though signIn succeeded and returned a cognito user. why?

cwomack commented 1 year ago

@davidjulakidze, you mentioned that both Auth.signIn() and getCurrentAuthenticatedUser() are working but you still see that error in the console. Can you share the frontend code that's calling the getCurrentAuthenticatedUser() where you see "user is not authenticated" happening? Does it happen on refresh as well?

davidjulakidze commented 1 year ago

I'm attempting to do everything server side, lets say you have next.js api routes set up like so:

src/app/api/configureAmplify/route.ts

import { NextRequest, NextResponse } from "next/server";
import { Amplify } from "aws-amplify";
import awsconfig from "@/aws-exports";

export async function POST(req: NextRequest): Promise<NextResponse> {
  try {
    Amplify.configure({ ...awsconfig, ssr: true });
    return new NextResponse(
      JSON.stringify({
        message: "Amplify configured",
      }),
      {
        status: 200,
        headers: {
          "Content-Type": "application/json",
        },
      }
    );
  } catch (error) {
    const { message } = error as { message: string };
    const responseError = {
      error: message,
    };
    return new NextResponse(JSON.stringify(responseError), {
      status: 500,
      headers: {
        "Content-Type": "application/json",
      },
    });
  }
}

src/app/api/signIn/route.ts

import { withSSRContext } from "aws-amplify";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest): Promise<NextResponse> {
  try {
    const { username, password } = await req.json();
    const SSR = withSSRContext({ req });
    const user = await SSR.Auth.signIn({
      username,
      password,
    });
    return new NextResponse(JSON.stringify(user), {
      status: 200,
      headers: {
        "Content-Type": "application/json",
      },
    });
  } catch (error) {
    const { code, name, message, stack } = error as any;
    const errorResponse = JSON.stringify({ code, name, message, stack });
    return new NextResponse(errorResponse, {
      status: 500,
      headers: {
        "Content-Type": "application/json",
      },
    });
  }
}

src/app/api/getAuthenticatedUser/route.ts

import { withSSRContext } from "aws-amplify";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest): Promise<NextResponse> {
  try {
    const SSR = withSSRContext({ req });
    const user = await SSR.Auth.currentAuthenticatedUser();
    return new NextResponse(JSON.stringify(user), {
      headers: { "content-type": "application/json" },
      status: 200,
    });
  } catch (e) {
    return new NextResponse(JSON.stringify(e), {
      headers: { "content-type": "application/json" },
      status: 500,
    });
  }
}

make the following api calls one after the other fetch("api/configureAmplify") -> fetch("api/signIn") -> fetch("api/getAuthenticatedUser")

fetch("api/configureAmplify") returns "amplify configured" fetch("api/signIn") returns a cognito user fetch("api/getAuthenticatedUser") returns not authenticated (?)

cwomack commented 1 year ago

@davidjulakidze, appreciate you sharing the your code and providing some additional clarity. I think I can help address what you're looking to accomplish and possibly offer a suggestion to improve your app's security.

When using withSSRContext, the request object that is used is "read-only" for getting the cookies sent from the client. If you're looking to have cookies be populated on the client side, having ssr: true configured will get you the cookies as long as you are calling the Auth API's on the client side. Currently, there is no way to refresh cookies or obtain new access tokens using the refresh token on the server side.

Additionally, the use of const { username, password } = await req.json(); does not look secure because you're extracting the username and password from the incoming request payload and sending them as JSON in the request body. Use of the Auth API's by contrast will always be encrypted with SRP.

Let me know if this provides the clarity you're looking for and helps determine next steps.

davidjulakidze commented 1 year ago

I have made changes to configure Amplify on both the server and client sides, moved the login logic to a server action, and attached the server action to my form. However, it seems there is still an error related to Amplify's configuration.

In the client component, I added the following code to configure Amplify on the client side:


"use client";
...
...
useEffect(() => {
  Amplify.configure({ ...awsconfig, ssr: true });
}, []);
...

I moved the login logic to a server action:


"use server";
...
export async function login(formData: FormData): Promise<void> {
  const username = formData.get("Username");
  const password = formData.get("Password");
  const SSR = withSSRContext();
  const user = await SSR.Auth.signIn({
    email,
    password,
  });
  ... (do stuff with user)
}

I attached the server action to my form in the client component:

<form action={login}>
...
</form>

` I make an api call to that configureAmplify api route mentioned above so that amplify is configured server side.

However, after filling out the form and pressing submit, I encountered the following error:

{"name":"NoUserPoolError","log":"\n Error: Amplify has not been configured correctly. \n The configuration object is missing required auth properties.\n This error is typically caused by one of the following scenarios:\n\n 1. Did you runamplify pushafter adding auth viaamplify add auth?\n See https://aws-amplify.github.io/docs/js/authentication#amplify-project-setup for more information\n\n 2. This could also be caused by multiple conflicting versions of amplify packages, see (https://docs.amplify.aws/lib/troubleshooting/upgrading/q/platform/js) for help upgrading Amplify packages.\n "}

affanshahid commented 1 year ago

@cwomack Please let me know if I understand correctly: calling Auth.signIn() on the server-side (i.e inside a server component/action) will NOT set the cookies?

asp3 commented 1 year ago

@cwomack Please let me know if I understand correctly: calling Auth.signIn() on the server-side (i.e inside a server component/action) will NOT set the cookies?

from what i understand, authentication mutation actions should be done from client side. only data fetching should be from server, which can read the cookies, or API calls (which can be signed with the token already in cookies)

davidjulakidze commented 1 year ago

@cwomack Please let me know if I understand correctly: calling Auth.signIn() on the server-side (i.e inside a server component/action) will NOT set the cookies?

from what i understand, authentication mutation actions should be done from client side. only data fetching should be from server, which can read the cookies, or API calls (which can be signed with the token already in cookies)

So I can't use a form action to sign my users in? This seems very redundant, especially since the latest next.js allows you to set cookies server side anyway

cwomack commented 1 year ago

@affanshahid, that is correct. As @asp3 stated, any Auth API calls should be done on the client side. If you attempt to call the Auth API's on the server side (or within a server component), the cookies will not get set. This means the cookies will not be sent to the client-side browser.

@davidjulakidze, Next.js will indeed allow you to write outgoing request cookies in a server action, but it won't work with the recommended Authentication flows that Amplify Authentication and Cognito will use. The form actions should be done on the client side to collect the user inputs along with calling the Auth API's on the client side.

davidjulakidze commented 1 year ago

@cwomack alright thank you for your answers. assuming this is only for auth, would it be fair to assume we might see this become possible in the future versions of amplify-js?

cwomack commented 1 year ago

@davidjulakidze, anytime! Given the specificity of this issue for something that isn't available (but desired) in the current version of Amplify, I'll actually convert this to a feature request and change the title. The ability for customers to be able to initiate Auth sessions and set cookies on the server side is something we may look into for a future version.

We appreciate you taking the time to open this issue! With it being converted to a feature request, more people may see this and give it some "thumbs up" or comments to help it gain traction.

MattyBalaam commented 10 months ago

I would really like to see this feature, but in our case for remix.

I have managed to get it working in a hacky fashion server side, but at the expense of the user agent and ip address being logged incorrectly.

rogueturnip commented 9 months ago

I wanted to bump this topic now that v6 is out. I gave things a try today to see if I could get login working with a server action but it did not work. I suspect we can only do auth on the client side still.

ryanwalters commented 7 months ago

@cwomack Can someone from the Amplify team please confirm if Next.js server actions are still unsupported in v6 for commands like signIn, signUp, etc?

From what I can tell, @aws-amplify/auth/server only exports fetchAuthSession, fetchUserAttributes, and getCurrentUser.

Basic server action example ```tsx // app/sign-in/page.tsx import { signIn } from '@aws-amplify/auth'; import { Amplify } from 'aws-amplify'; import { amplifyConfig } from '~/utils/amplify'; Amplify.configure(amplifyConfig, { ssr: true }); async function signInAction(formData: FormData) { 'use server'; try { const { username, password } = Object.fromEntries(formData.entries()); const response = await signIn({ username: username.toString(), password: password.toString() }); console.log(response); } catch (error) { console.error(error); } } export default function Page() { return (
); } ```
Error message ```bash UserUnAuthenticatedException: User needs to be authenticated to call this API. at assertAuthTokens (webpack-internal:///(rsc)/./node_modules/@aws-amplify/auth/dist/esm/providers/ cognito/utils/types.mjs:26:15) at getCurrentUser (webpack-internal:///(rsc)/./node_modules/@aws-amplify/auth/dist/esm/providers/co gnito/apis/internal/getCurrentUser.mjs:17:71) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async signInWithSRP (webpack-internal:///(rsc)/./node_modules/@aws-amplify/auth/dist/esm/provide rs/cognito/apis/signInWithSRP.mjs:71:23) at async $$ACTION_0 (webpack-internal:///(rsc)/./app/server-actions/page.tsx:27:26) at async \node_modules\next\dist\compiled\next-server\app-page.runtime.dev. js:39:406 at async t0 (\node_modules\next\dist\compiled\next-server\app-page.runtime. dev.js:38:5773) at async rh (\node_modules\next\dist\compiled\next-server\app-page.runtime. dev.js:39:23636) at async doRender (\node_modules\next\dist\server\base-server.js:1391:30) at async cacheEntry.responseCache.get.routeKind (\node_modules\next\dist\se rver\base-server.js:1552:28) at async DevServer.renderToResponseWithComponentsImpl (\node_modules\next\d ist\server\base-server.js:1460:28) at async DevServer.renderPageComponent (\node_modules\next\dist\server\base -server.js:1843:24) at async DevServer.renderToResponseImpl (\node_modules\next\dist\server\bas e-server.js:1881:32) at async DevServer.pipeImpl (\node_modules\next\dist\server\base-server.js: 909:25) at async NextNodeServer.handleCatchallRenderRequest (\node_modules\next\dis t\server\next-server.js:266:17) at async DevServer.handleRequestImpl (\node_modules\next\dist\server\base-s erver.js:805:17) { underlyingError: undefined, recoverySuggestion: 'Sign in before calling this API again.', constructor: [class AuthError extends AmplifyError] } ```

Edit: I see this was confirmed in a different issue: link

nadetastic commented 7 months ago

Hi @ryanwalters glad you were able to get a confirmation on the other issue. For additional reference here is the list of supported API for SSR usage:

nadetastic commented 6 months ago

Comment with additional context/usage - https://github.com/aws-amplify/amplify-js/issues/12660#issuecomment-1954899578

Hi @nadetastic , I want to use the amplify wrapper to interact with cognito because i'm familiar the amplify api. I need to be able to log users in and out, and send out invites for team members (multi tenant). I expect to write a lambda auth trigger to populate the tokens with tenant id.

Once I have an authenticated user I won't be passing the token back to AWS services beyond the remix server. I expect to build all the authentication in remix server-side.

At this stage in the product I just need basic auth functionality so i can get to the rest of the features.

gotgenes commented 4 months ago

I'm particularly interested in if this feature will allow Amplify to address known OAuth vulnerabilities to cross-site scripting attacks by turning the Amplify backend into a confidential client and a backend-for-frontend.

OAuth BFF