nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
24.05k stars 3.33k forks source link

Support for server side NextAuth.signin() #45

Closed DomenicoCammarota closed 6 years ago

DomenicoCammarota commented 6 years ago

Is there a way to perform signin & logout from the server side? NextAuth.signin()/logout() only work client side. Is there any work around (i.e, intercepting the post on /auth/signin ???)

iaincollins commented 6 years ago

NextAuth fully supports server side rendering.

If you try the included example site with JavaScript disabled in the browser, you can still sign in, view pages and be recognised as being signed in, and sign out again using <form method="post"> submission to endpoints like /auth/signin and /auth/signout.

This works for all types of sign in – although some oAuth providers like Google require JavaScript to display their sign in pages.

iaincollins commented 6 years ago

Note that if you want to sign in as someone else programmatically, the easiest way to do that is probably to define your own signIn() method in next-auth.functions.js and then do an HTTP POST to /auth/signin.

Note that you will need to save the cookie to make subsequent requests as that user, including sign out. This is a an example using the node fetch module from NPM that posts a username and password and saves the returned session cookie in a cookie jar so it can be sent with future requests.

const fetch = require('fetch')
const fetchUrl = fetch.fetchUrl
const cookieJar = new fetch.CookieJar()

const url = 'http://www.example.com/auth/signin'
const username = 'jsmith'
const password = 'abc123'

