mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-07 06:29:25 +08:00
fix(auth): should set user token as invalid when changing password (#5212)
* fix(auth): should log user out when changing password * fix: add passwordChangeTZ * fix: clear local token when token is invalid * fix: test * fix: field name
This commit is contained in:
parent
0a1ce687d7
commit
bd942342b0
@ -71,7 +71,7 @@ describe('middleware', () => {
|
||||
hasFn.mockImplementation(() => true);
|
||||
const res = await agent.resource('auth').check();
|
||||
expect(res.status).toBe(401);
|
||||
expect(res.text).toContain('token is not available');
|
||||
expect(res.text).toContain('Token is invalid');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -109,7 +109,10 @@ export class AuthManager {
|
||||
return async (ctx: Context & { auth: Auth }, next: Next) => {
|
||||
const token = ctx.getBearerToken();
|
||||
if (token && (await ctx.app.authManager.jwt.blacklist?.has(token))) {
|
||||
return ctx.throw(401, ctx.t('token is not available'));
|
||||
return ctx.throw(401, {
|
||||
code: 'TOKEN_INVALID',
|
||||
message: ctx.t('Token is invalid'),
|
||||
});
|
||||
}
|
||||
|
||||
const name = ctx.get(this.options.authKey) || this.options.default;
|
||||
|
@ -69,14 +69,14 @@ export class BaseAuth extends Auth {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const { userId, roleName } = await this.jwt.decode(token);
|
||||
const { userId, roleName, iat, temp } = await this.jwt.decode(token);
|
||||
|
||||
if (roleName) {
|
||||
this.ctx.headers['x-role'] = roleName;
|
||||
}
|
||||
|
||||
const cache = this.ctx.cache as Cache;
|
||||
return await cache.wrap(this.getCacheKey(userId), () =>
|
||||
const user = await cache.wrap(this.getCacheKey(userId), () =>
|
||||
this.userRepository.findOne({
|
||||
filter: {
|
||||
id: userId,
|
||||
@ -84,6 +84,10 @@ export class BaseAuth extends Auth {
|
||||
raw: true,
|
||||
}),
|
||||
);
|
||||
if (temp && user.passwordChangeTz && iat * 1000 < user.passwordChangeTz) {
|
||||
throw new Error('Token is invalid');
|
||||
}
|
||||
return user;
|
||||
} catch (err) {
|
||||
this.ctx.logger.error(err, { method: 'check' });
|
||||
return null;
|
||||
@ -106,6 +110,7 @@ export class BaseAuth extends Auth {
|
||||
}
|
||||
const token = this.jwt.sign({
|
||||
userId: user.id,
|
||||
temp: true,
|
||||
});
|
||||
return {
|
||||
user,
|
||||
@ -119,7 +124,7 @@ export class BaseAuth extends Auth {
|
||||
return;
|
||||
}
|
||||
const { userId } = await this.jwt.decode(token);
|
||||
await this.ctx.app.emitAsync('beforeSignOut', { userId });
|
||||
await this.ctx.app.emitAsync('cache:del:roles', { userId });
|
||||
await this.ctx.cache.del(this.getCacheKey(userId));
|
||||
return await this.jwt.block(token);
|
||||
}
|
||||
|
@ -98,6 +98,9 @@ export class APIClient extends APIClientSDK {
|
||||
if (errs.find((error: { code?: string }) => error.code === 'ROLE_NOT_FOUND_ERR')) {
|
||||
this.auth.setRole(null);
|
||||
}
|
||||
if (errs.find((error: { code?: string }) => error.code === 'TOKEN_INVALID')) {
|
||||
this.auth.setToken(null);
|
||||
}
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
@ -119,7 +122,7 @@ export class APIClient extends APIClientSDK {
|
||||
}
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
async (error) => {
|
||||
if (this.silence) {
|
||||
throw error;
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import React, { useContext, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActionContextProvider, DropdownVisibleContext, SchemaComponent, useActionContext } from '../';
|
||||
import { useAPIClient } from '../api-client';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const useCloseAction = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
@ -29,6 +30,7 @@ const useCloseAction = () => {
|
||||
};
|
||||
|
||||
const useSaveCurrentUserValues = () => {
|
||||
const navigate = useNavigate();
|
||||
const { setVisible } = useActionContext();
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
@ -40,6 +42,7 @@ const useSaveCurrentUserValues = () => {
|
||||
});
|
||||
await form.reset();
|
||||
setVisible(false);
|
||||
navigate('/signin');
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -7,9 +7,8 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { getLoggerFilePath } from './config';
|
||||
import { Logger, LoggerOptions } from './logger';
|
||||
import { pick } from 'lodash';
|
||||
import { pick, omit } from 'lodash';
|
||||
const defaultRequestWhitelist = [
|
||||
'action',
|
||||
'header.x-role',
|
||||
@ -21,6 +20,12 @@ const defaultRequestWhitelist = [
|
||||
'referer',
|
||||
];
|
||||
const defaultResponseWhitelist = ['status'];
|
||||
const defaultActionBlackList = [
|
||||
'params.values.password',
|
||||
'params.values.confirmPassword',
|
||||
'params.values.oldPassword',
|
||||
'params.values.newPassword',
|
||||
];
|
||||
|
||||
export interface RequestLoggerOptions extends LoggerOptions {
|
||||
skip?: (ctx?: any) => Promise<boolean>;
|
||||
@ -60,7 +65,7 @@ export const requestLogger = (appName: string, requestLogger: Logger, options?:
|
||||
message: `response ${ctx.url}`,
|
||||
...requestInfo,
|
||||
res: pick(ctx.response.toJSON(), options?.responseWhitelist || defaultResponseWhitelist),
|
||||
action: ctx.action?.toJSON?.(),
|
||||
action: omit(ctx.action?.toJSON?.(), defaultActionBlackList),
|
||||
userId: ctx.auth?.user?.id,
|
||||
status: ctx.status,
|
||||
cost,
|
||||
|
@ -45,6 +45,7 @@ export class Locale {
|
||||
name: 'locale',
|
||||
prefix: 'locale',
|
||||
store: 'memory',
|
||||
max: 2000
|
||||
});
|
||||
|
||||
await this.get(this.defaultLang);
|
||||
|
@ -123,6 +123,7 @@ export class MockServer extends Application {
|
||||
jwt.sign(
|
||||
{
|
||||
userId: typeof userOrId === 'number' ? userOrId : userOrId?.id,
|
||||
temp: true,
|
||||
},
|
||||
process.env.APP_KEY,
|
||||
{
|
||||
|
@ -408,7 +408,7 @@ export class PluginACLServer extends Plugin {
|
||||
});
|
||||
});
|
||||
|
||||
this.app.on('beforeSignOut', ({ userId }) => {
|
||||
this.app.on('cache:del:roles', ({ userId }) => {
|
||||
this.app.cache.del(`roles:${userId}`);
|
||||
});
|
||||
this.app.resourcer.use(setCurrentRole, { tag: 'setCurrentRole', before: 'acl', after: 'auth' });
|
||||
|
@ -102,6 +102,7 @@ describe('actions', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.db.clean({ drop: true });
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
@ -273,5 +274,28 @@ describe('actions', () => {
|
||||
expect(res.statusCode).toEqual(400);
|
||||
expect(res.error.text).toBe('Please enter a password');
|
||||
});
|
||||
|
||||
it('should sign user out when changing password', async () => {
|
||||
const userRepo = db.getRepository('users');
|
||||
const user = await userRepo.create({
|
||||
values: {
|
||||
username: 'test',
|
||||
password: '12345',
|
||||
},
|
||||
});
|
||||
const userAgent = await agent.login(user);
|
||||
const res = await userAgent.post('/auth:check').set({ 'X-Authenticator': 'basic' }).send();
|
||||
expect(res.statusCode).toEqual(200);
|
||||
expect(res.body.data.id).toBeDefined();
|
||||
const res2 = await userAgent.post('/auth:changePassword').set({ 'X-Authenticator': 'basic' }).send({
|
||||
oldPassword: '12345',
|
||||
newPassword: '123456',
|
||||
confirmPassword: '123456',
|
||||
});
|
||||
expect(res2.statusCode).toEqual(200);
|
||||
const res3 = await userAgent.post('/auth:check').set({ 'X-Authenticator': 'basic' }).send();
|
||||
expect(res3.statusCode).toEqual(200);
|
||||
expect(res3.body.data.id).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -29,15 +29,6 @@ export class PluginAuthServer extends Plugin {
|
||||
}
|
||||
|
||||
async load() {
|
||||
// Set up database
|
||||
await this.importCollections(resolve(__dirname, 'collections'));
|
||||
this.db.addMigrations({
|
||||
namespace: 'auth',
|
||||
directory: resolve(__dirname, 'migrations'),
|
||||
context: {
|
||||
plugin: this,
|
||||
},
|
||||
});
|
||||
this.cache = await this.app.cacheManager.createCache({
|
||||
name: 'auth',
|
||||
prefix: 'auth',
|
||||
@ -85,6 +76,9 @@ export class PluginAuthServer extends Plugin {
|
||||
const cache = this.app.cache as Cache;
|
||||
await cache.del(`auth:${user.id}`);
|
||||
});
|
||||
this.app.on('cache:del:auth', async ({ userId }) => {
|
||||
await this.cache.del(`auth:${userId}`);
|
||||
});
|
||||
}
|
||||
|
||||
async install(options?: InstallOptions) {
|
||||
|
@ -91,6 +91,10 @@ export default defineCollection({
|
||||
'x-component': 'Password',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'passwordChangeTz',
|
||||
type: 'bigInt',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'appLang',
|
||||
|
@ -7,11 +7,9 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Collection, Op } from '@nocobase/database';
|
||||
import { Collection, Model, Op } from '@nocobase/database';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { parse } from '@nocobase/utils';
|
||||
import { resolve } from 'path';
|
||||
import { Cache } from '@nocobase/cache';
|
||||
import * as actions from './actions/users';
|
||||
import { UserModel } from './models/UserModel';
|
||||
import PluginUserDataSyncServer from '@nocobase/plugin-user-data-sync';
|
||||
@ -155,19 +153,9 @@ export default class PluginUsersServer extends Plugin {
|
||||
}
|
||||
|
||||
async load() {
|
||||
await this.importCollections(resolve(__dirname, 'collections'));
|
||||
this.db.addMigrations({
|
||||
namespace: 'users',
|
||||
directory: resolve(__dirname, 'migrations'),
|
||||
context: {
|
||||
plugin: this,
|
||||
},
|
||||
});
|
||||
|
||||
this.app.resourcer.use(async (ctx, next) => {
|
||||
this.app.resourceManager.use(async (ctx, next) => {
|
||||
await next();
|
||||
const { associatedName, resourceName, actionName, values } = ctx.action.params;
|
||||
const cache = ctx.app.cache as Cache;
|
||||
if (
|
||||
associatedName === 'roles' &&
|
||||
resourceName === 'users' &&
|
||||
@ -175,9 +163,7 @@ export default class PluginUsersServer extends Plugin {
|
||||
values?.length
|
||||
) {
|
||||
// Delete cache when the members of a role changed
|
||||
for (const userId of values) {
|
||||
await cache.del(`roles:${userId}`);
|
||||
}
|
||||
await Promise.all(values.map((userId: number) => this.app.emitAsync('cache:del:roles', { userId })));
|
||||
}
|
||||
});
|
||||
|
||||
@ -185,6 +171,15 @@ export default class PluginUsersServer extends Plugin {
|
||||
if (userDataSyncPlugin && userDataSyncPlugin.enabled) {
|
||||
userDataSyncPlugin.resourceManager.registerResource(new UserDataSyncResource(this.db, this.app.logger));
|
||||
}
|
||||
|
||||
this.app.db.on('users.beforeUpdate', async (model: Model) => {
|
||||
if (!model._changed.has('password')) {
|
||||
return;
|
||||
}
|
||||
model.set('passwordChangeTz', Date.now());
|
||||
await this.app.emitAsync('cache:del:roles', { userId: model.get('id') });
|
||||
await this.app.emitAsync('cache:del:auth', { userId: model.get('id') });
|
||||
});
|
||||
}
|
||||
|
||||
getInstallingData(options: any = {}) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user