I'm trying to do authorization using JWT access&refresh tokens (Next.js SSR + Redux Toolkit RTK Query + NestJS). When I receive a response from the server on the client (for example using Postman) the cookies sent by the server are saved. But when I do it on SSR using RTK Query Set-Cookie from the server just doesn't do anything. Sorry if I misunderstood something, I'm new to this.
NSETJS auth.controller.ts:
import {
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Req,
Res,
UseGuards
} from '@nestjs/common';
import { Response } from 'express';
import { ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { AuthService } from './auth.service';
import { DiscordAuthGuard } from './guards/discord-auth.guard';
import JwtAuthGuard from './guards/jwt-auth.guard';
import { RequestWithUser } from './auth.interface';
import JwtRefreshGuard from './guards/jwt-auth-refresh.guard';
import { UserService } from '@/modules/user/user.service';
@Controller('auth')
@ApiTags('Auth routes')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService
) {}
@Get('login')
@HttpCode(HttpStatus.OK)
@UseGuards(DiscordAuthGuard)
login(@Req() _req: Request) {}
@Get('redirect')
@HttpCode(HttpStatus.OK)
@UseGuards(DiscordAuthGuard)
async redirect(@Req() req: RequestWithUser, @Res() res: Response) {
const { user } = req;
const accessTokenCookie = this.authService.getCookieWithJwtAccessToken(
user.id
);
const refreshTokenCookie = this.authService.getCookieWithJwtRefreshToken(
user.id
);
await this.userService.setCurrentRefreshToken(
refreshTokenCookie.token,
user.id
);
req.res.setHeader('Set-Cookie', [
accessTokenCookie.cookie,
refreshTokenCookie.cookie
]);
return res.redirect('http://localhost:3000');
}
@Get('refresh')
@HttpCode(HttpStatus.OK)
@UseGuards(JwtRefreshGuard)
async refresh(@Req() req: RequestWithUser) {
const { user } = req;
const accessTokenCookie = this.authService.getCookieWithJwtAccessToken(
user.id
);
req.res.setHeader('Set-Cookie', [accessTokenCookie.cookie]);
return user;
}
@Get('me')
@HttpCode(HttpStatus.OK)
@UseGuards(JwtAuthGuard)
me(@Req() req: RequestWithUser) {
const { user } = req;
return user;
}
@Post('logout')
@HttpCode(HttpStatus.OK)
@UseGuards(JwtAuthGuard)
async logout(@Req() req: RequestWithUser) {
const { user } = req;
await this.userService.removeRefreshToken(user.id);
req.res.setHeader('Set-Cookie', this.authService.getCookiesForLogOut());
}
}
NEXT.JS _app.tsx:
import '@/styles/globals.scss';
import { AppProps } from 'next/app';
import { wrapper } from '@/store';
import { me } from '@/store/auth/auth.api';
import { setCredentials } from '@/store/auth/auth.slice';
function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
App.getInitialProps = wrapper.getInitialAppProps(
(store) =>
async ({ ctx, Component }) => {
try {
const { data: user } = await store.dispatch(me.initiate());
if (user !== undefined) {
store.dispatch(
setCredentials({
user,
})
);
}
} catch (err) {
console.log(err);
}
return {
pageProps: {
...(Component.getInitialProps
? await Component.getInitialProps({ ...ctx, store })
: {}),
},
};
}
);
export default wrapper.withRedux(App);
NEXT.JS auth.api.ts:
import { parseCookies } from 'nookies';
import {
BaseQueryFn,
createApi,
fetchBaseQuery,
} from '@reduxjs/toolkit/query/react';
import { HYDRATE } from 'next-redux-wrapper';
import { Mutex } from 'async-mutex';
import { NextPageContext } from 'next/types';
import { IUser } from './auth.interface';
import { destroyCredentials } from './auth.slice';
const mutex = new Mutex();
const baseQuery = fetchBaseQuery({
baseUrl: 'http://localhost:7777/api/auth',
prepareHeaders: (headers, { extra }) => {
const ctx = extra as Pick<NextPageContext<any>, 'req'>;
const windowAvailable = () =>
!!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
if (windowAvailable()) {
console.log('running on browser, skipping header manipulation');
return headers;
}
const cookies = parseCookies(ctx);
// Build a cookie string from object
const cookieValue = Object.entries(cookies)
// .filter(([k]) => k === 'JSESSIONID') // only include relevant cookies
.map(([k, v]) => `${k}=${v}`) // rfc6265
.join('; ');
console.log('figured out cookie value: ' + cookieValue);
headers.set('Cookie', cookieValue);
return headers;
},
credentials: 'include',
});
const baseQueryWithReauth: BaseQueryFn = async (args, api, extraOptions) => {
await mutex.waitForUnlock();
console.log(' sending request to server');
let result = await baseQuery(args, api, extraOptions);
if (result?.error?.status === 401) {
if (!mutex.isLocked()) {
console.log(' 401, sending refresh token');
const release = await mutex.acquire();
try {
const refreshResult = await baseQuery('refresh', api, extraOptions);
const setCookies =
refreshResult.meta?.response?.headers.get('set-cookie');
console.log(' response set-cookies:', setCookies);
console.log(
' refresh response status:',
refreshResult.meta?.response?.status
);
if (refreshResult.data) {
const windowAvailable = () =>
!!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
if (!windowAvailable()) {
console.log(' running on server');
}
result = await baseQuery(args, api, extraOptions);
console.log(
' request response status after /refresh',
result.meta?.response?.status
);
} else {
api.dispatch(destroyCredentials());
}
} finally {
release();
}
}
}
return result;
};
export const authApi = createApi({
reducerPath: 'api/auth',
baseQuery: baseQueryWithReauth,
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === HYDRATE) {
return action.payload[reducerPath];
}
},
tagTypes: ['Auth'],
endpoints: (build) => ({
me: build.query<IUser, void>({
query: () => ({
url: '/me',
method: 'GET',
}),
providesTags: ['Auth'],
}),
logout: build.mutation<void, void>({
query: () => ({
url: '/logout',
method: 'POST',
}),
invalidatesTags: ['Auth'],
}),
}),
});
// Export hooks for usage in functional components
export const {
useMeQuery,
useLogoutMutation,
util: { getRunningOperationPromises },
} = authApi;
// export endpoints for use in SSR
export const { me, logout } = authApi.endpoints;
NEXT.JS auth.slice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';
import { RootState } from '..';
import { IUser } from './auth.interface';
export interface IAuthState {
user: IUser | null;
}
const initialState: IAuthState = {
user: null,
};
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setCredentials: (
state,
action: PayloadAction<{
user: IUser | null;
}>
) => {
const { user } = action.payload;
state.user = user;
},
destroyCredentials: (state) => {
state.user = null;
},
},
extraReducers: {
[HYDRATE]: (state, action) => {
return {
...state,
...action.payload.auth,
};
},
},
});
export const { setCredentials, destroyCredentials } = authSlice.actions;
export const selectCurrentUser = (state: RootState) => state.auth.user;
export default authSlice.reducer;
NEXT.JS store/index.ts:
import {
configureStore,
ImmutableStateInvariantMiddlewareOptions,
SerializableStateInvariantMiddlewareOptions,
ThunkAction,
} from '@reduxjs/toolkit';
import { Action, combineReducers } from 'redux';
import { Context, createWrapper } from 'next-redux-wrapper';
import botApi from './bots/bot.api';
import authReducer from './auth/auth.slice';
import { authApi } from './auth/auth.api';
// ThunkOptions not exported in getDefaultMiddleware, so we have a copy here
interface MyThunkOptions<E> {
extraArgument: E;
}
// GetDefaultMiddlewareOptions in getDefaultMiddleware does not allow
// providing type for ThunkOptions, so here is our custom version
// https://redux-toolkit.js.org/api/getDefaultMiddleware#api-reference
interface MyDefaultMiddlewareOptions {
thunk?: boolean | MyThunkOptions<Context>;
immutableCheck?: boolean | ImmutableStateInvariantMiddlewareOptions;
serializableCheck?: boolean | SerializableStateInvariantMiddlewareOptions;
}
const rootReducer = combineReducers({
// Add the generated reducer as a specific top-level slice
[botApi.reducerPath]: botApi.reducer,
[authApi.reducerPath]: authApi.reducer,
auth: authReducer,
});
const makeStore = (wtf: any) => {
const ctx = wtf.ctx as Context;
return configureStore({
reducer: rootReducer,
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: (gDM) =>
gDM<MyDefaultMiddlewareOptions>({
thunk: {
// https://github.com/reduxjs/redux-toolkit/issues/2228#issuecomment-1095409011
extraArgument: ctx,
},
}).concat(botApi.middleware, authApi.middleware),
devTools: process.env.NODE_ENV !== 'production',
});
};
export type RootState = ReturnType<AppStore['getState']>;
export type AppStore = ReturnType<typeof makeStore>;
export type AppDispatch = ReturnType<typeof makeStore>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action
>;
export const wrapper = createWrapper<AppStore>(makeStore);
I would be grateful to everyone for any help with a solution .