fetchUrl(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
  },
  body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`,
  cookieJar: cookieJar
}, (error, meta, body) => {
  // Check response
  console.log(body.toString())
})

You could add a Client ID / Secret pair or API Key to your user account object and accept that instead of a password in your signIn() function.

iaincollins commented 6 years ago

If you have any follow up questions please feel free to re-open.

DomenicoCammarota commented 6 years ago

Thanks for the update. I was hesitant to access the next-auth routes manually but was able to signin as you suggested above successfully.

One follow-up question, though. Is there a way to capture errors thrown in the next-auth.functions signin() method? For example, if signin() fails, next-auth redirects to error.js and passes '/auth/error?action=signin&type=credentials' in the query string. Is there a way to capture more detailed information regarding the failure - perhaps the error originally thrown during signin()??

iaincollins commented 6 years ago

Sorry for the delay in responding (been away for a few days)!

Hmm yes the error handling isn't very good there. We should make that at ticket and improve it.

Thanks for bringing it up! I think we could probably add another parameter to that query string with the original error message.

markitics commented 3 years ago

Hi Iain, thanks for great work with NextAuth! I've started to use it for a blank-slate project, basically rebuilding from scratch an old and creaking Django project.

There's a particular page where I want anonymous users to be automatically signed in

Here's how it works using django-allauth in the old Django project:

from django.shortcuts import render

def my_view(request, obscure_uid=None):
    # Does some server-side logic before rendering html to client in full-page load
    …
    if not request.user.is_authenticated():
        user = infer_user_based_on_obscure_code(obscure_uid)
        # user is a User, but maybe they haven't yet set a password
        # If they haven't yet set a password, log them in now and present the "set password" form in html
        # (This is a one-off event for the user, as they'll always have .password non-blank in future)
        # if they have a password, then don't do this - they need to be logged in, or see the "login form" in html
        if user and not user.password:
            # user is a User object we created in advance (because of a webhook), with an email, but no password set yet
            # hence they can't log in the normal way, but we'll log them in server-side just this once
            from allauth.account.utils import perform_login
            perform_login(request, listener, email_verification=None)
    …
    return render(request, "template.html", {'user': user, …})

As you see, perform_login can sign the user in, server-side, before the page loads.

I'm having a bit of difficulty spotting the equivalent in next-auth, and am aware it's been a couple of years since you wrote this:

Note that if you want to sign in as someone else programmatically, the easiest way to do that is probably to define your own signIn() method in next-auth.functions.js and then do an HTTP POST to /auth/signin.

Am I correct in saying next-auth.functions.js is deprecated in v3? What's the equivalent way to do perform_login, conditionally and server-side, using next-auth?

If it's bleedingly obvious in the docs, then apologies for missing it, feel free to just paste a link. This is not just my first foray into the world of Next, but my first React adventure too.

topbloc-beiswenger commented 2 years ago

I also have a use case for this! In our application we have one identity provider (Okta) that protects every page from being viewed if the user is not logged in. In the case that they are not logged in I'd like the page to redirect directly to the Okta login to start the authentication flow. My current options in version 4 are to either:

  1. Redirect to /api/auth/signin or /api/auth/signin/okta within getServerSideProps where the user then has to click to start the authentication flow
  2. Show a splash screen on the client side before calling signIn('okta') and starting the authentication flow

Neither of these are ideal as it would be best to just go straight to the Okta login page 🙂

balazsorban44 commented 2 years ago

Though not documented yet and you would need to try and fail a bit, but the next-auth core has been completely decoupled from Next.js #2857, so you might have a shot at this!

For reference, you can check out the getServerSession and try to implement a server-side signing function as well, which will return you the URL to redirect the user to: https://github.com/nextauthjs/next-auth/blob/35ee608d594c6a63d2236df9015e25fb1198a925/src/next/index.ts#L80-L103

export async function getServerSideProps(context) {
  const session = await getServerSession(context, options)
  if(session) {
    return { props: { session } }
  }

  const url = await getSignIn("okta", context, options) // you choose the signature to be whatever you want

  return {
    redirect: {
      destination: url,
      permanent: false
    }
  }
}
topbloc-beiswenger commented 2 years ago

@balazsorban44 Thanks for the response! How does the getServerSession function help in this scenario? As far as I can tell, that function just allows you to retrieve the session without having to make an http request on the server. What seems to be the difficulty in this implementation is creating the getSignIn function as I have to manually re-create the URL from parameters defaulted and passed into the provider.

balazsorban44 commented 2 years ago

It's just an example to give you an idea of how to implement it. getSignIn would create you the authorization url with all it's params and would return you the url that you can redirect the user to directly.

joematune commented 2 years ago

@balazsorban44 Would getSignIn be any different than getAuthorizationUrl? https://github.com/nextauthjs/next-auth/blob/30a0fc6bc0246b30eb03c19b8a0ceb6c040c1b2e/src/core/lib/oauth/authorization-url.ts#L17

MagnusSafty commented 2 years ago

Any news on this? I have a similar issue, no public URLs and I currently execute the signIn() in a useEffect, I would like to avoid the empty page render.

bymoe commented 2 years ago

+1

pfcodes commented 2 years ago

+1

davidtsai commented 2 years ago

Running into the same issue, happy to contribute a PR if someone is able to point in the right direction.

marcfalk commented 2 years ago

+1 ✌️

itsUndefined commented 2 years ago

+1. Can we re-open the issue?

pfcodes commented 2 years ago

bump

sheahwen commented 2 years ago

+1 Would love to see this feature 👍

BrandonClapp commented 2 years ago

Would be great to have. I'm currently hacking my way with a redirect to /api/auth/signin?callbackUrl=xyz from within getServerSideProps, but it would be nice to have a server side equivalent to next-auth/react's signIn to preserve the callbackUrl while attempting to redirect from the server.

ecrofeg commented 2 years ago

+1 👍 We'd like to have a possibility to create sessions for anonymous users during SSR

yanickrochon commented 1 year ago

I am amazed to see how the solution to almost every problem with this module is to perform self HTTP requests! There is no public APIs, everything is designed around making requests. This is both limiting and awkward.

ammmze commented 1 year ago

Here's what I'm probably going to be using to get a sign in url from the server side (borrowing ideas from the getServerSesssion method as mentioned here)...

import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from 'next';
import { getCsrfToken, SignInAuthorizationParams, SignInOptions } from 'next-auth/react';
import { NextAuthHandler } from 'next-auth/core';
import { setCookie } from 'cookies-next';
import type { NextAuthOptions } from 'next-auth';
import type { BuiltInProviderType, RedirectableProviderType } from 'next-auth/providers';
import type { LiteralUnion } from 'next-auth/react/types';

/**
 * Gets the sign in url from the server side.
 * @param context The request/response context (typically the context from getServerSideProps)
 * @param providerId The provider id that you want to sign in with.
 * @param authOptions The NextAuth options (i.e. from api/auth/[...nextauth].ts)
 * @param options Sign in options ... should be equivalent to the client side sign in options, minus the redirect
 *                param...i don't think it applies here
 * @param authorizationParams Additional parameters to pass to the authorization request
 */
export async function getServerSignInUrl<P extends RedirectableProviderType | undefined = undefined>(
    context: GetServerSidePropsContext | { req: NextApiRequest; res: NextApiResponse },
    providerId: LiteralUnion<P extends RedirectableProviderType ? P | BuiltInProviderType : BuiltInProviderType>,
    authOptions: NextAuthOptions,
    options: Omit<SignInOptions, 'redirect'> = {},
    authorizationParams: SignInAuthorizationParams = {}
): Promise<string> {
    const params = new URLSearchParams({
        ...options as Record<string, string>,
        csrfToken: await getCsrfToken({ ctx: context }),
        json: 'true',
    });
    const result = await NextAuthHandler({
        options: authOptions,
        req: {
            host: detectHost(context.req.headers['x-forwarded-host']),
            action: 'signin',
            method: 'POST',
            cookies: context.req.cookies,
            headers: context.req.headers,
            providerId,
            body: Object.fromEntries(params.entries()),
            query: authorizationParams as Record<string, string>,
        },
    });

    const { redirect, cookies } = result;

    cookies?.forEach((cookie) => setCookie(cookie.name, cookie.value, { ...cookie.options, req: context.req, res: context.res }));

    return redirect;
}

/**
 * Copied from next-auth since it isn't exported in a way that we can use it.
 * Tweaked types to be more explicit and accept an array of strings since that is what req.headers[*] potentially gives.
 * @param forwardedHost
 */
function detectHost(forwardedHost: string | string[]): string {
    // If we detect a Vercel environment, we can trust the host
    if (process.env.VERCEL ?? process.env.AUTH_TRUST_HOST)
        return Array.isArray(forwardedHost) ? forwardedHost[0] : forwardedHost;
    // If `NEXTAUTH_URL` is `undefined` we fall back to "http://localhost:3000"
    return process.env.NEXTAUTH_URL;
}

So usage would look something like this:

import { authOptions } from './api/auth/[...nextauth].page';
export const getServerSideProps: GetServerSideProps<PageProps> = async (context) => {
  const destination = await getServerSignInUrl(
    context,
    'keycloak', // the id of the provider you want to sign in with
    authOptions,
    { callbackUrl: 'http://localhost:3000/foo' }, // sign in options (if desired/needed)...for example to set callback url to send me to /foo after sign in
    { kc_idp_hint: 'mock-idp-oidc' } // authorization params (if desired/needed) ... in this example, im sending an idp hint to keycloak
  );
  return {
    redirect: {
      destination,
      permanent: false,
    }
  };
}
ammmze commented 1 year ago

Here's what I'm probably going to be using to get a sign in url from the server side

Welp...looks like that solution breaks with next-auth 4.18.1 due to a refactor to use standard request/response objects. I started updating, but then ran into some additional issues/bugs from that refactor. So will probably need to wait for #5991 to be complete before having a new method. And maybe I shouldn't call my stuff complete until I at least get a PR to contribute that getServerSignInUrl to next-auth.

ammmze commented 1 year ago

Welp...looks like that solution breaks with next-auth 4.18.1 due to a refactor to use standard request/response objects

Here's what works for me with v4.18.6

import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from 'next';
import { getCsrfToken } from 'next-auth/react';
import { AuthHandler as NextAuthHandler } from 'next-auth/core';
import type { SignInOptions } from 'next-auth/react';
import type { NextAuthOptions } from 'next-auth';
import type { BuiltInProviderType, RedirectableProviderType } from 'next-auth/providers';
import type { LiteralUnion } from 'next-auth/react/types';

/**
 * Gets the sign in url from the server side.
 * @param context The request/response context (typically the context from getServerSideProps)
 * @param providerId The provider id that you want to sign in with.
 * @param authOptions The NextAuth options (i.e. from api/auth/[...nextauth].ts)
 * @param options Sign in options ... should be equivalent to the client side sign in options, minus the redirect
 *                param...i don't think it applies here
 * @param authorizationParams Additional parameters to pass to the authorization request
 */
export async function getServerSignInUrl<P extends RedirectableProviderType | undefined = undefined>(
    context: GetServerSidePropsContext | { req: NextApiRequest; res: NextApiResponse },
    providerId: LiteralUnion<P extends RedirectableProviderType ? P | BuiltInProviderType : BuiltInProviderType>,
    authOptions: NextAuthOptions,
    options: Omit<SignInOptions, 'redirect'> = {},
    authorizationParams: Record<string, string> = {}
): Promise<string> {
    const { req } = context;
    const params = {
        ...options as Record<string, string>,
        csrfToken: await getCsrfToken({ ctx: context }),
        json: 'true',
    };
    const url = getURL(`/api/auth/signin/${encodeURIComponent(providerId)}`, req.headers as unknown as Headers);

    if (url instanceof Error) return null;

    Object.entries(authorizationParams).forEach(([key, value]) => {
        url.searchParams.append(key, value);
    });

    const authRequestHeaders = new Headers(req.headers as HeadersInit);
    authRequestHeaders.set('content-type', 'application/x-www-form-urlencoded');

    const request = new Request(url, {
        headers: authRequestHeaders,
        method: 'POST',
        body: JSON.stringify(params),
    });
    const result = await NextAuthHandler(request, authOptions);

    const { headers, status } = result;

    if (status >= 400) {
        throw new Error('There was an error while trying to retrieve the server sign in url.');
    }

    const cookies = `${headers.get('set-cookie') || ''}`.split(/(?<!expires=.{3}),/i);
    context.res.setHeader('set-cookie', cookies.map(cookie => cookie.trim()));

    return headers.get('Location');
}

/**
 * Copied from next-auth since it isn't exported in a way that we can use it.
 */
export function getURL(url: string | undefined, headers: Headers): URL | Error {
    try {
        if (!url) throw new Error('Missing url');
        if (process.env.NEXTAUTH_URL) {
            const base = new URL(process.env.NEXTAUTH_URL);
            if (!['http:', 'https:'].includes(base.protocol)) {
                throw new Error('Invalid protocol');
            }
            const hasCustomPath = base.pathname !== '/';

            if (hasCustomPath) {
                const apiAuthRe = /\/api\/auth\/?$/;
                const basePathname = base.pathname.match(apiAuthRe)
                    ? base.pathname.replace(apiAuthRe, '')
                    : base.pathname;
                return new URL(basePathname.replace(/\/$/, '') + url, base.origin);
            }
            return new URL(url, base);
        }
        const proto =
            headers.get('x-forwarded-proto') ??
            (process.env.NODE_ENV !== 'production' ? 'http' : 'https');
        const host = headers.get('x-forwarded-host') ?? headers.get('host');
        if (!['http', 'https'].includes(proto)) throw new Error('Invalid protocol');
        const origin = `${proto}://${host}`;
        if (!host) throw new Error('Missing host');
        return new URL(url, origin);
    } catch (error) {
        return error as Error;
    }
}
petdomaa100 commented 1 year ago

