USE [main] -- Session Management Cheatsheet: -- https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html IF OBJECT_ID('dbo.[ActivityLog]', 'U') IS NOT NULL DROP TABLE [ActivityLog] IF OBJECT_ID('dbo.[Session]', 'U') IS NOT NULL DROP TABLE [Session] IF OBJECT_ID('dbo.[Event]', 'U') IS NOT NULL DROP TABLE [Event] IF OBJECT_ID('dbo.[EventLevel]', 'U') IS NOT NULL DROP TABLE [EventLevel] IF OBJECT_ID('dbo.[Login]', 'U') IS NOT NULL DROP TABLE [Login] IF OBJECT_ID('dbo.[User]', 'U') IS NOT NULL DROP TABLE [User] CREATE TABLE [User] ( [User ID] INT IDENTITY(1,1), [Name] NVARCHAR(64) UNIQUE NOT NULL, [Email] NVARCHAR(64) UNIQUE NOT NULL, PRIMARY KEY([User ID]) ) CREATE TABLE [Login] ( [Login ID] INT NOT NULL IDENTITY(1, 1), [Password Hash] VARCHAR(1024) NOT NULL, [Salt] VARCHAR(512) NOT NULL, [User Key] INT NOT NULL, [Login Attempts] INT NOT NULL DEFAULT 0, [Last Login Attempt] DATETIME, PRIMARY KEY([Login ID]), FOREIGN KEY ([User Key]) REFERENCES [User]([User ID]) ) CREATE TABLE [Session] ( [Session ID] INT NOT NULL IDENTITY(1, 1), [Session Token] VARCHAR(1024) UNIQUE NOT NULL, [Expiration Date] DATETIME NOT NULL, [User ID] INT NOT NULL, [IP Address] VARCHAR(256) NOT NULL, [User Agent] VARCHAR(256) NOT NULL, [Pending Reset] BIT NOT NULL DEFAULT 0, PRIMARY KEY([Session ID]), FOREIGN KEY([User ID]) REFERENCES [User]([User ID]) ) CREATE TABLE [EventLevel] ( [Event Level ID] TINYINT NOT NULL, [Event Display Name] VARCHAR(64) NOT NULL, PRIMARY KEY([Event Level ID]) ) CREATE TABLE [Event] ( [Event ID] INT NOT NULL IDENTITY(1, 1), [Event Name] VARCHAR(128) NOT NULL, [Event Level ID] TINYINT NOT NULL, PRIMARY KEY([Event ID]), FOREIGN KEY([Event Level ID]) REFERENCES [EventLevel]([Event Level ID]) ) CREATE TABLE [ActivityLog] ( [Log ID] INT NOT NULL IDENTITY(1, 1), [Event ID] INT NOT NULL, [Event Description] NVARCHAR(512) NOT NULL, [Timestamp] DATETIME NOT NULL, [Component] VARCHAR(128) NOT NULL, PRIMARY KEY([Log ID]), FOREIGN KEY([Event ID]) REFERENCES [Event] ([Event ID]) ) -- The smaller the number the higher the severity. INSERT INTO [EventLevel] ([Event Level ID], [Event Display Name]) VALUES(0, 'Critical') INSERT INTO [EventLevel] ([Event Level ID], [Event Display Name]) VALUES(1, 'Error') INSERT INTO [EventLevel] ([Event Level ID], [Event Display Name]) VALUES(2, 'Warning') INSERT INTO [EventLevel] ([Event Level ID], [Event Display Name]) VALUES(3, 'Information') INSERT INTO [EventLevel] ([Event Level ID], [Event Display Name]) VALUES(255, 'Debug') INSERT INTO [Event] ([Event Name], [Event Level ID]) VALUES ('Login', 3) INSERT INTO [Event] ([Event Name], [Event Level ID]) VALUES ('Logout', 3) INSERT INTO [Event] ([Event Name], [Event Level ID]) VALUES ('Registration', 3) INSERT INTO [Event] ([Event Name], [Event Level ID]) VALUES ('Session Expiration', 3) INSERT INTO [Event] ([Event Name], [Event Level ID]) VALUES ('Password Reset Request', 3) INSERT INTO [Event] ([Event Name], [Event Level ID]) VALUES ('Password Reset', 3) /* INSERT INTO [User] (Name, Email) VALUES ('glm94', 'glmccune@gmail.com') INSERT INTO [User] (Name, Email) VALUES ('admin', 'admin@gmail.com') INSERT INTO [User] (Name, Email) VALUES ('nobody', 'none@gmail.com') */ -- Define Functions GO /****** Object: UserDefinedFunction [dbo].[UserExists] Script Date: 1/13/2021 7:23:30 PM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- ============================================= -- Author: -- Create date: -- Description: -- ============================================= CREATE OR ALTER FUNCTION [dbo].[UserExists] ( @UserName NVARCHAR(512) ) RETURNS BIT AS BEGIN DECLARE @Exists BIT = 0 IF EXISTS ( SELECT 1 FROM [User] WHERE [Name] = @UserName ) BEGIN SET @Exists = 1; END RETURN @Exists; END GO GO /****** Object: UserDefinedFunction [dbo].[UserExists] Script Date: 1/14/2021 5:31:27 PM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- ============================================= -- Author: -- Create date: -- Description: -- ============================================= CREATE OR ALTER FUNCTION [dbo].[EmailInUse] ( @Email NVARCHAR(512) ) RETURNS BIT AS BEGIN DECLARE @Exists BIT = 0 IF EXISTS ( SELECT 1 FROM [User] WHERE [Email] = @Email ) BEGIN SET @Exists = 1; END RETURN @Exists; END GO GO -- Define Stored Procedures /****** Object: UserDefinedFunction [dbo].[GetUserPasswordHashAndSalt] Script Date: 1/13/2021 7:23:30 PM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- ============================================= -- Author: -- Create date: -- Description: -- ============================================= CREATE OR ALTER FUNCTION [dbo].[GetUserPasswordHashAndSalt] ( @Username NVARCHAR(64) ) RETURNS TABLE AS RETURN ( SELECT [Login].[Password Hash], [Login].Salt FROM [Login] INNER JOIN [User] ON [User].[User ID] = [Login].[User Key] WHERE [User].[Name] = @Username ) GO /****** Object: StoredProcedure [dbo].[LoginUser] Script Date: 1/13/2021 7:23:30 PM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- ============================================= -- Author: -- Create date: -- Description: -- ============================================= CREATE OR ALTER PROCEDURE [dbo].[LoginUser] @UserName NVARCHAR(64), @SessionToken VARCHAR(1024), @ExpirationDate DATETIME, @IpAddress VARCHAR(256), @UserAgent VARCHAR(256) AS BEGIN -- SET NOCOUNT ON added to prevent extra result sets from -- interfering with SELECT statements. SET NOCOUNT, XACT_ABORT ON; DECLARE @UserID INT DECLARE @EventID INT DECLARE @Description NVARCHAR(512) SELECT @UserID = [User ID] FROM [User] WHERE [User].[Name] = @UserName BEGIN TRY BEGIN TRANSACTION; IF dbo.UserExists(@UserName) = 0 BEGIN; THROW 51000, 'User does not exist', 1; END; INSERT INTO [Session] ([Session Token], [Expiration Date], [User ID], [IP Address], [User Agent], [Pending Reset]) VALUES (@SessionToken, @ExpirationDate, @UserID, @IpAddress, @UserAgent, 0) SELECT @EventID = [Event ID] FROM [Event] WHERE [Event Name] = 'Login' SET @Description = FORMATMESSAGE('"%s" logged in', @UserName) INSERT INTO [ActivityLog] ([Event ID], [Event Description], [Timestamp], [Component]) VALUES (@EventID, @Description, SYSDATETIME(), CONCAT('SP_', OBJECT_NAME(@@PROCID))) COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 BEGIN ROLLBACK TRANSACTION END; THROW; END CATCH; SET NOCOUNT, XACT_ABORT OFF; END GO /****** Object: StoredProcedure [dbo].[LogoutUser] Script Date: 1/13/2021 7:23:30 PM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- ============================================= -- Author: -- Create date: -- Description: -- ============================================= CREATE OR ALTER PROCEDURE [dbo].[LogoutUser] @SessionToken NVARCHAR(1024) AS BEGIN -- SET NOCOUNT ON added to prevent extra result sets from -- interfering with SELECT statements. SET NOCOUNT ON; DECLARE @SessionID INT DECLARE @UserName NVARCHAR(512) DECLARE @Success BIT = 0 DECLARE @EventID INT SELECT @SessionID = [Session ID], @UserName = [Name] FROM [Session] INNER JOIN [User] ON [Session].[User ID] = [User].[User ID] WHERE [Session Token] = @SessionToken SELECT @EventID = [Event ID] FROM [Event] WHERE [Event Name] = 'Logout' SET NOCOUNT, XACT_ABORT ON; BEGIN TRY BEGIN TRANSACTION; IF @SessionID IS NULL BEGIN; THROW 51000, 'Session token not found', 1; END; DELETE FROM [Session] WHERE [Session ID] = @SessionID IF @EventID IS NOT NULL INSERT INTO ActivityLog ([Event Description], [Timestamp], [Component], [Event ID]) VALUES (FORMATMESSAGE('User "%s" has logged out', @UserName), SYSDATETIME(), CONCAT('SP_', OBJECT_NAME(@@PROCID)), @EventID) COMMIT TRANSACTION; SET @Success = 1 END TRY BEGIN CATCH IF @@TRANCOUNT > 0 BEGIN ROLLBACK TRANSACTION END; THROW; END CATCH; SET NOCOUNT, XACT_ABORT OFF; SELECT @Success AS Success END GO /****** Object: StoredProcedure [dbo].[LogPasswordResetRequest] Script Date: 1/13/2021 7:23:30 PM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- ============================================= -- Author: -- Create date: -- Description: -- ============================================= CREATE OR ALTER PROCEDURE [dbo].[LogPasswordResetRequest] @Email NVARCHAR(64), @SessionToken VARCHAR(1024), @ExpirationDate DATETIME, @IpAddress VARCHAR(256), @UserAgent VARCHAR(256) AS BEGIN -- SET NOCOUNT ON added to prevent extra result sets from -- interfering with SELECT statements. SET NOCOUNT, XACT_ABORT ON; DECLARE @UserID INT DECLARE @EventID INT DECLARE @Description NVARCHAR(512) DECLARE @UserName NVARCHAR(64) SELECT @UserID = [User ID], @UserName = [Name] FROM [User] WHERE [User].[Email] = @Email BEGIN TRY BEGIN TRANSACTION; IF dbo.UserExists(@UserName) = 0 BEGIN; THROW 51000, 'User does not exist', 1; END; INSERT INTO [Session] ([Session Token], [Expiration Date], [User ID], [IP Address], [User Agent], [Pending Reset]) VALUES (@SessionToken, @ExpirationDate, @UserID, @IpAddress, @UserAgent, 1) SELECT @EventID = [Event ID] FROM [Event] WHERE [Event Name] = 'Password Reset Request' SET @Description = FORMATMESSAGE('"%s" requested a password reset', @UserName) INSERT INTO [ActivityLog] ([Event ID], [Event Description], [Timestamp], [Component]) VALUES (@EventID, @Description, SYSDATETIME(), CONCAT('SP_', OBJECT_NAME(@@PROCID))) COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 BEGIN ROLLBACK TRANSACTION END; THROW; END CATCH; SET NOCOUNT, XACT_ABORT OFF; END GO /****** Object: StoredProcedure [dbo].[RegisterNewUser] Script Date: 1/13/2021 7:23:30 PM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- ============================================= -- Author: -- Create date: -- Description: -- ============================================= CREATE OR ALTER PROCEDURE [dbo].[RegisterNewUser] @UserName NVARCHAR(512), @Email NVARCHAR(512), @Password NVARCHAR(1024), @Salt NVARCHAR(512), @SessionToken NVARCHAR(1024), @ExpirationDate DATETIME, @UserAgent VARCHAR(256), @IpAddress VARCHAR(256) AS BEGIN -- SET NOCOUNT ON added to prevent extra result sets from -- interfering with SELECT statements. DECLARE @UserID INT DECLARE @EventID INT DECLARE @SessionID INT SET NOCOUNT, XACT_ABORT ON; BEGIN TRY BEGIN TRANSACTION; IF dbo.UserExists(@UserName) = 1 BEGIN; THROW 51000, 'This username is already in use', 1; END; IF dbo.EmailInUse(@Email) = 1 BEGIN; THROW 51001, 'This email is already in use', 1 END; SELECT @EventID = [Event ID] FROM [Event] WHERE [Event Name] = 'Registration' IF @EventID = NULL OR @EventID = 0 BEGIN; THROW 51002, 'Registration event type not found', 1; END; INSERT INTO [User] (Name, Email) VALUES (@UserName, @Email) SELECT @UserID = SCOPE_IDENTITY() INSERT INTO [Login] ([Password Hash], [Salt], [User Key]) VALUES (@Password, @Salt, @UserId) INSERT INTO [Session] ([Session Token], [Expiration Date], [User ID], [User Agent], [IP Address]) VALUES (@SessionToken, @ExpirationDate, @UserId, @UserAgent, @IpAddress) SELECT @SessionID = SCOPE_IDENTITY() INSERT INTO [ActivityLog] ([Event ID], [Event Description], [Timestamp], [Component]) VALUES (@EventID, FORMATMESSAGE('Registered user "%s".', @UserName), SYSDATETIME(), CONCAT('SP_', OBJECT_NAME(@@PROCID))) SELECT @UserID AS UserKey COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 BEGIN ROLLBACK TRANSACTION END; THROW; END CATCH; SET NOCOUNT, XACT_ABORT OFF; END GO /****** Object: StoredProcedure [dbo].[ResetPassword] Script Date: 1/13/2021 7:23:30 PM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- ============================================= -- Author: -- Create date: -- Description: -- ============================================= CREATE OR ALTER PROCEDURE [dbo].[ResetPassword] @PasswordHash VARCHAR(1024), @Salt VARCHAR(512), @SessionToken VARCHAR(1024) AS BEGIN -- SET NOCOUNT ON added to prevent extra result sets from -- interfering with SELECT statements. DECLARE @Username NVARCHAR(64) DECLARE @UserID INT DECLARE @EventID INT SELECT @UserID = [User ID] FROM [Session] WHERE [Session Token] = @SessionToken SELECT @Username = [Name] FROM [User] WHERE [User ID] = @UserID SET NOCOUNT, XACT_ABORT ON; BEGIN TRY BEGIN TRANSACTION; IF dbo.UserExists(@UserName) = 0 BEGIN; THROW 51002, 'User does not exist', 1; END; SELECT @EventID = [Event ID] FROM [Event] WHERE [Event Name] = 'Password Reset' IF @EventID = NULL OR @EventID = 0 BEGIN; THROW 51001, 'Password reset event type not found', 1; END; UPDATE [Login] SET [Password Hash] = @PasswordHash, [Salt] = @Salt, [Last Login Attempt] = NULL WHERE [User Key] = @UserID DELETE FROM [Session] WHERE [User ID] = @UserID INSERT INTO [ActivityLog] ([Event ID], [Event Description], [Timestamp], [Component]) VALUES (@EventID, FORMATMESSAGE('User "%s" has had their password reset.', @UserName), SYSDATETIME(), CONCAT('SP_', OBJECT_NAME(@@PROCID))) COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 BEGIN ROLLBACK TRANSACTION END; THROW; END CATCH; SET NOCOUNT, XACT_ABORT OFF; END GO /****** Object: StoredProcedure [dbo].[ValidateSessionToken] Script Date: 1/13/2021 7:23:30 PM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- ============================================= -- Author: -- Create date: -- Description: -- ============================================= CREATE OR ALTER PROCEDURE [dbo].[ValidateSessionToken] @SessionToken VARCHAR(1024), @IsResetToken BIT AS BEGIN -- SET NOCOUNT ON added to prevent extra result sets from -- interfering with SELECT statements. SET NOCOUNT, XACT_ABORT OFF; DECLARE @SessionExpiration DATETIME DECLARE @SessionID INT DECLARE @SessionIsValid BIT = 0 DECLARE @UserID INT DECLARE @EventID TINYINT SELECT @SessionID = [Session ID], @SessionExpiration = [Expiration Date], @UserID = [User ID] FROM [Session] WHERE [Session Token] = @SessionToken AND [Pending Reset] = @IsResetToken SELECT @EventID = [Event ID] FROM [Event] WHERE [Event Name] = 'Session Expiration' SET NOCOUNT, XACT_ABORT ON; BEGIN TRY BEGIN TRANSACTION; -- Code here IF @SessionID IS NOT NULL BEGIN -- Sesssion found. IF @SessionExpiration > SYSDATETIME() SET @SessionIsValid = 1 ELSE BEGIN DELETE FROM [Session] WHERE [Session ID] = @SessionID IF @EventID IS NOT NULL INSERT INTO [ActivityLog] ([Event Description], [Timestamp], [Event ID], [Component]) VALUES ('Session token has expired', SYSDATETIME(), @EventID, CONCAT('SP_', OBJECT_NAME(@@PROCID))) END END COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 BEGIN ROLLBACK TRANSACTION END; END CATCH; SET NOCOUNT, XACT_ABORT OFF; SELECT @SessionIsValid AS SessionIsValid END GO