Is this the correct and secure way to connect Next.js + NextAuth with a Django Rest Framework API? #1350
Replies: 11 comments 12 replies
-
Did you get anywhere with this? I am also interested! |
Beta Was this translation helpful? Give feedback.
-
I am also interested in the best way to integrate |
Beta Was this translation helpful? Give feedback.
-
Here’s some related information https://arunoda.me/blog/add-auth-support-to-a-next-js-app-with-a-custom-backend but I don’t have a clear answer Also, another Q&A about this topic that was unanswered #1226 |
Beta Was this translation helpful? Give feedback.
-
Hello, OP here. Sorry for the delay in response. I figured a better way to integrate I wrote an article here, the associated code can be found at here, under the branch named Assuming you have done everything mentioned in the article, (or you could clone the part-1 branch of the given repo and set it up), you create a separate file export namespace NextAuthUtils {
export const checkAccessTokenValidity = async function (accessToken) {
try {
const response = await axios.post(
"http://localhost:8000/api/auth/token/verify/",
{
token: accessToken,
},
);
return true;
} catch {
return false;
}
};
export const refreshToken = async function (refreshToken) {
try {
const response = await axios.post(
"http://localhost:8000/api/auth/token/refresh/",
{
refresh: refreshToken,
},
);
const { access, refresh } = response.data;
return [access, refresh];
} catch {
return [null, null];
}
};
} Then, in the const settings: InitOptions = {
secret: process.env.SESSION_SECRET,
session: {
jwt: true,
maxAge: 24 * 60 * 60, // 24 hours
},
jwt: {
secret: process.env.JWT_SECRET,
},
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
async signIn(user: AuthenticatedUser, account, profile) {
// may have to switch it up a bit for other providers
if (account.provider === "google") {
// extract these two tokens
const { accessToken, idToken } = account;
// make a POST request to the DRF backend
try {
const response = await axios.post(
// tip: use a seperate .ts file or json file to store such URL endpoints, or better, an environment variable
"http://127.0.0.1:8000/api/social/login/google/",
{
access_token: accessToken, // note the differences in key and value variable names
id_token: idToken,
},
);
// extract the returned token from the DRF backend and add it to the `user` object
const { access_token, refresh_token } = response.data;
user.accessToken = access_token;
user.refreshToken = refresh_token;
return true; // return true if everything went well
} catch (error) {
return false;
}
}
return false;
},
async jwt(token, user: AuthenticatedUser, account, profile, isNewUser) {
if (user) {
const { accessToken, refreshToken } = user;
// reform the `token` object from the access token we appended to the `user` object
token = {
...token,
accessToken,
refreshToken,
};
// remove the tokens from the user objects just so that we don't leak it somehow
delete user.accessToken;
delete user.refreshToken;
return token;
}
// cheking session validity, check if the token is still valid
const accessTokenValid = await NextAuthUtils.checkAccessTokenValidity(
token.accessToken,
);
// token has been invalidated, try refreshing it
if (!accessTokenValid) {
const [
newAccessToken,
newRefreshToken,
] = await NextAuthUtils.refreshToken(token.refreshToken);
if (newAccessToken && newRefreshToken) {
token = {
...token,
accessToken: newAccessToken,
refreshToken: newRefreshToken,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000 + 2 * 60 * 60),
};
return token;
}
// unable to refresh tokens from DRF backend, invalidate the token
return {
...token,
exp: 0,
};
}
// token valid
return token;
},
},
};
export default (req: NextApiRequest, res: NextApiResponse) =>
NextAuth(req, res, settings); Next, in your import { Provider as AuthProvider } from "next-auth/client";
import { AppProps } from "next/app";
import React from "react";
function MyApp({ Component, pageProps }: AppProps) {
return (
<AuthProvider
session={pageProps.session}
{ /* these values can change depending on the `ACCESS_TOKEN_LIFETIME` and `REFRESH_TOKEN_LIFETIME` in your django `settings.py` */ }
options={{
clientMaxAge: 5 * 60,
keepAlive: 3 * 60,
}}
>
<Component {...pageProps} />
</AuthProvider>
);
}
export default MyApp; This works, but with 2 caveats:
Let me know about your suggestions/criticisms. Thanks and have a nice day. |
Beta Was this translation helpful? Give feedback.
-
Small suggestions: the way how you patch user in the validity check of the access token shouldn't happen on each invocation of the jwt callback either, as it would contact the backend more often than needed. get hold of the expiry time of the access token, and add it to the check validity function. if still not expired, you could simply assume it is still valid without a backend call otherwise looking good! |
Beta Was this translation helpful? Give feedback.
-
How can I make this support authentication with credentials (username or email + password)? We also need to get the other user information that is associated with Django's User. |
Beta Was this translation helpful? Give feedback.
-
random, unrelated question... i was using |
Beta Was this translation helpful? Give feedback.
-
So is the code working with DRF or not?😅 |
Beta Was this translation helpful? Give feedback.
-
I've been stuck on this a couple of weeks now. Anyone found other alternatives besides next-auth? |
Beta Was this translation helpful? Give feedback.
-
Hello friends
/* eslint-disable no-undef */
import jwtDecode from "jwt-decode"
import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import axiosInstance from "../../../src/axios"
async function refreshAccessToken(token) {
try {
const res = await axiosInstance.post("/token/refresh/", {
refresh: token.refresh,
})
return {
...token,
access: res.data.access,
refresh: res.data.refresh,
}
} catch (error) {
console.error(error)
return {
...token,
error: "RefreshAccessTokenError",
}
}
}
export const authOptions = {
// Configure one or more authentication providers
providers: [
CredentialsProvider({
name: "Credentials",
async authorize(credentials) {
// Add logic here to look up the user from the credentials supplied
const { otp, phone } = credentials
try {
const res = await axiosInstance.post("token/", {
otp: otp,
phone: phone,
})
return res.data
} catch (error) {
return null
}
},
}),
],
secret: process.env.JWT_SECRET,
pages: {
signIn: "/login",
},
callbacks: {
async jwt({ token, account, user }) {
if (user && account) {
return {
...token,
access: user.access,
refresh: user.refresh,
is: user.is,
user: user.user,
_ag: user._ag,
_mI: user._mI,
_uI: user._uI,
}
}
const tokenParts = jwtDecode(token.access)
if (Date.now() < tokenParts.exp * 1000) {
console.log("EXISTING ACCESS TOKEN IS VALID")
return token
}
console.log("ACCESS TOKEN HAS EXPIRED, REFRESHING...")
return await refreshAccessToken(token)
},
async session({ session, token }) {
session.access = token.access
session.refresh = token.refresh
session.is = token.is
session.user = token.user
session._ag = token._ag
session._mI = token._mI
session._uI = token._uI
return session
},
},
}
export default NextAuth(authOptions) my login page import { signIn } from "next-auth/react"
....
const handleSubmited = (data) => {
if (timer > 0) {
setloading(true)
signIn("credentials", {
phone: data.phone,
otp: data.otp,
callbackUrl: '/panel',
})
}
}
...
/* eslint-disable no-undef */
import { getToken } from "next-auth/jwt"
import { NextResponse } from "next/server"
export async function middleware(req) {
const token = await getToken({ req, secret: process.env.JWT_SECRET })
const { pathname, search } = req.nextUrl
if (pathname.includes("/api/auth") || (token && pathname !== "/login")) {
return NextResponse.next()
}
if (token && pathname === "/login") {
return NextResponse.redirect("/panel")
}
if (!token && pathname !== "/login") {
return NextResponse.redirect("/login")
}
}
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
const getLayout = Component.getLayout || ((page) => page)
return (
<SessionProvider session={session}>
...
{getLayout(<Component {...pageProps} />)}
...
</SessionProvider>
)
} If you want to use the rendering server feature in the pages section, do the following: import { authOptions } from "../api/auth/[...nextauth]"
import { getServerSession } from "next-auth"
import { getSession } from "next-auth/react"
export async function getServerSideProps(context) {
const { access, _ag } = await getSession(context) ❌
const { access, _ag } = await getServerSession(context, authOptions) ✅
const headers = {
Authorization: `Bearer ${access}`,
"Content-Type": "application/json",
accept: "application/json",
}
const res = await axios.get(
`${process.env.BACKEND_URL}/api/full-location/${_ag}/`,
{
headers: headers,
}
)
const location = await res.data
return {
props: { location: location, ag: _ag },
}
} If you use the first method, I do not know why it uses the old token when refreshing the token. And that caused me a lot of trouble. |
Beta Was this translation helpful? Give feedback.
-
One idea is to put your server in front of Next.JS as a reverse-proxy. This is a Gin server, but same idea: |
Beta Was this translation helpful? Give feedback.
-
Your question
I have been working on a Next.js app with a custom backend using Django Rest Framework, with the main focus on authentication with social platforms (Google, Github etc). My question is whether the way I am doing it is secure and will not cause security issues.
What are you trying to do
I am trying to make my Next.js app interact securely with a Django Rest Framework backend. Here's the flow I am wanting to use:
dj-rest-auth
anddjango-allauth
setup to handle social authentication.Reproduction
Here's the code for context:
index.tsx
api/auth/[...nextauth].ts
Feedback
Documentation refers to searching through online documentation, code comments and issue history. The example project refers to next-auth-example.
Beta Was this translation helpful? Give feedback.
All reactions