+1

megaacheyounes commented 1 year ago

+1

petdomaa100 commented 1 year ago

Here is a working + cleaner version for Next Auth v4.18.7:

import { getCsrfToken } from 'next-auth/react';
import { init } from '../../../node_modules/next-auth/core/init'; // You have to import it like this
import getAuthorizationUrl from '../../../node_modules/next-auth/core/lib/oauth/authorization-url'; // You have to import it like this

import type { NextAuthOptions } from 'next-auth';
import type { BuiltInProviderType } from 'next-auth/providers';
import type { NextApiRequest } from 'next';

async function getServerSignInUrl(req: NextApiRequest, providerId: BuiltInProviderType, authOptions: NextAuthOptions) {
    const { options } = await init({
        action: 'signin',
        authOptions,
        cookies: req.cookies,
        isPost: true,
        csrfToken: await getCsrfToken({ req }),
        providerId
    });

    return getAuthorizationUrl({ options, query: {} });
}
trgsv commented 1 year ago

Does anyone have a solution on how to do this inside the NextJS 13 /app directory?

I get a OAUTH_CALLBACK_ERROR and it seems like I can't get the code_verifier cookie with the cookies().getAll() method.

sync-adm commented 1 year ago

Here is a working + cleaner version for Next Auth v4.18.7:

