How to exclude entity field from returned by controller JSON. NestJS + Typeorm
Lots of good answers in this thread. To build on apun's answer above, I think the following approach is the least likely to accidentally leak a password field:
@Column({ select: false })
password: string
If the entity doesn't select that field by default, and it can only be explicitly queried (e.g. via addSelect()
if using the query builder), I think it is a lot less likely that there's a slip somewhere, and there's less reliance on the "magic" of a framework (which is ultimately the class-transformer
library) to ensure security. Realistically in many projects the only place you'd explicitly select it is where you check credentials.
This approach can also help keep the password hash from accidentally leaking into log entries, etc, which is a consideration that hasn't been mentioned yet. It feels much safer to toss around the user object knowing that it doesn't include sensitive information, especially if it could end up serialized in a log entry somewhere.
All said, the documented approach for NestJS is to use the @Exclude()
decorator and the accepted answer is from the project's founder.
I definitely make frequent use of the Exclude()
decorator, but not necessarily for password or salt fields.
You can overwrite the toJSON method of the model like this.
@Entity()
export class User extends BaseAbstractEntity implements IUser {
static passwordMinLength: number = 7;
@ApiModelProperty({ example: faker.internet.email() })
@IsEmail()
@Column({ unique: true })
email: string;
@IsOptional()
@IsString()
@MinLength(User.passwordMinLength)
@Exclude({ toPlainOnly: true })
@Column({ select: false })
password: string;
@IsOptional()
@IsString()
@Exclude({ toPlainOnly: true })
@Column({ select: false })
passwordSalt: string;
toJSON() {
return classToPlain(this);
}
validatePassword(password: string) {
if (!this.password || !this.passwordSalt) {
return false;
}
return comparedToHashed(password, this.password, this.passwordSalt);
}
}
By using the class-transformer method of plainToClass along with the @Exclude({ toPlainOnly: true }), the password will be excluded from the JSON response, but will be available in the model instance. I like this solution because it keeps all the model configuration in the entity.
As an addition to Kamil's answer:
Instead of creating your own interceptor, you can now use the built-in ClassSerializerInterceptor
, see the serialization docs.
@UseInterceptors(ClassSerializerInterceptor)
You can use it on a controller class or its individual methods. Each entity returned by such a method will be transformed with class-transformer and hence take the @Exclude
annotations into account:
import { Exclude } from 'class-transformer';
export class User {
/** other properties */
@Exclude()
password: string;
}
You can customize its behavior by defining @SerializeOptions()
on your controller or its methods:
@SerializeOptions({
excludePrefixes: ['_'],
groups: ['admin']
})
to, for example, expose certain fields only to certain users:
@Expose({ groups: ["admin"] })
adminInfo: string;
I'd suggest creating an interceptor that takes advantage of the class-transformer library:
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
return call$.pipe(map(data => classToPlain(data)));
}
}
Then, simply exclude properties using @Exclude()
decorator, for example:
import { Exclude } from 'class-transformer';
export class User {
id: number;
email: string;
@Exclude()
password: string;
}