From 0a8945d934a1f73f146033f8cba85ff6b05f367f Mon Sep 17 00:00:00 2001
From: Philip Molares <philip.molares@udo.edu>
Date: Sat, 13 May 2023 14:56:42 +0200
Subject: [PATCH] feat(backend): handle username always in lowercase

This should make all usernames of new users into lowercase. Usernames are also searched in the DB as lowercase.

Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
---
 backend/src/api/private/auth/auth.controller.ts     |  5 +++--
 backend/src/api/private/notes/notes.controller.ts   |  7 ++++---
 backend/src/api/private/users/users.controller.ts   |  5 +++--
 backend/src/api/public/notes/notes.controller.ts    |  9 +++++----
 backend/src/identity/ldap/ldap-login.dto.ts         |  4 ++--
 backend/src/identity/ldap/ldap.strategy.ts          | 10 ++++++----
 backend/src/identity/local/local.strategy.ts        |  5 +++--
 backend/src/identity/local/login.dto.ts             |  9 ++++++---
 backend/src/identity/local/register.dto.ts          |  9 ++++++---
 backend/src/media/media-upload.dto.ts               |  8 +++++---
 backend/src/notes/note-permissions.dto.ts           | 10 +++++++---
 .../realtime-note/realtime-connection.spec.ts       |  3 ++-
 .../realtime-note/realtime-note.service.spec.ts     |  6 +++---
 .../realtime-note/realtime-user-status-adapter.ts   |  4 +++-
 .../realtime-note/test-utils/mock-connection.ts     |  7 ++++---
 .../realtime/websocket/websocket.gateway.spec.ts    |  3 ++-
 backend/src/session/session.service.spec.ts         |  2 +-
 backend/src/session/session.service.ts              |  9 +++++----
 backend/src/users/user-info.dto.ts                  |  8 +++++---
 backend/src/users/user.entity.ts                    |  7 ++++---
 backend/src/users/users.service.ts                  | 13 +++++++------
 backend/src/utils/username.ts                       | 11 +++++++++++
 backend/test/private-api/auth.e2e-spec.ts           |  3 ++-
 23 files changed, 99 insertions(+), 58 deletions(-)
 create mode 100644 backend/src/utils/username.ts

diff --git a/backend/src/api/private/auth/auth.controller.ts b/backend/src/api/private/auth/auth.controller.ts
index f0e659b10..36055e08c 100644
--- a/backend/src/api/private/auth/auth.controller.ts
+++ b/backend/src/api/private/auth/auth.controller.ts
@@ -29,6 +29,7 @@ import { ConsoleLoggerService } from '../../../logger/console-logger.service';
 import { SessionState } from '../../../session/session.service';
 import { User } from '../../../users/user.entity';
 import { UsersService } from '../../../users/users.service';
+import { makeUsernameLowercase } from '../../../utils/username';
 import { LoginEnabledGuard } from '../../utils/login-enabled.guard';
 import { OpenApi } from '../../utils/openapi.decorator';
 import { RegistrationEnabledGuard } from '../../utils/registration-enabled.guard';
@@ -107,8 +108,8 @@ export class AuthController {
     @Param('ldapIdentifier') ldapIdentifier: string,
     @Body() loginDto: LdapLoginDto,
   ): void {
-    // There is no further testing needed as we only get to this point if LocalAuthGuard was successful
-    request.session.username = loginDto.username;
+    // There is no further testing needed as we only get to this point if LdapAuthGuard was successful
+    request.session.username = makeUsernameLowercase(loginDto.username);
     request.session.authProvider = 'ldap';
   }
 
diff --git a/backend/src/api/private/notes/notes.controller.ts b/backend/src/api/private/notes/notes.controller.ts
index 63609375e..fa64c4f23 100644
--- a/backend/src/api/private/notes/notes.controller.ts
+++ b/backend/src/api/private/notes/notes.controller.ts
@@ -39,6 +39,7 @@ import { RevisionDto } from '../../../revisions/revision.dto';
 import { RevisionsService } from '../../../revisions/revisions.service';
 import { User } from '../../../users/user.entity';
 import { UsersService } from '../../../users/users.service';