import { getCsrfToken } from 'next-auth/react';
import { init } from '../../../node_modules/next-auth/core/init'; // You have to import it like this
import getAuthorizationUrl from '../../../node_modules/next-auth/core/lib/oauth/authorization-url'; // You have to import it like this

import type { NextAuthOptions } from 'next-auth';
import type { BuiltInProviderType } from 'next-auth/providers';
import type { NextApiRequest } from 'next';

async function getServerSignInUrl(req: NextApiRequest, providerId: BuiltInProviderType, authOptions: NextAuthOptions) {
  const { options } = await init({
      action: 'signin',
      authOptions,
      cookies: req.cookies,
      isPost: true,
      csrfToken: await getCsrfToken({ req }),
      providerId
  });

  return getAuthorizationUrl({ options, query: {} });
}

When using this work around, getAuthorizationUrl() always pick http://localhost:3000 as a host, even if I change NEXTAUTH_URL to another port. In my case since I'm using Auth0 as openId provider it returns:

signinUrl: 'http://localhost:3000/api/auth/signin/auth0', callbackUrl: 'http://localhost:3000/api/auth/callback/auth0'

Which end up causing a URL mismatch.

matdurand commented 1 year ago

Watch out because the init method also return cookies, including the callback url cookie. You need to merge the cookies returned by both the init and getAuthorizationUrl, and return them to the browser. My solution looks like this:

