Skip to content

Commit 746c72a

Browse files
committed
Changing password requires old password
1 parent dafa63c commit 746c72a

File tree

10 files changed

+114
-23
lines changed

10 files changed

+114
-23
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Admin must be set manually as a string in permissions for the first user (add `a
104104

105105
Users can modify or view their own data. Admins can do anything except refresh another user's token, which would allow the admin to impersonate that user.
106106

107-
The `UsernameEmailGuard` compares the user's email or username with the same field in a query. If any query or mutation in the resolver has `doAnythingWithUser(username: string)` or `doAnythingWithUser(email: string)` and that email / username matches the user which is requesting the action, it will be approved. Username and email are unique, and the user has already been verified via JWT.
107+
The `UsernameEmailGuard` compares the user's email or username with the same field in a query. If any query or mutation in the resolver has `doAnythingWithUser(username: string)` or `doAnythingWithUser(email: string)` and that email / username matches the user which is requesting the action, it will be approved. Username and email are unique, and the user has already been verified via JWT. **If there is not a username or email in the request, it will pass.** This is because the resolvers will set the action on the user making the request. For example, on updateUser if no username is specified, the modification is on the user making the request.
108108

109109
The `UsernameEmailAdminGuard` is the same as the `UsernameEmailGuard` except it also allows admins.
110110

@@ -269,7 +269,6 @@ mutation updateUser($updateUser: UpdateUserInput!) {
269269
{
270270
"updateUser": {
271271
"username": "newUserName",
272-
"password": "newPassword",
273272
"email": "[email protected]",
274273
"enabled": false
275274
}

src/auth/auth.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Module } from '@nestjs/common';
1+
import { Module, forwardRef } from '@nestjs/common';
22
import { AuthService } from './auth.service';
33
import { PassportModule } from '@nestjs/passport';
44
import { JwtModule, JwtModuleOptions } from '@nestjs/jwt';
@@ -26,7 +26,7 @@ import { ConfigModule } from '../config/config.module';
2626
},
2727
inject: [ConfigService],
2828
}),
29-
UsersModule,
29+
forwardRef(() => UsersModule),
3030
ConfigModule,
3131
],
3232
controllers: [],

src/auth/auth.resolvers.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,11 @@ import { AuthService } from './auth.service';
44
import { AuthenticationError } from 'apollo-server-core';
55
import { JwtAuthGuard } from './guards/jwt-auth.guard';
66
import { UseGuards } from '@nestjs/common';
7-
import { UsernameEmailGuard } from './guards/username-email.guard';
8-
import { UsersService } from '../users/users.service';
97
import { UserDocument } from '../users/schemas/user.schema';
108

119
@Resolver('Auth')
1210
export class AuthResolver {
13-
constructor(
14-
private authService: AuthService,
15-
private usersService: UsersService,
16-
) {}
11+
constructor(private authService: AuthService) {}
1712

1813
@Query('login')
1914
async login(@Args('user') user: LoginUserInput): Promise<LoginResult> {

src/auth/auth.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { Injectable, forwardRef, Inject } from '@nestjs/common';
22
import { JwtService } from '@nestjs/jwt';
33
import { UsersService } from '../users/users.service';
44
import { JwtPayload } from './interfaces/jwt-payload.interface';
@@ -10,6 +10,7 @@ import { resolve } from 'path';
1010
@Injectable()
1111
export class AuthService {
1212
constructor(
13+
@Inject(forwardRef(() => UsersService))
1314
private usersService: UsersService,
1415
private jwtService: JwtService,
1516
private configService: ConfigService,

src/graphql.classes.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@ export class LoginUserInput {
1717
password: string;
1818
}
1919

20+
export class UpdatePasswordInput {
21+
oldPassword: string;
22+
newPassword: string;
23+
}
24+
2025
export class UpdateUserInput {
2126
username?: string;
2227
email?: string;
23-
password?: string;
28+
password?: UpdatePasswordInput;
2429
enabled?: boolean;
2530
}
2631

@@ -32,7 +37,7 @@ export class LoginResult {
3237
export abstract class IMutation {
3338
abstract createUser(createUserInput?: CreateUserInput): User | Promise<User>;
3439

35-
abstract updateUser(username: string, fieldsToUpdate: UpdateUserInput): User | Promise<User>;
40+
abstract updateUser(fieldsToUpdate: UpdateUserInput, username?: string): User | Promise<User>;
3641

3742
abstract addAdminPermission(username: string): User | Promise<User>;
3843

src/users/users.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
import { Module } from '@nestjs/common';
1+
import { Module, forwardRef } from '@nestjs/common';
22
import { UsersService } from './users.service';
33
import { MongooseModule } from '@nestjs/mongoose';
44
import { UserSchema } from './schemas/user.schema';
55
import { UserResolver } from './users.resolvers';
66
import { DateScalar } from '../scalars/date.scalar';
77
import { ConfigModule } from '../config/config.module';
8+
import { AuthModule } from '../auth/auth.module';
89

910
@Module({
1011
imports: [
1112
MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),
1213
UsersModule,
1314
ConfigModule,
15+
forwardRef(() => AuthModule),
1416
],
1517
exports: [UsersService],
1618
controllers: [],

src/users/users.resolvers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,10 @@ export class UserResolver {
7878
async updateUser(
7979
@Args('username') username: string,
8080
@Args('fieldsToUpdate') fieldsToUpdate: UpdateUserInput,
81+
@Context('req') request: any,
8182
): Promise<User> {
8283
let user: UserDocument | undefined;
84+
if (!username && request.user) username = request.user.username;
8385
try {
8486
user = await this.usersService.update(username, fieldsToUpdate);
8587
} catch (error) {

src/users/users.service.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import { randomBytes } from 'crypto';
77
import { createTransport, SendMailOptions } from 'nodemailer';
88
import { ConfigService } from '../config/config.service';
99
import { MongoError } from 'mongodb';
10+
import { AuthService } from '../auth/auth.service';
1011

1112
@Injectable()
1213
export class UsersService {
1314
constructor(
1415
@InjectModel('User') private readonly userModel: Model<UserDocument>,
1516
private configService: ConfigService,
17+
private authService: AuthService,
1618
) {}
1719

1820
/**
@@ -97,11 +99,22 @@ export class UsersService {
9799
if (duplicateUser || !emailValid) fieldsToUpdate.email = undefined;
98100
}
99101

100-
const fields = {};
102+
const fields: any = {};
103+
104+
if (fieldsToUpdate.password) {
105+
if (
106+
await this.authService.validateUserByPassword({
107+
username,
108+
password: fieldsToUpdate.password.oldPassword,
109+
})
110+
) {
111+
fields.password = fieldsToUpdate.password.newPassword;
112+
}
113+
}
101114

102115
// Remove undefined keys for update
103116
for (const key in fieldsToUpdate) {
104-
if (typeof fieldsToUpdate[key] !== 'undefined') {
117+
if (typeof fieldsToUpdate[key] !== 'undefined' && key !== 'password') {
105118
fields[key] = fieldsToUpdate[key];
106119
}
107120
}

src/users/users.types.graphql

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type Query {
99

1010
type Mutation {
1111
createUser(createUserInput: CreateUserInput): User!
12-
updateUser(username: String!, fieldsToUpdate: UpdateUserInput!): User!
12+
updateUser(fieldsToUpdate: UpdateUserInput!, username: String): User!
1313
addAdminPermission(username: String!): User!
1414
removeAdminPermission(username: String!): User!
1515
resetPassword(username: String!, code: String!, password: String!): User!
@@ -35,6 +35,11 @@ input CreateUserInput {
3535
input UpdateUserInput {
3636
username: String
3737
email: String
38-
password: String
38+
password: UpdatePasswordInput
3939
enabled: Boolean
4040
}
41+
42+
input UpdatePasswordInput {
43+
oldPassword: String!
44+
newPassword: String!
45+
}

test/users.e2e-spec.ts

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,10 @@ describe('Users (e2e)', () => {
794794
fieldsToUpdate: {
795795
username: "newUsername1",
796796
797-
password: "newPassword",
797+
password: {
798+
oldPassword: "password",
799+
newPassword: "newPassword",
800+
}
798801
enabled: true
799802
}) {username, email, enabled}}`,
800803
};
@@ -873,7 +876,10 @@ describe('Users (e2e)', () => {
873876
fieldsToUpdate: {
874877
username: "newUsernameByAdmin",
875878
876-
password: "newPassword"
879+
password: {
880+
oldPassword: "password",
881+
newPassword: "newPassword",
882+
}
877883
}) {username, email}}`,
878884
};
879885

@@ -915,7 +921,10 @@ describe('Users (e2e)', () => {
915921
fieldsToUpdate: {
916922
username: "user1",
917923
918-
password:"newPassword"
924+
password: {
925+
oldPassword: "password",
926+
newPassword: "newPassword",
927+
}
919928
}) {username, email}}`,
920929
};
921930