+import { Username } from '../../../utils/username';
 import { GetNoteInterceptor } from '../../utils/get-note.interceptor';
 import { MarkdownBody } from '../../utils/markdown-body.decorator';
 import { OpenApi } from '../../utils/openapi.decorator';
@@ -203,7 +204,7 @@ export class NotesController {
   async setUserPermission(
     @RequestUser() user: User,
     @RequestNote() note: Note,
-    @Param('userName') username: string,
+    @Param('userName') username: Username,
     @Body('canEdit') canEdit: boolean,
   ): Promise<NotePermissionsDto> {
     const permissionUser = await this.userService.getUserByUsername(username);
@@ -221,7 +222,7 @@ export class NotesController {
   async removeUserPermission(
     @RequestUser() user: User,
     @RequestNote() note: Note,
-    @Param('userName') username: string,
+    @Param('userName') username: Username,
   ): Promise<NotePermissionsDto> {
     try {
       const permissionUser = await this.userService.getUserByUsername(username);
@@ -281,7 +282,7 @@ export class NotesController {
   async changeOwner(
     @RequestUser() user: User,
     @RequestNote() note: Note,
-    @Body('newOwner') newOwner: string,
+    @Body('newOwner') newOwner: Username,
   ): Promise<NoteDto> {
     const owner = await this.userService.getUserByUsername(newOwner);
     return await this.noteService.toNoteDto(
diff --git a/backend/src/api/private/users/users.controller.ts b/backend/src/api/private/users/users.controller.ts
index b03798d1b..32133fc26 100644
--- a/backend/src/api/private/users/users.controller.ts
+++ b/backend/src/api/private/users/users.controller.ts
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
@@ -9,6 +9,7 @@ import { ApiTags } from '@nestjs/swagger';
 import { ConsoleLoggerService } from '../../../logger/console-logger.service';
 import { UserInfoDto } from '../../../users/user-info.dto';
 import { UsersService } from '../../../users/users.service';
+import { Username } from '../../../utils/username';
 import { OpenApi } from '../../utils/openapi.decorator';
 
 @ApiTags('users')
@@ -23,7 +24,7 @@ export class UsersController {
 
   @Get(':username')
   @OpenApi(200)
-  async getUser(@Param('username') username: string): Promise<UserInfoDto> {
+  async getUser(@Param('username') username: Username): Promise<UserInfoDto> {
     return this.userService.toUserDto(
       await this.userService.getUserByUsername(username),
     );
diff --git a/backend/src/api/public/notes/notes.controller.ts b/backend/src/api/public/notes/notes.controller.ts
index 802d10584..b2aab7bee 100644
--- a/backend/src/api/public/notes/notes.controller.ts
+++ b/backend/src/api/public/notes/notes.controller.ts
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
@@ -42,6 +42,7 @@ import { RevisionDto } from '../../../revisions/revision.dto';
 import { RevisionsService } from '../../../revisions/revisions.service';
 import { User } from '../../../users/user.entity';
 import { UsersService } from '../../../users/users.service';
+import { Username } from '../../../utils/username';
 import { GetNoteInterceptor } from '../../utils/get-note.interceptor';
 import { MarkdownBody } from '../../utils/markdown-body.decorator';
 import { OpenApi } from '../../utils/openapi.decorator';
@@ -264,7 +265,7 @@ export class NotesController {
   async setUserPermission(
     @RequestUser() user: User,
     @RequestNote() note: Note,
-    @Param('userName') username: string,
+    @Param('userName') username: Username,
     @Body('canEdit') canEdit: boolean,
   ): Promise<NotePermissionsDto> {
     const permissionUser = await this.userService.getUserByUsername(username);
@@ -291,7 +292,7 @@ export class NotesController {
   async removeUserPermission(
     @RequestUser() user: User,
     @RequestNote() note: Note,
-    @Param('userName') username: string,
+    @Param('userName') username: Username,
   ): Promise<NotePermissionsDto> {
     try {
       const permissionUser = await this.userService.getUserByUsername(username);
@@ -377,7 +378,7 @@ export class NotesController {
   async changeOwner(
     @RequestUser() user: User,
     @RequestNote() note: Note,
-    @Body('newOwner') newOwner: string,
+    @Body('newOwner') newOwner: Username,
   ): Promise<NoteDto> {
     const owner = await this.userService.getUserByUsername(newOwner);
     return await this.noteService.toNoteDto(
diff --git a/backend/src/identity/ldap/ldap-login.dto.ts b/backend/src/identity/ldap/ldap-login.dto.ts
index ebccf6d7a..6926921f7 100644
--- a/backend/src/identity/ldap/ldap-login.dto.ts
+++ b/backend/src/identity/ldap/ldap-login.dto.ts
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
@@ -7,7 +7,7 @@ import { IsString } from 'class-validator';
 
 export class LdapLoginDto {
   @IsString()
-  username: string;
+  username: string; // This is not of type Username, because LDAP server may use mixed case usernames
   @IsString()
   password: string;
 }
diff --git a/backend/src/identity/ldap/ldap.strategy.ts b/backend/src/identity/ldap/ldap.strategy.ts
index 0c0c12495..e8bc69401 100644
--- a/backend/src/identity/ldap/ldap.strategy.ts
+++ b/backend/src/identity/ldap/ldap.strategy.ts
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
@@ -22,6 +22,7 @@ import authConfiguration, {
 import { NotInDBError } from '../../errors/errors';
 import { ConsoleLoggerService } from '../../logger/console-logger.service';
 import { UsersService } from '../../users/users.service';
+import { makeUsernameLowercase } from '../../utils/username';
 import { Identity } from '../identity.entity';
 import { IdentityService } from '../identity.service';
 import { ProviderType } from '../provider-type.enum';
@@ -85,7 +86,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') {
    */
   private loginWithLDAP(
     ldapConfig: LDAPConfig,
-    username: string,
+    username: string, // This is not of type Username, because LDAP server may use mixed case usernames
     password: string,
     doneCallBack: VerifiedCallback,
   ): void {
@@ -146,7 +147,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') {
     userId: string,
     ldapConfig: LDAPConfig,
     user: Record<string, string>,
-    username: string,
+    username: string, // This is not of type Username, because LDAP server may use mixed case usernames
   ): void {
     this.identityService
       .getIdentityFromUserIdAndProviderType(userId, ProviderType.LDAP)
@@ -162,8 +163,9 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') {
       .catch(async (error) => {
         if (error instanceof NotInDBError) {
           // The user/identity does not yet exist
+          const usernameLowercase = makeUsernameLowercase(username); // This ensures ldap user can be given permission via usernames
           const newUser = await this.usersService.createUser(
-            username,
+            usernameLowercase,
             // if there is no displayName we use the username
             user[ldapConfig.displayNameField] ?? username,
           );
diff --git a/backend/src/identity/local/local.strategy.ts b/backend/src/identity/local/local.strategy.ts
index 59bfc5900..a70850ca6 100644
--- a/backend/src/identity/local/local.strategy.ts
+++ b/backend/src/identity/local/local.strategy.ts
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
@@ -15,6 +15,7 @@ import { ConsoleLoggerService } from '../../logger/console-logger.service';
 import { UserRelationEnum } from '../../users/user-relation.enum';
 import { User } from '../../users/user.entity';
 import { UsersService } from '../../users/users.service';
+import { Username } from '../../utils/username';
 import { IdentityService } from '../identity.service';
 
 @Injectable()
@@ -31,7 +32,7 @@ export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
     logger.setContext(LocalStrategy.name);
   }
 
-  async validate(username: string, password: string): Promise<User> {
+  async validate(username: Username, password: string): Promise<User> {
     try {
       const user = await this.userService.getUserByUsername(username, [
         UserRelationEnum.IDENTITIES,
diff --git a/backend/src/identity/local/login.dto.ts b/backend/src/identity/local/login.dto.ts
index 290c52456..2e4d91a88 100644
--- a/backend/src/identity/local/login.dto.ts
+++ b/backend/src/identity/local/login.dto.ts
@@ -1,13 +1,16 @@
 /*
- * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
-import { IsString } from 'class-validator';
+import { IsLowercase, IsString } from 'class-validator';
+
+import { Username } from '../../utils/username';
 
 export class LoginDto {
   @IsString()
-  username: string;
+  @IsLowercase()
+  username: Username;
   @IsString()
   password: string;
 }
diff --git a/backend/src/identity/local/register.dto.ts b/backend/src/identity/local/register.dto.ts
index 8ce73a52f..54bc61cf9 100644
--- a/backend/src/identity/local/register.dto.ts
+++ b/backend/src/identity/local/register.dto.ts
@@ -1,13 +1,16 @@
 /*
- * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
-import { IsString } from 'class-validator';
+import { IsLowercase, IsString } from 'class-validator';
+
+import { Username } from '../../utils/username';
 
 export class RegisterDto {
   @IsString()
-  username: string;
+  @IsLowercase()
+  username: Username;
 
   @IsString()
   displayName: string;
diff --git a/backend/src/media/media-upload.dto.ts b/backend/src/media/media-upload.dto.ts
index b3d3fe2c0..bb4044920 100644
--- a/backend/src/media/media-upload.dto.ts
+++ b/backend/src/media/media-upload.dto.ts
@@ -1,13 +1,14 @@
 /*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 import { ApiProperty } from '@nestjs/swagger';
 import { Type } from 'class-transformer';
-import { IsDate, IsOptional, IsString } from 'class-validator';
+import { IsDate, IsLowercase, IsOptional, IsString } from 'class-validator';
 
 import { BaseDto } from '../utils/base.dto.';
+import { Username } from '../utils/username';
 
 export class MediaUploadDto extends BaseDto {
   /**
@@ -41,6 +42,7 @@ export class MediaUploadDto extends BaseDto {
    * @example "testuser5"
    */
   @IsString()
+  @IsLowercase()
   @ApiProperty()
-  username: string | null;
+  username: Username | null;
 }
diff --git a/backend/src/notes/note-permissions.dto.ts b/backend/src/notes/note-permissions.dto.ts
index 56424f68a..30372010e 100644
--- a/backend/src/notes/note-permissions.dto.ts
+++ b/backend/src/notes/note-permissions.dto.ts
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
@@ -8,20 +8,23 @@ import { Type } from 'class-transformer';
 import {
   IsArray,
   IsBoolean,
+  IsLowercase,
   IsOptional,
   IsString,
   ValidateNested,
 } from 'class-validator';
 
 import { BaseDto } from '../utils/base.dto.';
+import { Username } from '../utils/username';
 
 export class NoteUserPermissionEntryDto extends BaseDto {
   /**
    * Username of the User this permission applies to
    */
   @IsString()
+  @IsLowercase()
   @ApiProperty()
-  username: string;
+  username: Username;
 
   /**
    * True if the user is allowed to edit the note
@@ -38,8 +41,9 @@ export class NoteUserPermissionUpdateDto {
    * @example "john.smith"
    */
   @IsString()
+  @IsLowercase()
   @ApiProperty()
-  username: string;
+  username: Username;
 
   /**
    * True if the user should be allowed to edit the note
diff --git a/backend/src/realtime/realtime-note/realtime-connection.spec.ts b/backend/src/realtime/realtime-note/realtime-connection.spec.ts
index b72e759bb..d8a520bff 100644
--- a/backend/src/realtime/realtime-note/realtime-connection.spec.ts
+++ b/backend/src/realtime/realtime-note/realtime-connection.spec.ts
@@ -13,6 +13,7 @@ import { Mock } from 'ts-mockery';
 
 import { Note } from '../../notes/note.entity';
 import { User } from '../../users/user.entity';
+import { Username } from '../../utils/username';
 import * as NameRandomizerModule from './random-word-lists/name-randomizer';
 import { RealtimeConnection } from './realtime-connection';
 import { RealtimeNote } from './realtime-note';
@@ -39,7 +40,7 @@ describe('websocket connection', () => {
   let mockedUser: User;
   let mockedMessageTransporter: MessageTransporter;
 
-  const mockedUserName = 'mockedUserName';
+  const mockedUserName: Username = 'mocked-user-name';
   const mockedDisplayName = 'mockedDisplayName';
 
   beforeEach(() => {
diff --git a/backend/src/realtime/realtime-note/realtime-note.service.spec.ts b/backend/src/realtime/realtime-note/realtime-note.service.spec.ts
index 2595bb55d..35f60e3d4 100644
--- a/backend/src/realtime/realtime-note/realtime-note.service.spec.ts
+++ b/backend/src/realtime/realtime-note/realtime-note.service.spec.ts
@@ -40,9 +40,9 @@ describe('RealtimeNoteService', () => {
   let clientWithoutReadWrite: RealtimeConnection;
   let deleteIntervalSpy: jest.SpyInstance;
 
-  const readWriteUsername = 'canReadWriteUser';
-  const onlyReadUsername = 'canOnlyReadUser';
-  const noAccessUsername = 'noReadWriteUser';
+  const readWriteUsername = 'can-read-write-user';
+  const onlyReadUsername = 'can-only-read-user';
+  const noAccessUsername = 'no-read-write-user';
 
   afterAll(() => {
     jest.useRealTimers();
diff --git a/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts b/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts
index ff8a9eccf..fef3f7983 100644
--- a/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts
+++ b/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts
@@ -11,6 +11,8 @@ import {
 } from '@hedgedoc/commons';
 import { Listener } from 'eventemitter2';
 
+import { Username } from '../../utils/username';
+
 export type OtherAdapterCollector = () => RealtimeUserStatusAdapter[];
 
 /**
@@ -20,7 +22,7 @@ export class RealtimeUserStatusAdapter {
   private readonly realtimeUser: RealtimeUser;
 
   constructor(
-    private readonly username: string | null,
+    private readonly username: Username | null,
     private readonly displayName: string,
     private collectOtherAdapters: OtherAdapterCollector,
     private messageTransporter: MessageTransporter,
diff --git a/backend/src/realtime/realtime-note/test-utils/mock-connection.ts b/backend/src/realtime/realtime-note/test-utils/mock-connection.ts
index 9ffb1660c..d0a264af1 100644
--- a/backend/src/realtime/realtime-note/test-utils/mock-connection.ts
+++ b/backend/src/realtime/realtime-note/test-utils/mock-connection.ts
@@ -11,6 +11,7 @@ import {
 import { Mock } from 'ts-mockery';
 
 import { User } from '../../../users/user.entity';
+import { Username } from '../../../utils/username';
 import { RealtimeConnection } from '../realtime-connection';
 import { RealtimeNote } from '../realtime-note';
 import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter';
@@ -21,13 +22,13 @@ enum RealtimeUserState {
   WITH_READONLY,
 }
 
-const MOCK_FALLBACK_USERNAME = 'mock';
+const MOCK_FALLBACK_USERNAME: Username = 'mock';
 
 /**
  * Creates a mocked {@link RealtimeConnection realtime connection}.
  */
 export class MockConnectionBuilder {
-  private username: string | null;
+  private username: Username | null;
   private displayName: string | undefined;
   private includeRealtimeUserStatus: RealtimeUserState =
     RealtimeUserState.WITHOUT;
@@ -50,7 +51,7 @@ export class MockConnectionBuilder {
    *
    * @param username the username of the mocked user. If this value is omitted then the builder will user a {@link MOCK_FALLBACK_USERNAME fallback}.
    */
-  public withLoggedInUser(username?: string): this {
+  public withLoggedInUser(username?: Username): this {
     const newUsername = username ?? MOCK_FALLBACK_USERNAME;
     this.username = newUsername;
     this.displayName = newUsername;
diff --git a/backend/src/realtime/websocket/websocket.gateway.spec.ts b/backend/src/realtime/websocket/websocket.gateway.spec.ts
index 9545e22c3..1912cc958 100644
--- a/backend/src/realtime/websocket/websocket.gateway.spec.ts
+++ b/backend/src/realtime/websocket/websocket.gateway.spec.ts
@@ -41,6 +41,7 @@ import { Session } from '../../users/session.entity';
 import { User } from '../../users/user.entity';
 import { UsersModule } from '../../users/users.module';
 import { UsersService } from '../../users/users.service';
+import { Username } from '../../utils/username';
 import * as websocketConnectionModule from '../realtime-note/realtime-connection';
 import { RealtimeConnection } from '../realtime-note/realtime-connection';
 import { RealtimeNote } from '../realtime-note/realtime-note';
@@ -165,7 +166,7 @@ describe('Websocket gateway', () => {
           ),
       );
 
-    const mockUsername = 'mockUsername';
+    const mockUsername: Username = 'mock-username';
     jest
       .spyOn(sessionService, 'fetchUsernameForSessionId')
       .mockImplementation((sessionId: string) =>
diff --git a/backend/src/session/session.service.spec.ts b/backend/src/session/session.service.spec.ts
index cf803f43c..844178fd9 100644
--- a/backend/src/session/session.service.spec.ts
+++ b/backend/src/session/session.service.spec.ts
@@ -28,7 +28,7 @@ describe('SessionService', () => {
   let authConfigMock: AuthConfig;
   let typeormStoreConstructorMock: jest.SpyInstance;
   const mockedExistingSessionId = 'mockedExistingSessionId';
-  const mockUsername = 'mockUser';
+  const mockUsername = 'mock-user';
   const mockSecret = 'mockSecret';
   let sessionService: SessionService;
 
diff --git a/backend/src/session/session.service.ts b/backend/src/session/session.service.ts
index ca5c6c51f..27676ad22 100644
--- a/backend/src/session/session.service.ts
+++ b/backend/src/session/session.service.ts
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
@@ -19,10 +19,11 @@ import databaseConfiguration, {
 } from '../config/database.config';
 import { Session } from '../users/session.entity';
 import { HEDGEDOC_SESSION } from '../utils/session';
+import { Username } from '../utils/username';
 
 export interface SessionState {
   cookie: unknown;
-  username?: string;
+  username?: Username;
   authProvider: string;
 }
 
@@ -58,10 +59,10 @@ export class SessionService {
    * @param sessionId The session id for which the owning user should be found
    * @return A Promise that either resolves with the username or rejects with an error
    */
-  fetchUsernameForSessionId(sessionId: string): Promise<string | undefined> {
+  fetchUsernameForSessionId(sessionId: string): Promise<Username | undefined> {
     return new Promise((resolve, reject) => {
       this.typeormStore.get(sessionId, (error?: Error, result?: SessionState) =>
-        error || !result ? reject(error) : resolve(result.username),
+        error || !result ? reject(error) : resolve(result.username as Username),
       );
     });
   }
diff --git a/backend/src/users/user-info.dto.ts b/backend/src/users/user-info.dto.ts
index 29027128d..d26937da8 100644
--- a/backend/src/users/user-info.dto.ts
+++ b/backend/src/users/user-info.dto.ts
@@ -1,12 +1,13 @@
 /*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 import { ApiProperty } from '@nestjs/swagger';
-import { IsString } from 'class-validator';
+import { IsLowercase, IsString } from 'class-validator';
 
 import { BaseDto } from '../utils/base.dto.';
+import { Username } from '../utils/username';
 
 export class UserInfoDto extends BaseDto {
   /**
@@ -14,8 +15,9 @@ export class UserInfoDto extends BaseDto {
    * @example "john.smith"
    */
   @IsString()
+  @IsLowercase()
   @ApiProperty()
-  username: string;
+  username: Username;
 
   /**
    * The display name
diff --git a/backend/src/users/user.entity.ts b/backend/src/users/user.entity.ts
index 601dac0c7..85a2d15c3 100644
--- a/backend/src/users/user.entity.ts
+++ b/backend/src/users/user.entity.ts
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
@@ -20,6 +20,7 @@ import { HistoryEntry } from '../history/history-entry.entity';
 import { Identity } from '../identity/identity.entity';
 import { MediaUpload } from '../media/media-upload.entity';
 import { Note } from '../notes/note.entity';
+import { Username } from '../utils/username';
 
 @Entity()
 export class User {
@@ -29,7 +30,7 @@ export class User {
   @Column({
     unique: true,
   })
-  username: string;
+  username: Username;
 
   @Column()
   displayName: string;
@@ -77,7 +78,7 @@ export class User {
   private constructor() {}
 
   public static create(
-    username: string,
+    username: Username,
     displayName: string,
   ): Omit<User, 'id' | 'createdAt' | 'updatedAt'> {
     const newUser = new User();
diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts
index 3664e75bf..2712d3102 100644
--- a/backend/src/users/users.service.ts
+++ b/backend/src/users/users.service.ts
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
@@ -9,6 +9,7 @@ import { Repository } from 'typeorm';
 
 import { AlreadyInDBError, NotInDBError } from '../errors/errors';
 import { ConsoleLoggerService } from '../logger/console-logger.service';
+import { Username } from '../utils/username';
 import {
   FullUserInfoDto,
   UserInfoDto,
@@ -29,12 +30,12 @@ export class UsersService {
   /**
    * @async
    * Create a new user with a given username and displayName
-   * @param username - the username the new user shall have
-   * @param displayName - the display name the new user shall have
+   * @param {Username} username - the username the new user shall have
+   * @param {string} displayName - the display name the new user shall have
    * @return {User} the user
    * @throws {AlreadyInDBError} the username is already taken.
    */
-  async createUser(username: string, displayName: string): Promise<User> {
+  async createUser(username: Username, displayName: string): Promise<User> {
     const user = User.create(username, displayName);
     try {
       return await this.userRepository.save(user);
@@ -77,12 +78,12 @@ export class UsersService {
   /**
    * @async
    * Get the user specified by the username
-   * @param {string} username the username by which the user is specified
+   * @param {Username} username the username by which the user is specified
    * @param {UserRelationEnum[]} [withRelations=[]] if the returned user object should contain certain relations
    * @return {User} the specified user
    */
   async getUserByUsername(
-    username: string,
+    username: Username,
     withRelations: UserRelationEnum[] = [],
   ): Promise<User> {
     const user = await this.userRepository.findOne({
diff --git a/backend/src/utils/username.ts b/backend/src/utils/username.ts
new file mode 100644
index 000000000..4ea20fc70
--- /dev/null
+++ b/backend/src/utils/username.ts
@@ -0,0 +1,11 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type Username = Lowercase<string>;
+
+export function makeUsernameLowercase(username: string): Username {
+  return username.toLowerCase() as Username;
+}
diff --git a/backend/test/private-api/auth.e2e-spec.ts b/backend/test/private-api/auth.e2e-spec.ts
index ac682d6b5..d77b8f6d4 100644
--- a/backend/test/private-api/auth.e2e-spec.ts
+++ b/backend/test/private-api/auth.e2e-spec.ts
@@ -16,12 +16,13 @@ import { RegisterDto } from '../../src/identity/local/register.dto';
 import { UpdatePasswordDto } from '../../src/identity/local/update-password.dto';
 import { UserRelationEnum } from '../../src/users/user-relation.enum';
 import { checkPassword } from '../../src/utils/password';
+import { Username } from '../../src/utils/username';
 import { TestSetup, TestSetupBuilder } from '../test-setup';
 
 describe('Auth', () => {
   let testSetup: TestSetup;
 
-  let username: string;
+  let username: Username;
   let displayName: string;
   let password: string;