Next.js Discord

Discord Forum

Need help with authentication with another api

Unanswered
Pacific herring posted this in #help-forum
Open in Discord
Pacific herringOP
I have a backend in NeStjs and it returns a JWT. I am able to get the JWT returned via logging into that api but not sure what to do next. I know I can store it in localstorage but that's not very ideal. I know we can store cookies.. However every tutorial I have found have mainly used either some oAuth or internal next.js authentication (if that make sense). Just looking to know whats the ideal way and if someone can point me to a resource? I am guessing I use a middleware to check if the user is logged in or not? For what its worth, I am building an admin dashboard

142 Replies

So JWT's are intended to be stored in cookies. Anything exposed to the client side is not secure totally so regardless of where you store it youll have the same conundrum. I have rolled my own auth in a large org to do exactly what your doing any and I used cookies, as long as your signing your JWT properly cookies is the way to go
Pacific herringOP
Yeah JWT is signed properly from the backend api
Your middleware can check the cookie, see if it exsists, if not forward you to whatever other auth method you have.
Pacific herringOP
But I am just lost as to waht to do next .. let me show some code snippet
excuse my probably horrible code
So what you will want to do is from that API your authing to return a cookie and set it on the browser
Pacific herringOP
so the backend returns a cookie?
Yep! One sec lemme grab an example
Pacific herringOP
sure
on a side note i am also new to nest.js .. but thats whole another issue
Ive never used it lol
Pacific herringOP
I have been doing python and ruby for like 10+ years.. and using js for frontend only (react/vue, vanilla js, angular, backbone, etc)
yeah this is a project i took on to help a friend and i figured its a good way to learn. their backend is in nest.js
if this is server action, you can use nextjs's cookies function
Pacific herringOP
yes it is a server action
so i should have that in my login function?
yeah
export async function POST(request) {
    const data = await request.formData();
    let samlResponse = data.get('SAMLResponse'); //Get Saml Response from form data

    //Decode Saml response to readable stream
    const samlXML = base64DecodeToString(samlResponse);
    const validSAMLSignature = validateSAMLSignature(samlXML, samlPublicKey)


    //Calculating users access based on groups
    let access, authToken;
    try {
        access = await calculateUserAccess(user.userID, user.Groups)
        access = {
            roles: access.roles,
            views: access.views,
            homePage: access.homePage,
            userid: user.userID,
            firstName: user.FirstName,
            lastName: user.LastName,
            snowSupportGroup: {}
        }
        if (access.roles.some(role => role !== 'enduser')) {
            const snowDetails = await getServiceNowUserDetails(user.userID);
            access.snowSupportGroup = snowDetails;
        }

        authToken = await generateJWTToken({ access: access })
    }
    catch (error) {
        await logAuthentication(user.userID, 'Failed-10')
        return Response.redirect(`${process.env.LocalURL}/login/failed?reason=Error while reading authentication request&code=10`);
    }

    //Providing relevant cookies to browser
    try {
        cookies().set('bciamwp-auth', authToken, {
            httpOnly: true,
            path: "/",
            maxAge: 60 * process.env.JwtTimeToLive * 60,
            sameSite: 'strict',
            secure: true
        })

        cookies().set('bciamwp-nav', JSON.stringify({ access: access }), {
            httpOnly: false,
            path: "/",
            maxAge: 60 * process.env.JwtTimeToLive * 60,
            sameSite: 'strict',
            secure: true
        });
        await logAuthentication(user.userID, 'Success')
    }
    catch (error) {
        await logAuthentication(user.userID, 'Failed-11')
        return Response.redirect(`${process.env.LocalURL}/login/failed?reason=Error while reading authentication request&code=11`);
    }

    return Response.redirect(`${process.env.HttpsURL}/login/postlogin`);
}
I figure more is better here, but this is somewhat of how I have it setup
This isnt using server actions BUT the relevant snippet here is

        cookies().set('bciamwp-auth', authToken, {
            httpOnly: true,
            path: "/",
            maxAge: 60 * process.env.JwtTimeToLive * 60,
            sameSite: 'strict',
            secure: true
        })
