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

ability to create Guest/Anonymous sessions #6649

Open
antho1jp opened this issue Feb 8, 2023 · 15 comments
Open

ability to create Guest/Anonymous sessions #6649

antho1jp opened this issue Feb 8, 2023 · 15 comments
Labels
accepted The feature request is considered. If nobody works actively on it, feel free to. enhancement New feature or request

Comments

@antho1jp
Copy link

antho1jp commented Feb 8, 2023

Description 📓

In the Headless E-commerce space there is a need to create "Guest" sessions to track user actions in Backend Services. Currently using next-auth and the only way to create a session before a user logs in manually (or reviving a session) is to wait until the client code has loaded.

Now that getSession() server side is stable it would nice to be able to check the status of a session and create a 'Guest' session if the session is empty or unauthenticated.

How to reproduce ☕️

The functionality doesn't exist currently to show an example. Credential methods are only fireable through client side code.

Would like something like below on server side:

signIn('Guest')

Contributing 🙌🏽

No, I am afraid I cannot help regarding this

@antho1jp antho1jp added enhancement New feature or request triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime. labels Feb 8, 2023
@balazsorban44 balazsorban44 added accepted The feature request is considered. If nobody works actively on it, feel free to. and removed triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime. labels Feb 8, 2023
@balazsorban44 balazsorban44 changed the title Ability to create sessions server side ability to create Guest/Anonymous sessions Feb 8, 2023
@balazsorban44
Copy link
Member

@mico-tacl
Copy link

mico-tacl commented May 20, 2023

I think the best solution here is put a initial screen saying if they want to login as guest,using gmail or using their credentials if the user click guest mode put the default username and password, if not use the credentials provided by the user.

I face the same issue today, but i came up with this approach.

Its. really hard to tell if you do it in your home screen onload.

@antho1jp
Copy link
Author

That might work in some cases, unfortunately for a large brand e-commerce site, compared to sites like Best Buy or Amazon that step would be problematic. The goal is to reduce as many steps as possible for sales.

Really I just need a method to create the Next-Auth cookie on the server in getInitialProps. There we could check if one exists, validate it, and if it didn't pass or didn't exist we just create one with guest context all prior to the page load.

So far we can check exists and validate, but can't create one without using sign-in client side. I haven't looked into this in a few months though so maybe something has changed in the new @auth package

@jason-allen-oneal
Copy link

I agree. A method to create a "guest" session with some default values that mirror whatever data a user session stores would be a huge help.

@maxeth
Copy link

maxeth commented Jul 24, 2023

I just came across this use case and created a custom JWT via the exported encode method which I then set as the session token.

import { encode } from "next-auth/jwt";
import { serialize } from "cookie";

// [...]
// inside an API route

const tokenName =
  process.env.NODE_ENV === "production"
    ? "__Secure-next-auth.session-token"
    : "next-auth.session-token";

const myCustomJWTToken = encode({
// just a simplified payload example, this would associate every anonymous visitor with the same dummy user
 token: {
    email: "",
    name: "Unknown User",
    picture: "",
    sub: "idOfDummyDBUser",
},
secret: process.env.NEXTAUTH_SECRET!
});

res.setHeader(
  "Set-Cookie",
  serialize(tokenName, myCustomJWTToken, {
    httpOnly: true,
    path: "/",
    sameSite: "lax",
    secure: process.env.NODE_ENV === "production",
    domain:
      process.env.NODE_ENV === "production"
        ? "example.com"
        : "localhost",
  })
);

Based on my first tests this seems to work — allowing you to set any custom payload in the JWT, including an "anonymous session" with any custom logic.

Does that have any limitations that I'm missing?

I basically just let the client do a simple fetch for an API route that creates this anonymous/dummy session.

@Puetz
Copy link

Puetz commented Aug 17, 2023

Hey @maxeth did you find any major downsides to your solution yet?

And could you please explain how you make the API call?
I tried to implement a simple GET request:

export async function GET() {
  // ... your coding example
  
  return new Response("Hello", {
    status: 200,
    headers: {
      "Set-Cookie": serialize(tokenName, myCustomJWTToken, {
        httpOnly: true,
        path: "/",
        sameSite: "lax",
        secure: process.env.NODE_ENV === "production",
        domain: process.env.NODE_ENV === "production" ? "example.com" : "localhost",
      }),
    },
  });
}

When I first navigate to my page I make a fetch request to my route and get the "Hello" message returned.

  const session = await getServerSession(options);
  if (!session) {
    const res = await fetch("http://localhost:3000/api/default").then(res => res.text());
    console.log(res);
  }

Unfortunately this does not set the cookie for me.
If I manually type the API route into my browser and open localhost:3000 afterwards, the cookie was set successfully.

Thanks a lot for your help :)

@maxeth
Copy link

maxeth commented Aug 23, 2023

@Puetz

did you find any major downsides to your solution yet?

For my use case it works perfectly fine :)

Unfortunately this does not set the cookie for me.

