diff --git a/.env.example b/.env.example index 8b0ed98..7f6a17f 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ CLOUDINARY_API_KEY= CLOUDINARY_API_SECRET= CLOUDINARY_CLOUD_NAME= DATABASE_URL= +FRONTEND_URL= REDIS_USERNAME= REDIS_PASSWORD= REDIS_HOST= @@ -10,8 +11,13 @@ GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GOOGLE_REDIRECT_URI= PORT= +NODE_ENV= API_PREFIX= ALLOWED_ORIGINS= +COOKIE_HTTP_ONLY= +COOKIE_SECURE= +COOKIE_SAME_SITE= +COOKIE_DOMAIN= ACCESS_JWT_SECRET= REFRESH_JWT_SECRET= ACCESS_JWT_EXPIRES_IN= diff --git a/app/app.ts b/app/app.ts index e19d675..27250ad 100644 --- a/app/app.ts +++ b/app/app.ts @@ -1,4 +1,5 @@ import express from 'express' +import cookieParser from 'cookie-parser' import cors from 'cors' import helmet from 'helmet' import logger from 'morgan' @@ -13,8 +14,9 @@ export const app = express() z.config(zodConfig) app.use(helmet()) -app.use(cors({ origin: env.ALLOWED_ORIGINS })) +app.use(cors({ origin: env.ALLOWED_ORIGINS, credentials: true })) app.use(logger(app.get('env') === 'development' ? 'dev' : 'combined')) +app.use(cookieParser()) app.use(express.json()) app.use(env.API_PREFIX, apiRouter) diff --git a/app/config/env.config.ts b/app/config/env.config.ts index 4f78268..ca7bb38 100644 --- a/app/config/env.config.ts +++ b/app/config/env.config.ts @@ -9,6 +9,7 @@ const envSchema = z.object({ CLOUDINARY_API_SECRET: z.string(), CLOUDINARY_CLOUD_NAME: z.string(), DATABASE_URL: z.string(), + FRONTEND_URL: z.url(), GOOGLE_CLIENT_ID: z.string(), GOOGLE_CLIENT_SECRET: z.string(), GOOGLE_REDIRECT_URI: z.url(), @@ -19,6 +20,11 @@ const envSchema = z.object({ ACCESS_JWT_SECRET: z.string().transform(v => new TextEncoder().encode(v)), REFRESH_JWT_SECRET: z.string().transform(v => new TextEncoder().encode(v)), PORT: z.coerce.number().int().positive().min(1000).max(65535), + NODE_ENV: z.enum(['development', 'production']).default('development'), + COOKIE_HTTP_ONLY: z.stringbool(), + COOKIE_SECURE: z.stringbool(), + COOKIE_SAME_SITE: z.enum(['lax', 'strict', 'none']), + COOKIE_DOMAIN: z.string(), API_PREFIX: z.string(), ALLOWED_ORIGINS: z .string() diff --git a/app/controllers/auth.controller.ts b/app/controllers/auth.controller.ts index 8ab0b11..ff51d30 100644 --- a/app/controllers/auth.controller.ts +++ b/app/controllers/auth.controller.ts @@ -1,11 +1,6 @@ import crypto from 'crypto' -import type { - GoogleCodeSchema, - RefreshTokenSchema, - SigninSchema, - SignupSchema -} from '@/schemas' -import type { JwtPayload, TypedRequestBody } from '@/types' +import type { GoogleCodeSchema, SigninSchema, SignupSchema } from '@/schemas' +import type { JwtPayload, TypedRequestBody, TypedRequestQuery } from '@/types' import type { NextFunction, Request, Response } from 'express' import { prisma } from '@/prisma' @@ -26,7 +21,12 @@ const { REFRESH_JWT_ALGORITHM, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, - GOOGLE_REDIRECT_URI + GOOGLE_REDIRECT_URI, + FRONTEND_URL, + COOKIE_HTTP_ONLY, + COOKIE_DOMAIN, + COOKIE_SECURE, + COOKIE_SAME_SITE } = env class AuthController { @@ -36,6 +36,9 @@ class AuthController { GOOGLE_REDIRECT_URI ) + private readonly ACCESS_TOKEN_NAME = 'accessToken' + private readonly REFRESH_TOKEN_NAME = 'refreshToken' + signup = async ( { body }: TypedRequestBody, res: Response, @@ -54,13 +57,11 @@ class AuthController { } }) - const newSession = await prisma.session.create({ - data: { userId: user.id } - }) + const tokens = await this.getNewTokens({ id: user.id }) - const tokens = await this.getNewTokens({ id: user.id, sid: newSession.id }) + this.setTokensCookie(res, tokens) - res.json({ user, ...tokens }) + res.json({ user }) } signin = async ( @@ -83,16 +84,14 @@ class AuthController { if (!isPasswordMatch) return next(Unauthorized('Email or password invalid')) - const newSession = await prisma.session.create({ - data: { userId: user.id } - }) + const tokens = await this.getNewTokens({ id: user.id }) - const tokens = await this.getNewTokens({ id: user.id, sid: newSession.id }) + this.setTokensCookie(res, tokens) - res.json({ user: userWithoutPassword, ...tokens }) + res.json({ user: userWithoutPassword }) } - getGoogleRedirectUrl = async (_: Request, res: Response) => { + googleInitiate = async (_: Request, res: Response) => { const state = crypto.randomBytes(32).toString('hex') await redisClient.set(`oauth_state:${state}`, 'true', 'EX', 5 * 60) @@ -107,11 +106,11 @@ class AuthController { } googleCallback = async ( - req: TypedRequestBody, + req: TypedRequestQuery, res: Response, next: NextFunction ) => { - const { code, state: receivedState } = req.body + const { code, state: receivedState } = req.query const redisStateKey = `oauth_state:${receivedState}` @@ -126,15 +125,12 @@ class AuthController { if (!tokens.id_token) return next(Forbidden()) const ticket = await this.googleClient.verifyIdToken({ - idToken: tokens.id_token, - audience: GOOGLE_CLIENT_ID + idToken: tokens.id_token }) const payload = ticket.getPayload() - if (!payload || !payload.email) { - return next(Forbidden('Invalid token')) - } + if (!payload || !payload.email) return next(Forbidden('Invalid token')) const { email, @@ -151,59 +147,39 @@ class AuthController { data: { name, email, avatar: picture } }) - const newSession = await prisma.session.create({ - data: { userId: user.id } - }) + const tokens = await this.getNewTokens({ id: user.id }) - const tokens = await this.getNewTokens({ - id: user.id, - sid: newSession.id - }) + this.setTokensCookie(res, tokens) - res.json({ user, ...tokens }) + res.redirect(FRONTEND_URL) } else { - const newSession = await prisma.session.create({ - data: { userId: user.id } - }) + const tokens = await this.getNewTokens({ id: user.id }) - const tokens = await this.getNewTokens({ - id: user.id, - sid: newSession.id - }) + this.setTokensCookie(res, tokens) - res.json({ user, ...tokens }) + res.redirect(FRONTEND_URL) } } - refresh = async ( - { body }: TypedRequestBody, - res: Response, - next: NextFunction - ) => { + refresh = async (req: Request, res: Response, next: NextFunction) => { + const refreshToken = req.cookies[this.REFRESH_TOKEN_NAME] + + if (!refreshToken) return next(Forbidden()) + try { const { - payload: { id, sid } - } = await jwtVerify(body.refreshToken, REFRESH_JWT_SECRET) + payload: { id } + } = await jwtVerify(refreshToken, REFRESH_JWT_SECRET) const user = await prisma.user.findFirst({ where: { id } }) if (!user) return next(Forbidden()) - const currentSession = await prisma.session.findFirst({ - where: { id: sid } - }) - - if (!currentSession) return next(Forbidden()) - - await prisma.session.delete({ where: { id: currentSession.id } }) + const tokens = await this.getNewTokens({ id: user.id }) - const newSid = await prisma.session.create({ - data: { userId: user.id } - }) - - const tokens = await this.getNewTokens({ id: user.id, sid: newSid.id }) + this.setTokensCookie(res, tokens) - res.json(tokens) + res.json({ message: 'Tokens refreshed successfully' }) } catch (error) { if (error instanceof JWTExpired) return next(Forbidden(error.code)) @@ -211,8 +187,9 @@ class AuthController { } } - logout = async ({ session }: Request, res: Response) => { - await prisma.session.delete({ where: { id: session } }) + logout = async (_: Request, res: Response) => { + res.clearCookie(this.ACCESS_TOKEN_NAME) + res.clearCookie(this.REFRESH_TOKEN_NAME) res.sendStatus(204) } @@ -230,6 +207,25 @@ class AuthController { return { accessToken, refreshToken } } + + private setTokensCookie = ( + res: Response, + tokens: { accessToken: string; refreshToken: string } + ) => { + res.cookie(this.ACCESS_TOKEN_NAME, tokens.accessToken, { + httpOnly: COOKIE_HTTP_ONLY, + secure: COOKIE_SECURE, + sameSite: COOKIE_SAME_SITE, + domain: COOKIE_DOMAIN + }) + + res.cookie(this.REFRESH_TOKEN_NAME, tokens.refreshToken, { + httpOnly: COOKIE_HTTP_ONLY, + secure: COOKIE_SECURE, + sameSite: COOKIE_SAME_SITE, + domain: COOKIE_DOMAIN + }) + } } export const authController = new AuthController() diff --git a/app/middlewares/authenticate.ts b/app/middlewares/authenticate.ts index 642a2cf..55f3bf5 100644 --- a/app/middlewares/authenticate.ts +++ b/app/middlewares/authenticate.ts @@ -4,6 +4,7 @@ import type { NextFunction, Request, Response } from 'express' import { prisma } from '@/prisma' import { Unauthorized } from 'http-errors' import { jwtVerify } from 'jose' +import { JWTExpired } from 'jose/errors' import { env } from '@/config' @@ -12,14 +13,13 @@ export const authenticate = async ( _: Response, next: NextFunction ) => { - const { authorization = '' } = req.headers - const [bearer, token] = authorization.split(' ') + const token: string = req.cookies.accessToken - if (bearer !== 'Bearer') return next(Unauthorized()) + if (!token) return next(Unauthorized()) try { const { - payload: { id, sid } + payload: { id } } = await jwtVerify(token, env.ACCESS_JWT_SECRET) const user = await prisma.user.findFirst({ @@ -27,15 +27,14 @@ export const authenticate = async ( omit: { password: false } }) - const session = await prisma.session.findFirst({ where: { id: sid } }) - - if (!user || !session) return next(Unauthorized()) + if (!user) return next(Unauthorized()) req.user = user - req.session = session.id next() - } catch { + } catch (e) { + if (e instanceof JWTExpired) return next(Unauthorized(e.code)) + return next(Unauthorized()) } } diff --git a/app/routes/api/auth.ts b/app/routes/api/auth.ts index c5c6979..88c5ace 100644 --- a/app/routes/api/auth.ts +++ b/app/routes/api/auth.ts @@ -4,12 +4,7 @@ import { authController } from '@/controllers' import { authenticate, validateRequest } from '@/middlewares' -import { - GoogleCodeSchema, - RefreshTokenSchema, - SigninSchema, - SignupSchema -} from '@/schemas' +import { GoogleCodeSchema, SigninSchema, SignupSchema } from '@/schemas' export const authRouter = Router() @@ -25,18 +20,14 @@ authRouter.post( authController.signin ) -authRouter.get('/google/initiate', authController.getGoogleRedirectUrl) +authRouter.post('/google/initiate', authController.googleInitiate) -authRouter.post( +authRouter.get( '/google/callback', - validateRequest({ body: GoogleCodeSchema }), + validateRequest({ query: GoogleCodeSchema }), authController.googleCallback ) -authRouter.post( - '/refresh', - validateRequest({ body: RefreshTokenSchema }), - authController.refresh -) +authRouter.post('/refresh', authController.refresh) authRouter.post('/logout', authenticate, authController.logout) diff --git a/app/schemas/auth.schema.ts b/app/schemas/auth.schema.ts index b99086e..7281f5c 100644 --- a/app/schemas/auth.schema.ts +++ b/app/schemas/auth.schema.ts @@ -10,10 +10,6 @@ export const SignupSchema = z.object({ name: z.string().min(2) }) -export const RefreshTokenSchema = z.object({ - refreshToken: z.string().min(1) -}) - export const GoogleCodeSchema = z.object({ code: z.string().min(1), state: z.optional(z.string()) diff --git a/app/schemas/index.ts b/app/schemas/index.ts index 1670178..394fc23 100644 --- a/app/schemas/index.ts +++ b/app/schemas/index.ts @@ -15,10 +15,5 @@ export { ColumnParamsSchema, UpdateColumnOrderSchema } from './column.schema' -export { - SigninSchema, - SignupSchema, - RefreshTokenSchema, - GoogleCodeSchema -} from './auth.schema' +export { SigninSchema, SignupSchema, GoogleCodeSchema } from './auth.schema' export { EditUserSchema, NeedHelpSchema } from './user.schema' diff --git a/app/types/express.d.ts b/app/types/express.d.ts index 37052c1..45f0201 100644 --- a/app/types/express.d.ts +++ b/app/types/express.d.ts @@ -3,7 +3,6 @@ import type { User } from '@prisma/client' declare global { namespace Express { interface Request { - session: string user: User } } diff --git a/app/types/jwt-payload.ts b/app/types/jwt-payload.ts index 00351be..23d7152 100644 --- a/app/types/jwt-payload.ts +++ b/app/types/jwt-payload.ts @@ -1,4 +1,3 @@ export type JwtPayload = { id?: string - sid?: string } diff --git a/package.json b/package.json index 2564cfd..e2d4c42 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,10 @@ }, "dependencies": { "@prisma/client": "^6.13.0", + "@types/cookie-parser": "^1.4.9", "argon2": "^0.44.0", "cloudinary": "^2.7.0", + "cookie-parser": "^1.4.7", "cors": "2.8.5", "dotenv": "^17.2.1", "express": "^5.1.0", diff --git a/swagger.json b/swagger.json index dd185a4..366de1a 100644 --- a/swagger.json +++ b/swagger.json @@ -88,41 +88,52 @@ } }, "/auth/google/initiate": { - "get": { + "post": { "tags": ["Auth"], - "summary": "Get redirect url for Google authentication", + "summary": "Create a redirect url for Google authentication", "responses": { "200": { "$ref": "#/components/responses/GoogleInitiateResponse" } } } }, "/auth/google/callback": { - "post": { + "get": { "tags": ["Auth"], - "summary": "Handle Google OAuth callback and sign-in/sign-up", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["code", "state"], - "properties": { - "code": { - "type": "string", - "example": "4/0AT5xKif_S_..." - }, - "state": { - "type": "string", - "example": "1d73efc8d4f8d426a3e51f6337ee7cb7" - } - } - } + "summary": "Receives the authorization code from Google's redirect", + "parameters": [ + { + "name": "code", + "in": "query", + "required": true, + "description": "The authorization code provided by Google.", + "schema": { + "type": "string", + "example": "4/0AT5xKif_S_..." + } + }, + { + "name": "state", + "in": "query", + "required": false, + "description": "An opaque value used to maintain state between the request and callback.", + "schema": { + "type": "string", + "example": "4552c8890bb4d9d44e971653..." } } - }, + ], "responses": { - "200": { "$ref": "#/components/responses/GoogleResponse" }, + "302": { + "headers": { + "Location": { + "description": "The URL to redirect to", + "schema": { + "type": "string", + "example": "http://localhost:3000" + } + } + } + }, "400": { "$ref": "#/components/responses/BadRequestError" }, "403": { "$ref": "#/components/responses/Forbidden" } } @@ -132,24 +143,7 @@ "post": { "tags": ["Auth"], "summary": "Get new access and refresh tokens", - "security": [{ "Bearer": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["refreshToken"], - "properties": { - "refreshToken": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2NzE3NDNkYjg1Yzk2NDUzZTA0YTY4NCIsInNpZCI6IjY2NzE3OWE0YTgxZmI1MjIzOTg0NmY2MSIsImlhdCI6MTcxODcxMjc0MCwiZXhwIjoxNzE5MzE3NTQwfQ.B8htZY5kU0kvX2UYsEN0u7ZUPVxTKfCs5ASAocTc-P4" - } - } - } - } - } - }, + "security": [{ "CookieAuth": [] }], "responses": { "200": { "$ref": "#/components/responses/RefreshResponse" }, "400": { "$ref": "#/components/responses/BadRequestError" }, @@ -162,7 +156,7 @@ "post": { "tags": ["Auth"], "summary": "Logout a user", - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "responses": { "204": { "description": "The user was logged out successfully." }, "401": { "$ref": "#/components/responses/Unauthorized" } @@ -173,7 +167,7 @@ "get": { "tags": ["User"], "summary": "Get current user", - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "responses": { "200": { "$ref": "#/components/responses/UserResponse" }, "401": { "$ref": "#/components/responses/Unauthorized" } @@ -184,7 +178,7 @@ "post": { "tags": ["User"], "summary": "Send email need help", - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "requestBody": { "required": true, "content": { @@ -232,7 +226,7 @@ "patch": { "tags": ["User"], "summary": "Edit user profile", - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "requestBody": { "required": true, "content": { @@ -286,7 +280,7 @@ "get": { "tags": ["Board"], "summary": "Get all boards", - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "responses": { "200": { "$ref": "#/components/responses/AllBoardsResponse" }, "401": { "$ref": "#/components/responses/Unauthorized" } @@ -295,7 +289,7 @@ "post": { "tags": ["Board"], "summary": "Add new board", - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "requestBody": { "required": true, "content": { @@ -354,7 +348,7 @@ } } ], - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "responses": { "200": { "$ref": "#/components/responses/BoardByIdResponse" }, "401": { "$ref": "#/components/responses/Unauthorized" }, @@ -375,7 +369,7 @@ } } ], - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "requestBody": { "required": true, "content": { @@ -432,7 +426,7 @@ } } ], - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "responses": { "204": { "description": "The resource was deleted successfully." }, "401": { "$ref": "#/components/responses/Unauthorized" }, @@ -455,7 +449,7 @@ } } ], - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "requestBody": { "required": true, "content": { @@ -497,7 +491,7 @@ } } ], - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "requestBody": { "required": true, "content": { @@ -548,7 +542,7 @@ } } ], - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "requestBody": { "required": true, "content": { @@ -587,7 +581,7 @@ } } ], - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "responses": { "204": { "description": "The resource was deleted successfully." }, "401": { "$ref": "#/components/responses/Unauthorized" }, @@ -610,7 +604,7 @@ } } ], - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "requestBody": { "required": true, "content": { @@ -666,7 +660,7 @@ } } ], - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "requestBody": { "required": true, "content": { @@ -714,7 +708,7 @@ } } ], - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "requestBody": { "required": true, "content": { @@ -767,7 +761,7 @@ } } ], - "security": [{ "Bearer": [] }], + "security": [{ "CookieAuth": [] }], "responses": { "204": { "description": "The resource was deleted successfully." }, "401": { "$ref": "#/components/responses/Unauthorized" }, @@ -812,15 +806,7 @@ "AuthResponseSchema": { "type": "object", "properties": { - "user": { "$ref": "#/components/schemas/User" }, - "accessToken": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3NDcyMzcxOWI0N2ZkMDhlNmEyZDUxNyIsInNpZCI6IjY3NDcyMzcyOWI0N2ZkMDhlNmEyZDUxOCIsImlhdCI6MTczMjcxNTM3OCwiZXhwIjoxNzMyNzE4OTc4fQ.ZCaaWh4rc4AHzTSljDbc7mq5gbHvjMx54y6LoJsXX-4" - }, - "refreshToken": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3NDcyMzcxOWI0N2ZkMDhlNmEyZDUxNyIsInNpZCI6IjY3NDcyMzcyOWI0N2ZkMDhlNmEyZDUxOCIsImlhdCI6MTczMjcxNTM3OCwiZXhwIjoxNzMzMzIwMTc4fQ.y17k3eWBJ4WeqNZeuguOh_5UQDiyaCPYFmn_Ir-Ftog" - } + "user": { "$ref": "#/components/schemas/User" } } }, "BoardBackground": { @@ -1009,6 +995,14 @@ "application/json": { "schema": { "$ref": "#/components/schemas/AuthResponseSchema" } } + }, + "headers": { + "Set-Cookie": { + "schema": { + "type": "string", + "example": "accessToken=eyJhbGci...; Path=/; HttpOnly; SameSite=Lax; Secure; refreshToken=eyJhbGci...; Path=/; HttpOnly; SameSite=Lax; Secure;" + } + } } }, "GoogleInitiateResponse": { @@ -1027,53 +1021,27 @@ } } }, - "GoogleResponse": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { "$ref": "#/components/schemas/AuthResponseSchema" }, - { - "type": "object", - "properties": { - "user": { - "type": "object", - "properties": { - "avatar": { - "type": "string", - "format": "url", - "example": "https://lh3.googleusercontent.com/a/ACg8ocLWRkQo4yOicLduVfTehM7hCPx1Q7WGFjgOFu1GT" - }, - "avatarPublicId": { - "type": "string", - "example": "google-picture" - } - } - } - } - } - ] - } - } - } - }, "RefreshResponse": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "acesssToken": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2NzE3NDNkYjg1Yzk2NDUzZTA0YTY4NCIsInNpZCI6IjY2NzE3OWE0YTgxZmI1MjIzOTg0NmY2MSIsImlhdCI6MTcxODcxMjc0MCwiZXhwIjoxNzE4NzE2MzQwfQ.hwdlZg0fft0PjwN63qwfjFTg1ZeyOOukFdZVbF0VOUU" - }, - "refreshToken": { + "message": { "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2NzE3NDNkYjg1Yzk2NDUzZTA0YTY4NCIsInNpZCI6IjY2NzE3OWE0YTgxZmI1MjIzOTg0NmY2MSIsImlhdCI6MTcxODcxMjc0MCwiZXhwIjoxNzE5MzE3NTQwfQ.B8htZY5kU0kvX2UYsEN0u7ZUPVxTKfCs5ASAocTc-P4" + "example": "Tokens refreshed successfully" } } } } + }, + "headers": { + "Set-Cookie": { + "schema": { + "type": "string", + "example": "accessToken=eyJhbGci...; Path=/; HttpOnly; SameSite=Lax; Secure; refreshToken=eyJhbGci...; Path=/; HttpOnly; SameSite=Lax; Secure;" + } + } } }, "UserResponse": { @@ -1233,7 +1201,12 @@ } }, "securitySchemes": { - "Bearer": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" } + "CookieAuth": { + "type": "apiKey", + "in": "cookie", + "name": "accessToken", + "description": "Access token is sent as an HTTP-only cookie." + } } } } diff --git a/yarn.lock b/yarn.lock index 3f6c00c..3688f5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -578,6 +578,11 @@ dependencies: "@types/node" "*" +"@types/cookie-parser@^1.4.9": + version "1.4.9" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.9.tgz#f0e79c766a58ee7369a52e7509b3840222f68ed2" + integrity sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g== + "@types/cors@^2.8.19": version "2.8.19" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" @@ -1302,12 +1307,25 @@ conventional-commits-parser@^5.0.0: meow "^12.0.1" split2 "^4.0.0" +cookie-parser@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.7.tgz#e2125635dfd766888ffe90d60c286404fa0e7b26" + integrity sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw== + dependencies: + cookie "0.7.2" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + cookie-signature@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== -cookie@^0.7.1: +cookie@0.7.2, cookie@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==