import { getCsrfToken } from 'next-auth/react';
import { init } from '../../../../node_modules/next-auth/core/init'; // You have to import it like this
import getAuthorizationUrl from '../../../../node_modules/next-auth/core/lib/oauth/authorization-url';
import { setCookie } from '../../../../node_modules/next-auth/next/utils';
import type { NextAuthOptions } from 'next-auth';
import { getServerSession } from 'next-auth';
import { GetServerSidePropsContext } from 'next';
import { IncomingMessage } from 'http';
import { NextApiRequestCookies } from 'next/dist/server/api-utils';

async function getServerSignInUrl(
  req: IncomingMessage,
  cookies: NextApiRequestCookies,
  authOptions: NextAuthOptions
) {
  const { options, cookies: initCookies } = await init({
    action: 'signin',
    authOptions,
    isPost: true,
    cookies,
    csrfToken: await getCsrfToken({ req }),
    callbackUrl: req.url,
  });
  const { redirect, cookies: authCookies } = await getAuthorizationUrl({options, query: {} });
  return {
    redirect,
    cookies: [...initCookies, ...authCookies],
  };
}

and to call it

    const redirect = await getServerSignInUrl(
      context.req,
      context.req.cookies,
      authOptions // your authOptions
    );
    redirect.cookies?.forEach((cookie) => setCookie(context.res, cookie));
    context.res.writeHead(302, { Location: redirect.redirect });
    context.res.end();
72L commented 1 year ago

Does anyone have an example for next.js app/ router?