I assume you're using the new app router, right? I'm still using the pages directory and just doing a basic fetch from the client after React hydration, not on the server-side. You're issue is probably app-router-specific, so I can't really say much.

@rinvii
Copy link

rinvii commented Aug 23, 2023

Unfortunately this does not set the cookie for me.

Cookies cannot be set in server components with Next.js app router.

Instead, you can pass the request to a client component and have the client send the request which will set the cookies from the response.

If you still want to set the cookies server-side, you can do this through the middleware.

@DVGY
Copy link

DVGY commented Sep 23, 2023

This is really an important enchantment for us too. Waiting for this one

@pablojsx
Copy link

Interested

@cleverlight
Copy link

@maxeth I started with the same approach, but eventually moved over to the two-provider setup described in this issue.

export const authOptions: AuthOptions = {
    providers: [
        CredentialsProvider({
            name: "anonymous",
            credentials: {},
            async authorize(credentials, req) {
                return createAnonymousUser();
            },
        }),
        GithubProvider({
            clientId: process.env.GITHUB_CLIENT_ID as string,
            clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
        }),
    ],
   ...
}
const handler = NextAuth(authOptions);
export {handler as GET, handler as POST}

You can then automatically wrap it inside your NextAuthProvider:

export default function NextAuthProvider({...}) {
    return (
          <SessionProvider>
              <AnonymousSessionProvider>
                  {children}
              </AnonymousSessionProvider>
          </SessionProvider>
    );
}

For anyone interested, I've released a complete example in this repo and I wrote a short explanation of the interesting bits in a blog post.

@rijk
Copy link

rijk commented Oct 9, 2023

I use the following code in a server action, to create an anonymous user on the first interaction:

import { cookies } from 'next/headers'
import { encode } from 'next-auth/jwt'

import { defaultCookies } from '../../node_modules/next-auth/core/lib/cookie'

async function getOrCreateUserId() {
  const session = await getServerSession(authOptions)
  if (session) {
    return session.user.id
  } else {
    const id = await insert('users', {})

    // Sign in the new user by generating a token manually...
    const token = await encode({
      token: { sub: id },
      secret: env.NEXTAUTH_SECRET,
    })

    // ...and setting it as a cookie
    const secure = process.env.NODE_ENV === 'production'
    const { name, options } = defaultCookies(secure).sessionToken
    cookies().set(name, token, options)

    return id
  }
}

@lukevella
Copy link

lukevella commented Oct 27, 2023

I've implemented a similar solution to the ones described here using credentials provider but I don't think this is the best approach. This forces you into using the jwt session strategy unless you are willing to customize your database adapter to accommodate guest users which I don't think is a good idea.

If I had to start over, I would consider building a separate solution for guest users that works in parallel to next-auth, i.e. store the guest user in a separate cookie. Then I would create an abstraction for API's like getServerSession() and useSession() that consolidate the two with one interface.

import { getServerSession as getNextAuthServerSession } from "next-auth/next"

const nextAuthConfig  = {
  // next-auth config
}

export async function getServerSession(req, res) {
    const nextAuthSession = await getNextAuthServerSession(req, res, nextAuthConfig);

    if (!nextAuthSession) {
        return await getGuestUserSession(req, res);
    }

    return nextAuthSession;
}

The abstraction simply allows you to fallback to a guest user if next-auth session doesn't exist. This way you can still use database sessions for authenticated users and makes it less of a headache if you ever decide to move away from next-auth.

@rijk
Copy link

rijk commented Oct 30, 2023

I use the following code in a server action, to create an anonymous user on the first interaction:

import { cookies } from 'next/headers'
import { encode } from 'next-auth/jwt'

import { defaultCookies } from '../../node_modules/next-auth/core/lib/cookie'

async function getOrCreateUserId() {
  const session = await getServerSession(authOptions)
  if (session) {
    return session.user.id
  } else {
    const id = await insert('users', {})

    // Sign in the new user by generating a token manually...
    const token = await encode({
      token: { sub: id },
      secret: env.NEXTAUTH_SECRET,
    })

    // ...and setting it as a cookie
    const secure = process.env.NODE_ENV === 'production'
    const { name, options } = defaultCookies(secure).sessionToken
    cookies().set(name, token, options)

    return id
  }
}

Just to add to this, if you have already mounted the <SessionProvider> it won't pick up on the updated cookie automatically. Calling session.update() will not work either, because of this check of if (loading || !session) return.

To make the front end pick up on the new next-auth.session-token cookie, I found that simply doing this works:

signIn('email', { redirect: false })

Providing an actual email is not necessary, and omitting that also prevents in my case (magic link provider) unwanted verification emails being sent.

@jadiaheno
Copy link

jadiaheno commented Feb 8, 2024

Spent some time on this problem as a take-home test, so here is the full solution: https://github.com/jadiaheno/vention-machine-cloud-test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted The feature request is considered. If nobody works actively on it, feel free to. enhancement New feature or request
Projects
None yet
Development

No branches or pull requests