@@ -958,7 +967,10 @@ describe('Users (e2e)', () => {
958967
fieldsToUpdate: {
959968
username: "newUsername3",
960969
961-
password:"newPassword"
970+
password: {
971+
oldPassword: "password",
972+
newPassword: "newPassword",
973+
}
962974
}) {username, email}}`,
963975
};
964976

@@ -1001,7 +1013,10 @@ describe('Users (e2e)', () => {
10011013
fieldsToUpdate: {
10021014
username: "newUsername5",
10031015
email:"invalidEmail",
1004-
password:"newPassword"
1016+
password: {
1017+
oldPassword: "password",
1018+
newPassword: "newPassword",
1019+
}
10051020
}) {username, email}}`,
10061021
};
10071022

@@ -1053,6 +1068,60 @@ describe('Users (e2e)', () => {
10531068
).toBeTruthy();
10541069
});
10551070

1071+
it(`update other fields with bad old password - also verifies updates requesting user's info
1072+
with no username set`, async () => {
1073+
await usersService.create({
1074+
username: 'userToUpdate55',
1075+
1076+
password: 'password',
1077+
});
1078+
1079+
const result = await authService.validateUserByPassword({
1080+
username: 'userToUpdate55',
1081+
password: 'password',
1082+
});
1083+
const token = result!.token;
1084+
1085+
const data = {
1086+
query: `mutation {
1087+
updateUser(
1088+
fieldsToUpdate: {
1089+
username: "newUsername55",
1090+
1091+
password: {
1092+
oldPassword: "notthepassword",
1093+
newPassword: "newPassword",
1094+
}
1095+
enabled: true
1096+
}) {username, email, enabled}}`,
1097+
};
1098+
1099+
await request(app.getHttpServer())
1100+
.post('/graphql')
1101+
.set('Authorization', `Bearer ${token!}`)
1102+
.send(data)
1103+
.expect(200)
1104+
.expect(response => {
1105+
expect(response.body.data.updateUser).toMatchObject({
1106+
username: 'newUsername55',
1107+
1108+
enabled: true,
1109+
});
1110+
});
1111+
1112+
let login = await authService.validateUserByPassword({
1113+
username: 'newUsername55',
1114+
password: 'newPassword',
1115+
});
1116+
expect(login).toBeFalsy();
1117+
1118+
login = await authService.validateUserByPassword({
1119+
username: 'newUsername55',
1120+
password: 'password',
1121+
});
1122+
expect(login).toBeTruthy();
1123+
});
1124+
10561125
it('fails to update with wrong username', async () => {
10571126
const data = {
10581127
query: `mutation {

0 commit comments

Comments
 (0)