Vyhledávání na webu

typescript - NextAuth v API routách: unstable_getServerSession a getToken vždy vrací null


Snažím se použít unstable_getServerSession v API routě nextJS (dříve jsem používal vlastní metodu autentizace). Možná mám špatné nastavení s JWT? Možná už pro mě není jasný celek a vidím jen detaily.

Řídil jsem se tutoriálem "Zabezpečení API cest": pages/api/get-session-example.js https://next-auth.js.org/tutorials/securing-pages-and-api-routes#securing-api-routes

  • ve funkci getServerSideProps volám svůj Next/API endpoint přes Axios
  • Předávám Bearer token do dalšího volání API
  • API endpoint by měl ověřit, že uživatel má povolení jej volat pomocí JWT
  • API endpoint by měl poté načíst předplatná Stripe na základě stripe_customer_id vloženého v JWT

Ale ať už cokoliv zkouším, session a token na dalším API koncovém bodě jsou vždy null

Toto je moje funkce getServerSideProps:

export const getServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext) => {

    // ### contains the correct SESSION (it works!)
    const session = await unstable_getServerSession(context.req, context.res, authOptions)

    // ### contains the correct TOKEN (it works!)
    const token = await getToken(context).then(token => {
            return token.token as string; 
        })
    if (!session) {
        return {
            redirect: {
                permanent: false,
                destination: url.accountSignIn().href,
            },
        };
    }
    else {
        // My SESSION IS AVAILABLE (I use this to show user data.
        // #### NOW I CALL THE NEXT/API endpoint with the getToken() response
        const stripeSubscriptions = await axios.get(`${process.env.NEXT_PUBLIC_URL}/api/account/subscriptions`, { headers: { 'Authorization': 'Bearer ' + token } }).then(res => {
            console.log('res.data :>> ', res.data);
            const subscriptions = res.data.data as Stripe.Subscription[]
            return subscriptions
        }).catch((err: any) => {
            console.log("Dashboard ERR", err.response.data.message || err); // #### I ALWAYS END with {
  error: "Unauthorized",  message: "not authorized",  data: []}
  
            signOut({ callbackUrl: 'http://localhost:8000/account/login' })
            return err.response.data

        });

        return {
            props: { stripeSubscriptions, session }, // will be passed to the page component as props
        };
    }

Toto je můj další API endpoint /api/account/subscriptions

import { NextApiRequest, NextApiResponse } from 'next';
import Stripe from 'stripe';

const secret = process.env.NEXTAUTH_SECRET // #### is correct
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "../../api/auth/[...nextauth]"
import { getToken } from "next-auth/jwt"