I am using an external authentication provider (not oauth). The user lands on my server with a code that verifies they are authenticated. Now I want to redirect them to a page that gives them a next-auth session automatically.

Or perhaps I can set a cookie directly?

72L commented 1 year ago

From my experience trying to hack this together using the above comments, this issue does not seem closed? Seems like a lot of people have this need.

lucioreyli commented 1 year ago

I think a alternative using (the new) Server Actions can be a good approach to do server side sign-in, since we can pass any data from client side to server side, including redirects.

rinvii commented 1 year ago

Does anyone have an example for next.js app/ router?

I am using an external authentication provider (not oauth). The user lands on my server with a code that verifies they are authenticated. Now I want to redirect them to a page that gives them a next-auth session automatically.

Or perhaps I can set a cookie directly?

Here's how I do server sign in by just copying what next-auth does for the signIn HTTP request except I do it on the server.

You can redirect to give them a session token if you want, but you can also just set the cookie in a response which is what I did.

I'm using tRPC, but I imagine you can simply change it to set cookies using Next.js API Route (pages router api), or using the cookies function (app router api).

import { stringify } from "qs";

...

// we need a csrf token and a callback url as cookies for the credentials endpoint
// this csrf endpoint returns both of them
const csrfApiResponse = await fetch(`${ env.NEXTAUTH_URL }/api/auth/csrf`);
const csrfSetCookiesWithOptions =
  await csrfApiResponse.headers.getSetCookie();
const setCookiesArray = [...csrfSetCookiesWithOptions];
const setCookiesKeyValue = setCookiesArray
  .map((cookie) => cookie.split(";")[0]) // we only want the key value pair, not the options
  .join("; ");
const csrfAuthToken: string = (await csrfApiResponse.json()).csrfToken;
const credentialsResponse = await fetch(
  `${env.NEXTAUTH_URL}/api/auth/callback/credentials?`,
  {
    headers: {
      "content-type": "application/x-www-form-urlencoded",
      cookie: setCookiesKeyValue,
    },
    body: stringify({
      email: "user@example.com",
      totpCode, // optional
      callbackUrl: "/",
      redirect: "false",
      csrfToken: csrfAuthToken,
      json: true,
    }),
    method: "POST",
  },
);

if (credentialsResponse.status !== 200) {
  throw new TRPCError({
    code: "INTERNAL_SERVER_ERROR",
    message: "Failed to authenticate sign in attempt",
  });
}

setCookiesArray.push(
  ...credentialsResponse.headers
    .getSetCookie()
    .filter((cookie) => cookie.includes("next-auth.session-token")),
);

for (const cookie of setCookiesArray) {
  opts.ctx.resHeaders?.append("Set-Cookie", cookie);
}
mnemitz commented 6 months ago

Would love to see a version of @petdomaa100's workaround integrated into NextAuth.

mrbianchi commented 5 months ago

Would love to see a version of @petdomaa100's workaround integrated into NextAuth.

yes please, looks impossible to me to pass a parameter in the email provider in order to achieve a simple recaptcha validation

leog commented 4 months ago

Just in case it helps someone, here is a React Server component for which you can configure Next-Auth to point to using custom pages, in order to skip the sign in page altogether: https://gist.github.com/leog/8f713ba75e83d1ddd220455e3a89ee0c

Yarmeli commented 1 month ago

For anyone using OAuth and wanting to skip the default Sign-In page, this is what I did.

Ideally, I'd like to skip the last step and directly call something like redirect(SignInURL("discord", { callback })) in checkAuth, but this is good enough:

Uses:

  1. Setup a way to get the callbackUrl:
    
    // middleware.ts or src/middleware.ts

