admin管理员组文章数量:1310988
My backend sends a refresh token as a cookie after the login happens.
Although this cookie does not get persisted after closing or refreshing my Next.js
page.
I am sending the refresh token cookie after loginIn as a server side cookie so its secure.
This is the login
logic:
@Post('/login')
@UsePipes(new ZodValidationPipe(authenticateBodySchema))
async login(
@Body() body: AuthenticateBodySchema,
@Res({ passthrough: true }) response: Response,
) {
const { email, password } = body
const user = await this.authService.validateUser(email, password)
const { senha, ...userWithoutPassword } = user
const token = this.authService.generateAccessToken(user.id)
const refreshToken = this.authService.generateRefreshToken(user.id)
response.cookie('refreshToken', refreshToken, {
path: '/',
httpOnly: true,
secure: this.configService.getOrThrow('ENVIRONMENT') === 'PRODUCTION',
sameSite: true,
maxAge: 7 * 24 * 60 * 60 * 1000,
})
return { token, user: userWithoutPassword }
}
@Post('/refresh')
async refresh(
@Req() request: Request,
@Res({ passthrough: true }) response: Response,
) {
const refreshToken = request.cookies.refreshToken
const { sub, user } =
await this.authService.validateRefreshToken(refreshToken)
const generatedAccessToken = this.authService.generateAccessToken(sub)
const generatedRefreshToken = this.authService.generateRefreshToken(sub)
const { senha, ...userWithoutPassword } = user
response.cookie('refreshToken', generatedRefreshToken, {
path: '/',
httpOnly: true,
secure: this.configService.getOrThrow('ENVIRONMENT') === 'PRODUCTION',
sameSite: false,
maxAge: 7 * 24 * 60 * 60 * 1000,
})
return { token: generatedAccessToken, user: userWithoutPassword }
}
Main.js
of Nest.js:
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { ConfigService } from '@nestjs/config'
import { Env } from './env'
import * as cookieParser from 'cookie-parser'
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
cors: {
origin: ['http://localhost:3001', ''],
credentials: true,
},
})
app.use(cookieParser())
const configService: ConfigService<Env, true> = app.get(ConfigService)
const port = configService.get('PORT', { infer: true })
await app.listen(port)
}
bootstrap()
After login in I get the normal cookie and the refresh token (that was sent from the backend, this refresh token gets lost after refreshing, making not possible to refresh):
The login logic in Next.js
frontend:
const signIn = async (email: string, password: string) => {
const response = await AuthService.login(email, password)
const decodedToken = jwtDecode(response.token)
if (!decodedToken.exp) {
toast('Erro ao fazer login')
return
}
destroyCookie(undefined, 'mywebsite-token')
const tokenExpiration = (decodedToken?.exp * 1000 - Date.now()) / 1000
setCookie(undefined, 'mywebsite-token', response.token, {
maxAge: tokenExpiration,
})
http.defaults.headers.Authorization = `Bearer ${response.token}`
setUser(response.user)
router.replace('/receitas')
}
My backend sends a refresh token as a cookie after the login happens.
Although this cookie does not get persisted after closing or refreshing my Next.js
page.
I am sending the refresh token cookie after loginIn as a server side cookie so its secure.
This is the login
logic:
@Post('/login')
@UsePipes(new ZodValidationPipe(authenticateBodySchema))
async login(
@Body() body: AuthenticateBodySchema,
@Res({ passthrough: true }) response: Response,
) {
const { email, password } = body
const user = await this.authService.validateUser(email, password)
const { senha, ...userWithoutPassword } = user
const token = this.authService.generateAccessToken(user.id)
const refreshToken = this.authService.generateRefreshToken(user.id)
response.cookie('refreshToken', refreshToken, {
path: '/',
httpOnly: true,
secure: this.configService.getOrThrow('ENVIRONMENT') === 'PRODUCTION',
sameSite: true,
maxAge: 7 * 24 * 60 * 60 * 1000,
})
return { token, user: userWithoutPassword }
}
@Post('/refresh')
async refresh(
@Req() request: Request,
@Res({ passthrough: true }) response: Response,
) {
const refreshToken = request.cookies.refreshToken
const { sub, user } =
await this.authService.validateRefreshToken(refreshToken)
const generatedAccessToken = this.authService.generateAccessToken(sub)
const generatedRefreshToken = this.authService.generateRefreshToken(sub)
const { senha, ...userWithoutPassword } = user
response.cookie('refreshToken', generatedRefreshToken, {
path: '/',
httpOnly: true,
secure: this.configService.getOrThrow('ENVIRONMENT') === 'PRODUCTION',
sameSite: false,
maxAge: 7 * 24 * 60 * 60 * 1000,
})
return { token: generatedAccessToken, user: userWithoutPassword }
}
Main.js
of Nest.js:
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { ConfigService } from '@nestjs/config'
import { Env } from './env'
import * as cookieParser from 'cookie-parser'
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
cors: {
origin: ['http://localhost:3001', 'https://mywebsite.vercel.app'],
credentials: true,
},
})
app.use(cookieParser())
const configService: ConfigService<Env, true> = app.get(ConfigService)
const port = configService.get('PORT', { infer: true })
await app.listen(port)
}
bootstrap()
After login in I get the normal cookie and the refresh token (that was sent from the backend, this refresh token gets lost after refreshing, making not possible to refresh):
The login logic in Next.js
frontend:
const signIn = async (email: string, password: string) => {
const response = await AuthService.login(email, password)
const decodedToken = jwtDecode(response.token)
if (!decodedToken.exp) {
toast('Erro ao fazer login')
return
}
destroyCookie(undefined, 'mywebsite-token')
const tokenExpiration = (decodedToken?.exp * 1000 - Date.now()) / 1000
setCookie(undefined, 'mywebsite-token', response.token, {
maxAge: tokenExpiration,
})
http.defaults.headers.Authorization = `Bearer ${response.token}`
setUser(response.user)
router.replace('/receitas')
}
Share
Improve this question
edited Feb 2 at 2:37
Nilton Schumacher F
asked Feb 1 at 21:43
Nilton Schumacher FNilton Schumacher F
1,1803 gold badges18 silver badges58 bronze badges
9
- Where does setCookie come from? – shotor Commented Feb 2 at 2:06
- @shotor setCookie from nookies, but the problem is not with the setCookies from frontend, is with the refresh tokenj cookies sent from backed. – Nilton Schumacher F Commented Feb 2 at 2:36
- Problem is probably that you're running backend and frontend on different domains/hostnames. So the browser will set the cookie just fine. But on the backend hostname. In the current setup you need to return the refresh token in the backend in the response. And do another setCookie in nextjs. – shotor Commented Feb 2 at 2:43
- Yep, probably that's the issue, this isn't secure too, correct? – Nilton Schumacher F Commented Feb 2 at 14:55
- 1 No. You want both tokens, access and refresh to be stored as http-only cookies. That way the JS in your app won't be able to access it. So if you want to go the current route (api/frontend different hostnames). You need to make sure that setCookie in the signIn function is only ran server-side and it sets httpOnly to true. The other option is to proxy the api on the frontend. So you create a proxy url in nextjs. Anything that goes to frontend/api gets proxied to backend/api. That way your original setCookie would set the cookie on the correct domain – shotor Commented Feb 2 at 18:36
1 Answer
Reset to default 1This answer is based on the discussion in the comments under the question.
Issue in the question
We determined in the discussion that the backend and frontend run on different domains.
So in the backend when you do:
response.cookie('refreshToken', refreshToken, {
path: '/',
httpOnly: true,
secure: this.configService.getOrThrow('ENVIRONMENT') === 'PRODUCTION',
sameSite: true,
maxAge: 7 * 24 * 60 * 60 * 1000,
})
You're setting the cookie on the domain of the backend. Which is not what we want. To fix that issue we need to instead return that cookie in the backend and set it in the frontend server side (we'll get to that in a moment):
return {
token: generatedAccessToken,
user: userWithoutPassword,
refreshToken
}
Then in the frontend we set that cookie alongside the token itself:
setCookie(undefined, 'mywebsite-token', response.token, {
maxAge: tokenExpiration,
})
setCookie(undefined, 'mywebsite-refresh-token', response.refreshToken, {
maxAge: tokenExpiration,
})
This code however is insecure.
If you're setting sensitive information such as tokens in cookies. You must set them as http-only
cookies:
setCookie(undefined, 'mywebsite-token', response.token, {
maxAge: tokenExpiration,
httpOnly: true
})
setCookie(undefined, 'mywebsite-refresh-token', response.refreshToken, {
maxAge: tokenExpiration,
})
If you don't, then any javscript script in your frontend will be able to access the cookie through document.cookie
. If a malicious user finds an XSS vulnerability in your code and manages to embed a script in your code. They'll be able to extract the cookie from any user that logs in.
That also means that your signIn
function in NextJS must be ran server side. Luckily, it should be impossible to set an httpOnly
cookie client side: How can I create secure/httpOnly cookies with document.cookie?
Alternative: Proxy the api
If you don't want to return the cookie as a JSON response and then set it in nextjs. You can use NextJS rewrites
module.exports = {
async rewrites() {
return [
{
source: '/api',
destination: 'http://backend-domain',
},
]
},
}
Then direct any API requests in your frontend to frontend-domain/api
instead of backend-domain/api
.
Now you're free to use setCookie
in your backend. The domains will match and your browser will happily set the cookie.
Downside: Every request to your backend has to go through your frontend. It adds load to your frontend. If you want to go this route and you expect a lot of traffic, it would be better to use a reverse proxy like nginx
to do this rewrite.
Session vs. Token
A session is how we track a users presence on a website. This could be a logged in user, but could also be a non-logged in user. If we wish to distinguish every user.
A token is the identifier by which we recognize the user. We issue a token, either if the user doesn't have one (if we want to track guest access) or when a user logs in.
The token can be anything you want. The most common ones you'll see are:
- An opaque uuid - sufficiently large that collisions are less likely to happen (2 users get the same token)
- JWT - A non-opaque token that can be verified against a secret and contains information about the user you can decode. In your code I see the cookie token starts with
eyJ
which suggests a JWT.
Choosing between session token and jwt as session token
A session token is nice because it's opaque. It's a random string that doesn't contain any information about the user. But you must store the session token in some database and grab it from the database on every request to check if it's valid and who the user is.
The JWT is nice because you can avoid storing any state. Instead you grab the JWT from the cookie that's being sent, verify it. If it's valid you decode it. And it tells you who the user is.
Some people say it's better to use an opaque token because it's more secure. JWTs can be decoded at any time, even if it's expired and may contain personal information. You can use https://jwt.io/ to decode yours and see what's inside
I support the opinion that httpOnly
is secure. And if someones able to get your JWT even though it's set to httpOnly
you have bigger problems on your hand.
Another consideration: JWTs are much larger than opaque tokens. A UUID v4 is 36 characters, while a JWT is usually a few hundred characters. And since the user is sending this on every request, it could become a problem.
A note about refresh tokens
I mentioned that technically you don't need to send the refresh token to the user. Similarly to what I described in the previous section about storing session tokens in a database. You can do the same for refresh tokens. Like I said, I think it's fine to send JWTs as httpOnly
only cookies. But if you really wanted to you could do it like this:
async login(...) {
const token = this.authService.generateAccessToken(user.id)
const refreshToken = this.authService.generateRefreshToken(user.id)
await redis.storeRefreshToken(user.id, refreshToken)
return { token, user: userWithoutPassword }
}
async refresh(...) {
const refreshToken = await redis.getRefreshToken(user.id)
const { sub, user } =
await this.authService.validateRefreshToken(refreshToken)
const generatedAccessToken = this.authService.generateAccessToken(sub)
const generatedRefreshToken = this.authService.generateRefreshToken(sub)
const { senha, ...userWithoutPassword } = user
await redis.storeRefreshToken(user.id, generatedRefreshToken)
return { token: generatedAccessToken, user: userWithoutPassword }
}
You could also extend this and send an opaque token instead of the JWT:
async login(...) {
const jwt = this.authService.generateAccessToken(user.id)
const refreshToken = this.authService.generateRefreshToken(user.id)
const opaqueToken = uuidv4() // from uuid@npm
await Promise.all([
redis.storeJWT(user.id, jwt, opaqueToken)
redis.storeRefreshToken(user.id, refreshToken)
])
return { token: opaqueToken, user: userWithoutPassword }
}
版权声明:本文标题:javascript - Refresh token coming from Nest.js as cookie does not get persisted after refresh - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1741862569a2401711.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论