Pacific herringOP
so id set jwt as the name and teh value would be the data.accessToken
@Jboncz js export async function POST(request) { const data = await request.formData(); let samlResponse = data.get('SAMLResponse'); //Get Saml Response from form data //Decode Saml response to readable stream const samlXML = base64DecodeToString(samlResponse); const validSAMLSignature = validateSAMLSignature(samlXML, samlPublicKey) //Calculating users access based on groups let access, authToken; try { access = await calculateUserAccess(user.userID, user.Groups) access = { roles: access.roles, views: access.views, homePage: access.homePage, userid: user.userID, firstName: user.FirstName, lastName: user.LastName, snowSupportGroup: {} } if (access.roles.some(role => role !== 'enduser')) { const snowDetails = await getServiceNowUserDetails(user.userID); access.snowSupportGroup = snowDetails; } authToken = await generateJWTToken({ access: access }) } catch (error) { await logAuthentication(user.userID, 'Failed-10') return Response.redirect(`${process.env.LocalURL}/login/failed?reason=Error while reading authentication request&code=10`); } //Providing relevant cookies to browser try { cookies().set('bciamwp-auth', authToken, { httpOnly: true, path: "/", maxAge: 60 * process.env.JwtTimeToLive * 60, sameSite: 'strict', secure: true }) cookies().set('bciamwp-nav', JSON.stringify({ access: access }), { httpOnly: false, path: "/", maxAge: 60 * process.env.JwtTimeToLive * 60, sameSite: 'strict', secure: true }); await logAuthentication(user.userID, 'Success') } catch (error) { await logAuthentication(user.userID, 'Failed-11') return Response.redirect(`${process.env.LocalURL}/login/failed?reason=Error while reading authentication request&code=11`); } return Response.redirect(`${process.env.HttpsURL}/login/postlogin`); }
me when i have chaos to support username+password and passkey

https://github.com/RiskyMH/EmailThing/blob/main/app/(auth)/login/action.ts
I wouldnt set it to jwt 😂
I issue two tokens... one is an auth token and one is a unencoded profile token (which just drives visuals and isnt used for authorization of course)
Pacific herringOP
@riský your code logs in directly from next.js though right?
like decode the jwt and use the name, email, etc?
tomato tomato though
Pacific herringOP
and its okay to set the cookie in that action and wont be an issue?
Yerp
Pacific herringOP
i just want to know best practices really
@Pacific herring <@657067112434499595> your code logs in directly from next.js though right?
wdym? i just use server actions and some madness
Pacific herringOP
okay let me try
const user = await db.query.User.findFirst({ where: eq(User.username, parsedData.data.username),
Then your middleware you will want to define either

Authenticated Routes or Unauthenticated routes and either allow or deny depending on which you go with.
my middleware is a little messy.... lol
Pacific herringOP
gotcha
but the important parts are
    if (mw_IsNoAuthRoute(requestedPath) == true) {
        return NextResponse.next();
    }


export const config = {
    matcher: '/((?!api|_next|static|public|favicon.ico).*)'
}
Pacific herringOP
do you use useEffect in next? i havent seen much use of it
@Jboncz This isnt using server actions BUT the relevant snippet here is js cookies().set('bciamwp-auth', authToken, { httpOnly: true, path: "/", maxAge: 60 * process.env.JwtTimeToLive * 60, sameSite: 'strict', secure: true })
samesite strict is most of the time good, i just got annoyed when using google and it kept on not being signed in (as intended by spec)
@Pacific herring do you use useEffect in next? i havent seen much use of it
You can, though dont overuse it 😄
Pacific herringOP
I have pretty solid experience with just react/redux
@riský samesite strict is most of the time good, i just got annoyed when using google and it kept on not being signed in (as intended by spec)
Yeah, since its internal to the network, I dont have to worry about most of that
Pacific herringOP
okay let me try soemthing quickly
meh, just something I never noticed. More than just me in this code base 😂
Pacific herringOP
so what would be my return for that login function?
Uhhhh im assuming you wanna redirect after they login?
Pacific herringOP
oh okay so no return just a redirect
got it
this what i have now
https://nextjs.org/docs/app/api-reference/functions/redirect

You wanna do the following
1. set cookie
2. redirect to 'home' or if failed return to failed auth or something
3. return nothing;
Did you import the cookies module?
Pacific herringOP
yes
Weird, no syntax highlighting
Pacific herringOP
i guess i need to remove sameSite?
also for the love of god, please use your semi colons 😄
Pacific herringOP
oh which extension should i use?
It should just do it if your in vscode
import { cookies } from 'next/headers'
Pacific herringOP
sorry i am so used to ruby/python i rarely if ever use ;
yeah its imported
I get it I get it, just a pet peeve of mine
After that you can just do return
Pacific herringOP
if its not then it would give me an error
and you should see a cookie after you login
Pacific herringOP
Even on server actions im not returning anything I ussually still at the very least return a true or false, but thats just me. Even if im redirecting away, idk why
Pacific herringOP
something like that?
yerp
Pacific herringOP
okay let me try
I would probably do
redirect('/dashboard', 'push')


so it adds to the history stack
Pacific herringOP
got it
okay let me try running this
one second
So remember server actions are just rest calls like anything else. so saying redirect just returns a 307 to the client and redirects you, whatever you return from server actions have to be searlizable as well.
Im about to go lay down for bed again so if you run into issues I wont be able to help much but I can help more in the morn
Pacific herringOP
lol the backend api is down.. nice. perfect time..but okay i think this was helpful
thanks so much
😂
Pacific herringOP
really appreciate it
Np
Original message was deleted
Dont forget to close when you do test it, using the replied message instructions.... also please do let me know if that all worked out well for you. Nothing worse than helping and then getting ghosted and not knowing if you got it figured out
Pacific herringOP
trust me. i have helped and mentored many people before and i know how you feel
i will definitely update here.. its late here too so maybe tomorrow but lets see
Np I’m off to bed now gl and look forward to hearing if it worked out.
Pacific herringOP
thank you
both you and @riský really appreciate the help and the lack of condescending comments that a lot of people make when you ask for basic help
Damn next time I’ll have to remember to at least be a little condescending
Pacific herringOP
hey @Jboncz / @riský so it is setting the jwt token in cookies now.. thanks for the help
i now created a middleware file to check if the jwt exists, if not redirect.. i guess thats the next logical step?
is it okay to grab the token from the cookie every time or should we have a global state for the jwt?
I personally grab the cookie everytime.
Pacific herringOP
gotcha
Are you self hosting?
Pacific herringOP
what do you mean by self hosting?
Are you hosting on vercel or something or your own hardware/work hardware
Pacific herringOP
oh right now just locally
but the actual applicaiton will be on aws
the backend is on aws (but i have a local copy running)
Gotcha, thats okay, as long as your not hosting on vercel in a serverless environment.
Pacific herringOP
got it
import { cookies } from 'next/headers'

export async function middleware(request, res) {
    const cookieStore = cookies();

    const authenticationCookie = cookieStore.get(process.env.AuthCookieName);
    const navCookie = cookieStore.get(process.env.NavCookieName);
}
Pacific herringOP
and hten grab the .value?
si
JSON.parse(navCookie.value)
Pacific herringOP
yeah
cool
next dumb question.. how do i use that value in other actions? etc
do i just grab it again?
validating the cookie in middleware is a bit more of a challenge. Middleware runs on the edge, so you either have to use a library thats edge compatible like jose, or you will have to fetch from a backend route to validate it.
You could rely on your auth endpoint to do validation but then you run into having to make a network request to validate.
Pacific herringOP
backend validates it
this app will be used by one person.. so im not super worried about bunch of these things yet
Right so I navigate to /admin, middleware sends a http request to your backend to validate the cookie?
Ahhh okay.
@Pacific herring do i just grab it again?
I grab it each time I want to validate something or use it for authorization for something.
I dont ever cache the cookie on the backend.
Pacific herringOP
so youd grab it again in the actions?
so for context im building an admin dashboard
since we dont directly connect to the db, we make request to the backend for stats, and other functionalities
lets say i have an action called createUser, in that action id grab the cookie value again and send it?
Absolutely yes, every server action I have a wrapper to check for authentication and or access to that specific function.
lemme grab it sec
Pacific herringOP
ok sure
everytime I call a backend rest api I use

export async function validateJWTToken(token) {
    try {
        if (!token) {
            token = getAuthenticationCookie();

            if (!token) {
                throw 'Authentication cookie not present';
            }
        }
        const publicKey = await importSPKI(jwtPublicKey, 'RS256');

        //Verify the token
        const { payload, protectedHeader } = await jwtVerify(token, publicKey);

        //Check additional claims
        const { alg, type, } = protectedHeader;

        if (alg !== 'RS256' || type !== 'JWT' || payload.exp < Date.now() / 1000) {
            throw new Error('Invalid token claims');
        }

        return payload;
    }
    catch (error) {
        return false;
    }
}


then let it go through. I have route based protection through middleware so I already know they are allowed to be on that page, I still validate the token on any frontend to backend request.
Pacific herringOP
gotcha
There might be better ways to do it.... this is just my implementation.
Pacific herringOP
ill try to understand the code a bit better
i wonder if there is a way to run middleware validation just once for certain time
or if thats a safe enough practice
You would have to cache the session on the backend,
you can.
its acceptable, I just didnt wanna do anything like that on the backend.
Pacific herringOP
gotcha
Kinda defeats the purpose of JWT based auth
Pacific herringOP
cool, thanks for the help. let me go ahead and do some more work on it and ill update
np
I would start off with the following.... create helper functions for
getAuthCookie()
validateAuthCookie()
generateAuthCookie()
mw_validateAuthhcookie()
With those functions you can essentially go anywhere.
The reason you need a seperate function for mw validation is because mw runs on the edge and not with full nodejs api