import { NextResponse } from "next/server"; import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) { // Add a new header x-current-path which passes the path to downstream components const headers = new Headers(request.headers); headers.set( "x-current-path", request.nextUrl.pathname + // e.g. "/account" request.nextUrl.search // e.g. "?name=foo&action=bar ); return NextResponse.next({ headers }); }

export const config = { matcher: [ // match all routes except static files and APIs "/((?!api|_next/static|_next/image|favicon.ico).*)", ], };


2. Create a function that will check if the user is logged in and redirect to `/api/auth/login` if needed
```ts
export const checkAuth = async (callbackUrl?: string | null) => {
    const session = await getServerSession(authOptions);
    if (!session) {
        const url =
            "/api/auth/login" + // custom page that will run "signIn" on the client side
            (callbackUrl ? `?callbackUrl=${callbackUrl}` : "");
        redirect(url);
    }
};

2.5 Use the checkAuth function inside a page/layout:

export default async function AppLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    const headerList = headers();
    const pathname = headerList.get("x-current-path");
    await checkAuth(pathname);
    // only logged in users should be allowed to see any of this
    return (
        <main>
            <SidebarLayout>{children}</SidebarLayout>
        </main>
    );
}
  1. Create /api/auth/login/page.tsx
    • Why create a custom page instead of using the default one?
    • The default Next-Auth login page still exists (/api/auth/signin) and you can still:
      • Handle multiple providers
      • Have a custom sign-in page
      • Handle any errors and not be stuck in an infinite loop
"use client";
import { signIn } from "next-auth/react";

export default function Page({
    searchParams,
}: {
    searchParams?: Record<string, string | string[] | undefined>;
}) {
    const callbackUrl = (searchParams?.callbackUrl as string) ?? "/";

    // Client-Component --- immediately call the signIn flow
    // The user won't accidentally navigate to this page sooooo 
    // we can safely start the signIn flow using this endpoint
    void signIn("discord", { callbackUrl });
}
kentmichael commented 2 days ago

Does anyone have an example for next.js app/ router? I am using an external authentication provider (not oauth). The user lands on my server with a code that verifies they are authenticated. Now I want to redirect them to a page that gives them a next-auth session automatically. Or perhaps I can set a cookie directly?

Here's how I do server sign in by just copying what next-auth does for the signIn HTTP request except I do it on the server.

You can redirect to give them a session token if you want, but you can also just set the cookie in a response which is what I did.

I'm using tRPC, but I imagine you can simply change it to set cookies using Next.js API Route (pages router api), or using the cookies function (app router api).

import { stringify } from "qs";

...

// we need a csrf token and a callback url as cookies for the credentials endpoint
// this csrf endpoint returns both of them
const csrfApiResponse = await fetch(`${ env.NEXTAUTH_URL }/api/auth/csrf`);
const csrfSetCookiesWithOptions =
  await csrfApiResponse.headers.getSetCookie();
const setCookiesArray = [...csrfSetCookiesWithOptions];
const setCookiesKeyValue = setCookiesArray
  .map((cookie) => cookie.split(";")[0]) // we only want the key value pair, not the options
  .join("; ");
const csrfAuthToken: string = (await csrfApiResponse.json()).csrfToken;
const credentialsResponse = await fetch(
  `${env.NEXTAUTH_URL}/api/auth/callback/credentials?`,
  {
    headers: {
      "content-type": "application/x-www-form-urlencoded",
      cookie: setCookiesKeyValue,
    },
    body: stringify({
      email: "user@example.com",
      totpCode, // optional
      callbackUrl: "/",
      redirect: "false",
      csrfToken: csrfAuthToken,
      json: true,
    }),
    method: "POST",
  },
);

if (credentialsResponse.status !== 200) {
  throw new TRPCError({
    code: "INTERNAL_SERVER_ERROR",
    message: "Failed to authenticate sign in attempt",
  });
}

setCookiesArray.push(
  ...credentialsResponse.headers
    .getSetCookie()
    .filter((cookie) => cookie.includes("next-auth.session-token")),
);

for (const cookie of setCookiesArray) {
  opts.ctx.resHeaders?.append("Set-Cookie", cookie);
}

I test this out and it works! This called the JWT callback, but the Session callback is not firing, thus not saving the session..