Skip to content

Commit

Permalink
Enabled cookie responses.
Browse files Browse the repository at this point in the history
  • Loading branch information
redondi88 committed Dec 4, 2024
1 parent b2f3a15 commit 729d787
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 20 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ yarn add @redon2inc/strapi-plugin-refresh-token

## Config

You will need to set the following environment variables:
```
PRODUCTION_URL=value # used for cookie security if enabled
REFRESH_JWT_SECRET=string
```

This component relies on extending the `user-permissions` types. Extend it by adding the following to `./src/extensions/user-permissions/content-types/user/schema.json`

```javascript
Expand Down Expand Up @@ -62,6 +68,7 @@ Modify your plugins file `config/plugin.ts` to have the following:
refreshTokenExpiresIn: '30d', // this value should be higher than the jwt.expiresIn
requestRefreshOnAll: false, // automatically send a refresh token in all login requests.
refreshTokenSecret: env('REFRESH_JWT_SECRET') || 'SomethingSecret',
cookieResponse: false // if set to true, the refresh token will be sent in a cookie
},
}
```
Expand Down Expand Up @@ -101,5 +108,5 @@ if the Refresh token is valid, the API will return
```

## TODO:
- Currently the tokens do not get removed from the DB on usage. Only if they are expired.
- Currently the tokens do not get removed from the DB on usage. They are cleaned when a new token is requested and the old ones have expired.
- Expose API so user can clear all sessions on their own.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"type": "git",
"directory": "."
},
"version": "0.1.0",
"version": "0.1.1",
"keywords": [],
"type": "commonjs",
"exports": {
Expand Down
1 change: 1 addition & 0 deletions server/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default {
refreshTokenSecret : 'SuperSecretKey',
requestRefreshOnAll : false,
refreshTokenExpiresIn : '1d',
cookieResponse: false,
},
validator() {},
};
49 changes: 48 additions & 1 deletion server/src/middlewares/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('Auth Middleware', () => {

beforeEach(() => {
// Mocking Strapi dependencies
process.env.PRODUCTION_URL = 'https://redon2.ca/'; //awesome ppl
strapiMock = {
config: {
get: jest.fn().mockReturnValue({
Expand Down Expand Up @@ -72,10 +73,13 @@ describe('Auth Middleware', () => {
},
send: jest.fn(),
status: 200,
cookies: {
set: jest.fn(),
}
};
});

it('should add refresh token to response body on successful /api/auth/local', async () => {
it('Generate refreshToken in body /api/auth/local', async () => {
const middleware = auth({ strapi: strapiMock });

await middleware(ctxMock, () => Promise.resolve());
Expand All @@ -89,6 +93,49 @@ describe('Auth Middleware', () => {
{ expiresIn: '30d' }
);
});
it.each([
{ refreshTokenExpiresIn: '1h' },
{ refreshTokenExpiresIn: '15m' },
{ refreshTokenExpiresIn: '7d' },
])('Generate refreshToken in cookie with refreshTokenExpiresIn: %o /api/auth/local', async ({ refreshTokenExpiresIn }) => {
strapiMock.config.get.mockReturnValueOnce({
...strapiMock.config.get(),
cookieResponse: true,
refreshTokenExpiresIn
});

const middleware = auth({ strapi: strapiMock });

await middleware(ctxMock, () => Promise.resolve());

// Assert that a refresh token is added to response body
expect(ctxMock.cookies.set).toHaveBeenCalledWith(
'refreshToken',
expect.any(String),
expect.objectContaining({
httpOnly: true,
secure: expect.any(Boolean),
maxAge: expect.any(Number),
domain: expect.any(String),
})
);
expect(strapiMock.plugin).toHaveBeenCalledWith(expect.stringContaining(PLUGIN_ID));
expect(jwt.sign).toHaveBeenCalledWith(
expect.objectContaining({ userId: 1, secret: 'testDocumentId' }),
'testSecretKey',
{ expiresIn: refreshTokenExpiresIn }
);
});
it('Fail refresh to bad config', async () => {
strapiMock.config.get.mockReturnValueOnce({
...strapiMock.config.get(),
cookieResponse: true,
refreshTokenExpiresIn: '1t', //bad param
});
const middleware = auth({ strapi: strapiMock });

await expect(middleware(ctxMock, () => Promise.resolve())).rejects.toThrow('Invalid tokenExpires format. Use formats like "30d", "1h", "15m".');
});

it('should send a new JWT on valid /api/auth/local/refresh', async () => {
ctxMock.request = {
Expand Down
59 changes: 43 additions & 16 deletions server/src/middlewares/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,39 @@ import jwt from 'jsonwebtoken';
import { PLUGIN_ID } from '../pluginId';

interface JwtPayload {
userId : number,
secret: string,
userId: number;
secret: string;
}
function calculateMaxAge(param){
const unit = param.slice(-1); // Get the unit (d, h, m)
const value = parseInt(param.slice(0, -1)); // Get the numerical value

let maxAge;

switch (unit) {
case 'd':
maxAge = 1000 * 60 * 60 * 24 * value;
break;
case 'h':
maxAge = 1000 * 60 * 60 * value;
break;
case 'm':
maxAge = 1000 * 60 * value;
break;
default:
throw new Error('Invalid tokenExpires format. Use formats like "30d", "1h", "15m".');
}

return maxAge;
}
function auth({ strapi }) {
const config = strapi.config.get(`plugin::${PLUGIN_ID}`);

return async (ctx, next) => {
await next();
if (ctx.request.method === 'POST' && ctx.request.path === '/api/auth/local') {
const requestRefresh = ctx.request.body?.requestRefresh || config.requestRefreshOnAll;
if (ctx.response.body && ctx.response.message==='OK' && requestRefresh) {

if (ctx.response.body && ctx.response.message === 'OK' && requestRefresh) {
const refreshEntry = await strapi
.plugin(PLUGIN_ID)
.service('service')
Expand All @@ -26,23 +46,30 @@ function auth({ strapi }) {
expiresIn: config.refreshTokenExpiresIn,
}
);
ctx.response.body = {
...ctx.response.body,
refreshToken: refreshToken,
};
if (config.cookieResponse) {
ctx.cookies.set('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production' ? true : false,
maxAge: calculateMaxAge(config.refreshTokenExpiresIn),
domain:
process.env.NODE_ENV === 'development' ? 'localhost' : process.env.PRODUCTION_URL,
});
} else {
ctx.response.body = {
...ctx.response.body,
refreshToken: refreshToken,
};
}
}
} else if (ctx.request.method === 'POST' && ctx.request.path === '/api/auth/local/refresh') {
const refreshToken = ctx.request.body?.refreshToken;
if (refreshToken) {
try {
const decoded = await jwt.verify(refreshToken, config.refreshTokenSecret) as JwtPayload;
console.log('Token successfully verified:', decoded);
const decoded = (await jwt.verify(refreshToken, config.refreshTokenSecret)) as JwtPayload;
if (decoded) {
const data = await strapi
.query('plugin::refresh-token.token')
.findOne({
where: { documentId: decoded.secret },
});
const data = await strapi.query('plugin::refresh-token.token').findOne({
where: { documentId: decoded.secret },
});

if (data) {
ctx.send({
Expand All @@ -64,4 +91,4 @@ function auth({ strapi }) {
}
};
}
export default auth;
export default auth;
2 changes: 1 addition & 1 deletion server/src/services/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const service = ({ strapi }: { strapi: Core.Strapi }) => ({
await strapi.query('plugin::refresh-token.token').delete({
where: { id: token.id },
});
console.log(`Deleted token with id: ${token.id}`);
// console.log(`Deleted token with id: ${token.id}`);
}
},
async create(user, request) {
Expand Down

0 comments on commit 729d787

Please sign in to comment.