I use jwt tokens to authorize actions and to request protected ressources from my backend.
Frontend: Vue, Vue Router, VueX, Vuetify
Backend: Express, jwt tokens, cookieParser
If I successfully call the login route an accessToken and user data (username, email, userId) will be delivered back to the frontend. Additionally to that I send back an httpOnly cookie (containing the refreshToken) and insert the refreshToken on the other side in my database. Via the vuex store I trigger the state.loggedIn to true and assign the user object to state.user. In the same function I save the user object to my localStorage.
To know If a user should be automatically logged in after reopening a new tab, closing window, etc. I read out the data of the localStorage and initiate the vueX store at the beginning with loggedIn and user.
// read out localStorage for the user
const user = JSON.parse(localStorage.getItem('user'));
const initialState = user
? { user, loggedIn: true, accessToken: null }
: { user: null, loggedIn: false, accessToken: null };
Before a protected route will be called I use the beforeEach method on the vue router object to check if my vueX getter auth/getUser returns a user object. If not, the vue router redirects to the login page, so the user can authorize himself again.
const routes = [
{ path: '/login', name: 'Login', component: Login, meta: { authRequired: false }},
{ path: '/home', name: 'Home', component: Home, meta: { authRequired: true }}
]
const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes });
router.beforeEach(async (to, from, next) => {
if (to.meta.authRequired) {
let user = VuexStore.getters['auth/getUser'];
if (!user) {
next({ path: '/login'});
}
}
next();
});
But, who guarantees that the user object in the localStorage is valid and not just set to true. Is this the way to go, or should I use the received accessToken or the cookie with the refreshToken in any way for the auto login process? Thanks.
Yeah you're almost there. What you're looking for is a refresh token:
https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/#silent_refresh
https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/
Essentially you want to keep track in your localStorage when the current token expires. And then auto-refresh the token in the background when it's about to. So the user isn't auto-logged out.
Edited to add:
But, who guarantees that the user object in the localStorage is valid and not just set to true. Is this the way to go, or should I use the received accessToken or the cookie with the refreshToken in any way for the auto login process? Thanks.
Your API guarantees that. The second that user will try to interact with your API using an invalid access token, your API throws an error. It won't work. So if a user starts messing with client-side data, that's fine. Just never trust it server-side ;-)
Related
I have a NestJS backend that exposes the following API:
#Post('sign-in-with-google-account')
async signInWithGoogleAccount(
#Body body: { idToken: string },
#Res({ passthrough: true }) response: Response
) {
const user = await getUserFromGoogleIdToken(body.idToken)
const tokens = await generateAccessAndRefreshTokensForUser(user)
response.cookie('refreshToken', tokens.refreshToken, {
httpOnly: true,
expires: new Date(tokenExpirationDate),
secure: true,
sameSite: 'none'
})
return { accessToken: tokens.accessToken }
}
It receives id token from google oauth, finds the user in the DB and signs a JWT access token and refresh token. The refresh token is stored as httpOnly cookie and the access token is returned.
Now in my next.js app configured with next-auth I have the following:
import GoogleProvider from "next-auth/providers/google";
...
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET
})
]
...
The problem is that next-auth generates its own tokens. But I want next-auth to use my own access and refresh tokens from the NestJS backend, how can I do that?
Also, In NestJS I have a API to refresh the access token like so:
#Get('refresh-access-token')
async refreshAccessToken(#Req() request: Request) {
const accessToken = await getNewAccessTokenFromRefreshToken(request.cookies.refreshToken)
return { accessToken }
}
How can I tell next-auth to refresh the access token using refresh-access-token API every 10 minutes (the access token expiration date)?
I think you need to save the previous time to local storage and then compare it with current time to call the api. You can use moment.unix() or moment.diff() to do this.
I have an exiting Django project that I am trying to move from templates to NextJs frontend. I came across Next-Auth-js which seems to be nice in Next Auth.
However, the doc seems to focus more with JS related Backend Auth. Following this example I have sent the NEXTAUTH_URL environment variable to my DRF Endpoint localhost:8002. While the frontend runs on localhost:3000. While my _app.js looks like this:
<Provider options={{site: process.env.NEXTAUTH_URL,}} session={pageProps.session} >
<Component {...pageProps} />
</Provider>
Using the Nav.js for a test, I changed the signin/out href to point to my Django endpoints but it seems next-auth-js ignores this and places a session fetch to my frontend http://localhost:3000/api/auth/session instead of the the http://localhost:8002/api/auth/session.
I will appreciate any assistance on how I can correctly/securely implement this authentication using Django Rest Framework (DRF)
I think that is the way it should work, your nextjs site would be a kind of proxy/middleware to your django API client -> nextjs -> DRF, you should let it handle the sessions and for any action you need to do in your API for any authentication step, put code to hit those endpoints in the callbacks or events configuration, I think this tutorial is more accurate for your use case
from the docs
pages/api/auth/[...nextauth].js
import Providers from `next-auth/providers`
...
providers: [
Providers.Credentials({
// The name to display on the sign in form (e.g. 'Sign in with...')
name: 'Credentials',
// The credentials is used to generate a suitable form on the sign in page.
// You can specify whatever fields you are expecting to be submitted.
// e.g. domain, username, password, 2FA token, etc.
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
},
authorize: async (credentials) => {
// Add logic here to look up the user from the credentials supplied
const user = { id: 1, name: 'J Smith', email: 'jsmith#example.com' }
if (user) {
// call your DRF sign in endpoint here
// Any object returned will be saved in `user` property of the JWT
return Promise.resolve(user)
} else {
// If you return null or false then the credentials will be rejected
return Promise.resolve(null)
// You can also Reject this callback with an Error or with a URL:
// return Promise.reject(new Error('error message')) // Redirect to error page
// return Promise.reject('/path/to/redirect') // Redirect to a URL
}
}
})
]
...
events: {
signOut: async (message) => { /* call your DRF sign out endpoint here */ },
}
You can use callbacks here. https://next-auth.js.org/configuration/callbacks
callbacks: {
async signIn(user, account, profile) {
return true
},
async redirect(url, baseUrl) {
return baseUrl
},
async session(session, user) {
return session
},
async jwt(token, user, account, profile, isNewUser) {
return token
}
}
in signIn callback, you can get accessToken and tokenId from provider login. Here, call your DRF API and pass those tokens to your DRF and when you get back the access_token and refresh_token from DRF. Add them to your user instance. And then in JWT callback, get the access and refresh from user and add them into token
Got this from some blog
Though, you also need to handle the refresh token.
I have nestjs application which uses typeorm and mysql. Now I would like to add firebase for authentication handling, i.e for signup, signin, email verification, forgot password etc.
Plans is create user first in firebase, then same user details will be added into mysql user table for further operaiton. So for this I am using customized middleware
#Injectable()
export class FirebaseAuthMiddleware implements NestMiddleware {
async use(req: Request, _: Response, next: Function) {
const { authorization } = req.headers
// Bearer ezawagawg.....
if(authorization){
const token = authorization.slice(7)
const user = await firebase
.auth()
.verifyIdToken(token)
.catch(err => {
throw new HttpException({ message: 'Input data validation failed', err }, HttpStatus.UNAUTHORIZED)
})
req.firebaseUser = user
next()
}
}
}
Full code is available in Github
Problem with above code is that, it always looks for auth token, const { authorization } = req.headers const token = authorization.slice(7)
However, when user first time access application, authorization header always be null.
example if user access signup page we cannot pass auth header.
please let me know how can I modify above code when user access signup page, it allows user create user firebase, then same details can be stored in database.
We can exclude the routes for which you don't want this middleware.
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/(.*)',
)
.forRoutes(CatsController);
You can just next() to skip this middleware if there is no authorization. so it s okay to access sign up api when no authorization
I am using the following code to generate a JWT token:
jwt.sign(id, TOKEN_SECRET, { expiresIn: '24h' });
Once generated, I send the token to the client, which stores it within a cookie:
document.cookie = `session=${token}` + ';' + expires + ';path=/'
Furthermore, I am using vue.js Router for my navigation. From my understanding, if one adds the following code in the router file, one can insert middle-ware in order to protect some routes.
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
let token = Vue.cookie.get('session')
if (token == null) {
next({
path: '/',
params: { nextUrl: to.fullPath }
})
}
} else {
next()
}
})
However, I am having difficulty understanding how can one verify the validity of the JWT token using this approach, which needs to be done on the server, where the TOKEN_SECRET is stored, and not on the client side.
Let me start with this: your goal in guarding routes is to prevent the user from having a bad experience by proceeding to a page that will attempt to retrieve information that they are not authorized to view.
So, you don't need to validate the token on the client side. Since a token will only be in hand if the server validated the user and returned a token, you - the author of the client code - can use the presence of the token as a means to inform what route to take the user through.
In other words, the client having a token is all the validation you need to allow the user through to protected routes.
Remember, it is not as though a protected page has private data in and of itself. A protected page will always retrieve that protected data from the server, which means that the server has the chance to authenticate the token after all.
I'm creating a React/Redux app and am new to setting up the backend with Node/Express and authentication with Passport. What I'd like to have happen is the user logs in, credentials are checked, and if user is found from database, redirect the user to the next page and return user's info so I can update reducer.
My endpoint to login works as I think it does, as I'm able to access req.user object and am getting back the correct user info upon successful authentication. Where I'm stuck is redirecting the user to another page, while also passing along that user's info so I may update redux store.
EDIT: So I updated response by sending user info after authentication as well as the next route to redirect the user. Since I'm using redux, I perform login action, and upon getting a response, I redirect user with window.location. This partially solves my problem as even though I redirect after logging in, I believe that after updating window.location, my redux store gets refreshed, and I lose any information I grabbed server side.
app.post('/api/login', passport.authenticate('local-login', { failureRedirect: '/login'}), (req, res) => {
res.json({
redirect: '/profile
userInfo: req.user.dataValues})
})
})
export const login = (email, password) => {
return(dispatch) => {
axios
.post('/api/login', {
email: email,
password: password
})
.then(resp => {
dispatch({
type: 'LOGIN_USER',
data: resp.data
})
window.location = resp.data.redirect
})
.catch(errors => {
console.log(erros)
})
}
}
You're losing your state because you are forcing a page reload with window.location = resp.data.redirect. You should use a router with your application to do the url changes (such as react-router-dom). You can programmatically change the urls by using pushState (or maybe hashRouting?) instead of redirecting the way you currently are.