export default async (req: NextApiRequest, res: NextApiResponse) => {

    // #### the header definitely has the Bearer token in it's request: req.headers.authorization: `"Bearer eyJhbGciOiJIUzI1NiJ9......"`
    const session = await unstable_getServerSession(req, res, authOptions)  // ### ALWAYS RETURNS "NULL"
    const token = await getToken({ req, secret }) // ### ALWAYS RETURNS "NULL"

    if (session) { // ### ALWAYS "NULL"

        return new Promise<void>((resolve) => {
            const customer_id = session.stripe_id as string; // #### should get extracted from JWT
            const stripe = new Stripe(process.env.STRIPE_API_SECRET!, { apiVersion: '2020-08-27' });

            stripe.subscriptions.list({ customer: customer_id }).then(subscriptions => {
                res.status(200).json({ data: subscriptions });
                return resolve();
            }).catch(err => {
                res.status(500).json({ error: `Stripe Subscriptions error: ${err.message}` });
                return resolve();
            }
            );
        })
    } else {
        // Not Signed in
        // #### I ALWAYS END HERE
        res.status(401).json({ error: 'Unauthorized', message: "not authorized", data: [] });
    }
    res.end()

a zde je můj KOMPLETNÍ soubor [...nextauth].ts:

Musím zmínit, že backend v Rails vytváří JWT.

import jwtDecode from 'jwt-decode';
import NextAuth from 'next-auth';
import type { NextAuthOptions } from 'next-auth';

import ProviderCredentials from 'next-auth/providers/credentials';
import { JWT } from 'next-auth/jwt';

export const authOptions: NextAuthOptions = {
    // Configure one or more authentication providers
    providers: [
        ProviderCredentials({
            id: 'credentials',
            name: 'credentials',
            credentials: {
                username: {
                    label: 'Email',
                    type: 'email',
                    placeholder: '[email ',
                },
                password: { label: 'Passwort', type: 'password' },
            },
            async authorize(credentials, req) {
                // The 'url' is pointing to a Rails API endpoint which returns a JWT Token, and account data
                const url = `${process.env.NEXT_PUBLIC_API_URL}/auth/login`;

                const res = await fetch(url, {
                    method: 'POST',
                    body: JSON.stringify(credentials),
                    headers: {
                        'Content-Type': 'application/json',
                    },
                });
                const user = await res.json();
                // If no error and we have user data, return it
                console.log('user authorize :>> ', user);
                if (user && (user.error || user.message)) {
                    throw new Error(user.message || user.error);
                }

                if (res.ok && user) {
                    return user; // MY CONTENT
                }
                // Return null if user data could not be retrieved
                return null;
            },
        }),
    ],
    session: {
        strategy: 'jwt',
    },
    // callback is where we decide whether the token is ready to be refreshed.
    jwt: {
        secret: process.env.NEXTAUTH_SECRET,
    },
    secret: process.env.NEXTAUTH_SECRET,
    callbacks: {
        // This callback is called whenever a JSON Web Token is created (i.e. at sign in)
        // or updated (i.e whenever a session is accessed in the client).
        // The returned value will be signed and optionally encrypted,
        // and it is stored in a cookie

        // The arguments user, account, profile and isNewUser are only passed the first time
        // this callback is called on a new session, after the user signs in.
        // In subsequent calls, only token will be available.
        async jwt(props) {
            var { token, user, account, profile, isNewUser } = props
            console.log('JWT: props :>> ', props);
            // This user return by provider {} as you mentioned above MY CONTENT {token:}
            if (user) {
                if (user.token) {
                    var token = {
                        token: user.token,
                        name: user.name,
                        email: user.email,
                        shop: user.shop,
                        stripeId: user.stripe_id,
                        billingAddress: user.billingAddress,
                    };
                }
            }
            return token;
        },

        // controls if a user is allowed to sign in.
        // user object is the response returned from the authorize callback
        async signIn(props) {
            const { user, account } = props

            console.log('signIn: props :>> ', props);
            return true;
        },

        // signs out the user
        // user object is the response returned from the authorize callback
        async signOut({ token, session }) {
            console.log('signOUT token :>> ', token);
            console.log('signOUT session :>> ', session);
            return true;
        },
        // session callback is where we specify what will be available on the client with useSession() or getSession().
        // The session callback is called WHENEVER a SESSION IS CHECKED.
        // By default, only a subset of the TOKEN is returned for increased security.
        // If you want to make something available you ADDED TO THE TOKEN through the jwt() callback,
        // you have to explicitly forward it here to make it available to the client.
        async session(props) {
            const { session, token, user } = props;
            console.log('session: props :>> ', props);
            // first time jwt callback is run, user object is available
            if (token) {
                session.user = token;
            }
            //if you want to add user details info
            return session;
        },

        redirect({ url, baseUrl }) {
            if (url.startsWith(baseUrl)) return url;
            // Allows relative callback URLs
            else if (url.startsWith('/')) return new URL(url, baseUrl).toString();
            return baseUrl;
        },
    },

    pages: {
        signIn: 'account/login',
        // signOut: 'account/logout',
        // error: '/auth/error', // Error code `passed` in query string as ?error=
        // verifyRequest: '/auth/verify-request', // (used for check email message)
        // newUser: '/auth/new-user', // New users will be directed here on first sign in (leave the property out if not of interest)
    },
    // events: {
    //     async signIn(message) {
    //         console.log('user sign in EVENT called ;:>> ', message);
    //     },
    //     async signOut(message) {
    //         console.log('user sign out EVENT called ;:>> ', message);
    //     },
    // },
};
export default NextAuth(authOptions);

Prosím, pomozte mi, co dělám špatně?

Už šest dní se snažím tento problém vyřešit. (Kopíruji otázku odtud (https://github.com/nextauthjs/next-auth/discussions/5402) na Stackoverflow, protože na Githubu jsem do dneška nedostal žádnou odpověď). Doufám v inteligenci „davu“)

Odpověď č. 1

v zpětném volání JWT

 return Promise.resolve(token)

v zpětném volání relace

return Promise.resolve(session)

autorizovat

if (res.ok && user) {
      return Promise.resolve(user)
  }

v sessions zpětném volání

session.user = token.user