diff --git a/src/controllers/auth-controller.ts b/src/controllers/auth-controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f613480e726195eaf0c62dc80a6ea799e72c69ff
--- /dev/null
+++ b/src/controllers/auth-controller.ts
@@ -0,0 +1,47 @@
+import * as AuthService from "../services/auth-service";
+import { NextFunction, Request, Response } from "express";
+import { generateResponse } from "../utils/response";
+import { StatusCodes } from "http-status-codes";
+
+const signup = async (
+  req: Request,
+  res: Response,
+  next: NextFunction,
+): Promise<void> => {
+  try {
+    const createdUser = await AuthService.signup(req.body);
+    generateResponse(res, StatusCodes.OK, createdUser);
+  } catch (err) {
+    next(err);
+  }
+};
+
+// The storage of tokens on the client side follows the recommendations provided by OWASP
+// https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html#token-storage-on-client-side
+const login = async (
+  req: Request,
+  res: Response,
+  next: NextFunction,
+): Promise<void> => {
+  try {
+    const accessTokenAndFingerPrint = await AuthService.login(req.body);
+    setFingerprintCookie(res, accessTokenAndFingerPrint.fingerprint);
+    generateResponse(res, StatusCodes.OK);
+  } catch (err) {
+    next(err);
+  }
+};
+
+const setFingerprintCookie = (
+  res: Response,
+  fingerprint: string,
+): void => {
+  res.cookie("__Secure-fingerprint", fingerprint, {
+    httpOnly: true,
+    secure: true,
+    sameSite: "strict",
+    maxAge: 60 * 15, // 15 minutes max age (same as access token expiry)
+  });
+};
+
+export { signup, login };
diff --git a/src/cores/app.ts b/src/cores/app.ts
index eff68148f16c9d2d86b336cfe875c1fc5ccea232..62f3059dcfdc9da04635c9ccf3ffee983d3edc01 100644
--- a/src/cores/app.ts
+++ b/src/cores/app.ts
@@ -1,4 +1,5 @@
 import express, { Express } from "express";
+import cookieParser from "cookie-parser";
 import dotenv from "dotenv";
 import apiRouter from "../routers/api";
 
@@ -8,6 +9,7 @@ export const app: Express = express();
 const port: string | undefined = process.env.EXPRESS_PORT;
 
 app.use(express.json());
+app.use(cookieParser());
 app.use(apiRouter);
 
 app.listen(port, () => {
diff --git a/src/errors/standard-error.ts b/src/errors/standard-error.ts
index 874e89bf1175815707ba24a4ef89607686a92ff1..c7bf854af68d8df4791a0e5473538a27404553f3 100644
--- a/src/errors/standard-error.ts
+++ b/src/errors/standard-error.ts
@@ -6,10 +6,12 @@ enum ErrorType {
   WRONG_PASSWORD,
   PASSWORD_HASH_FAILURE,
   PASSWORD_VERIFICATION_FAILURE,
-  TOKEN_GENERATION_FAILURE,
-  TOKEN_MISSING,
-  TOKEN_EXPIRED,
-  TOKEN_NOT_ACTIVE,
+  ACCESS_TOKEN_GENERATION_FAILURE,
+  ACCESS_TOKEN_MISSING,
+  ACCESS_TOKEN_EXPIRED,
+  ACCESS_TOKEN_NOT_ACTIVE,
+  AUTHORIZATION_HEADER_NOT_SET,
+  FINGERPRINT_MISSING,
   ALBUM_NOT_FOUND,
   INVALID_API_KEY,
 }
@@ -46,26 +48,36 @@ class StandardError {
         this.status = StatusCodes.INTERNAL_SERVER_ERROR;
         break;
 
-      case ErrorType.TOKEN_GENERATION_FAILURE:
-        this.title = "Failed to generate token."
+      case ErrorType.ACCESS_TOKEN_GENERATION_FAILURE:
+        this.title = "Failed to generate access token."
         this.status = StatusCodes.INTERNAL_SERVER_ERROR;
         break;
 
-      case ErrorType.TOKEN_MISSING:
+      case ErrorType.ACCESS_TOKEN_MISSING:
         this.title = "Your access token is missing."
         this.status = StatusCodes.UNAUTHORIZED;
         break;
 
-      case ErrorType.TOKEN_EXPIRED:
+      case ErrorType.ACCESS_TOKEN_EXPIRED:
         this.title = "Your access token is expired."
         this.status = StatusCodes.UNAUTHORIZED;
         break;
 
-      case ErrorType.TOKEN_NOT_ACTIVE:
+      case ErrorType.ACCESS_TOKEN_NOT_ACTIVE:
         this.title = "Your access token is not active yet."
         this.status = StatusCodes.UNAUTHORIZED;
         break;
 
+      case ErrorType.AUTHORIZATION_HEADER_NOT_SET:
+        this.title = "Authorization header not set."
+        this.status = StatusCodes.UNAUTHORIZED;
+        break;
+
+      case ErrorType.FINGERPRINT_MISSING:
+        this.title = "Fingerprint is missing."
+        this.status = StatusCodes.UNAUTHORIZED;
+        break;
+
       case ErrorType.ALBUM_NOT_FOUND:
         this.title = "Album not found."
         this.status = StatusCodes.NOT_FOUND;
diff --git a/src/middlewares/verify-token.ts b/src/middlewares/verify-token.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7273ff9501c563e9516e7c7073a92fde4e78e0c1
--- /dev/null
+++ b/src/middlewares/verify-token.ts
@@ -0,0 +1,70 @@
+import { NextFunction, Request, Response } from "express";
+import jwt, { NotBeforeError, TokenExpiredError } from "jsonwebtoken";
+import { ErrorType, StandardError } from "../errors/standard-error";
+import { hashFingerprint } from "../utils/token";
+
+interface TonalityPayload {
+  uid: number;
+  usr: string;
+  fgp: string;
+  exp: number;
+  nbf: number;
+  iss: string;
+}
+
+// Compare fingerprint in the token payload with
+// the fingerprint stored in the cookie
+const verifyFingerprint = async (
+  fingerprint: string,
+  d: TonalityPayload,
+): Promise<boolean> => {
+  return (await hashFingerprint(fingerprint)) === d.fgp;
+};
+
+const verifyToken = async (req: Request, res: Response, next: NextFunction) => {
+  // Authorization Bearer ${accessToken}
+  const authHeader = req.headers.authorization;
+
+  if (!authHeader) {
+    throw new StandardError(ErrorType.AUTHORIZATION_HEADER_NOT_SET);
+  }
+
+  const accessToken = authHeader.split(" ")[1];
+
+  if (!accessToken) {
+    throw new StandardError(ErrorType.ACCESS_TOKEN_MISSING);
+  }
+
+  const fingerprint = req.cookies["__Secure-fingerprint"];
+
+  if (!fingerprint) {
+    throw new StandardError(ErrorType.FINGERPRINT_MISSING);
+  }
+
+  try {
+    const decodedPayload = jwt.verify(
+      accessToken,
+      process.env.JWT_SHARED_SECRET as string,
+      {
+        algorithms: ["HS256"],
+        issuer: "Tonality REST Service",
+      },
+    );
+
+    // Will generate run-time error when the decoded payload is not of the type TonalityPayload
+    // This is a wanted behavior because we want to validate the payload type
+    await verifyFingerprint(fingerprint, decodedPayload as TonalityPayload);
+
+    next(); // The token is verified, pass to the next middleware
+  } catch (error) {
+    if (error instanceof TokenExpiredError) {
+      throw new StandardError(ErrorType.ACCESS_TOKEN_EXPIRED);
+    } else if (error instanceof NotBeforeError) {
+      throw new StandardError(ErrorType.ACCESS_TOKEN_NOT_ACTIVE);
+    }
+
+    throw error;
+  }
+};
+
+export { verifyToken };
diff --git a/src/routers/api.ts b/src/routers/api.ts
index 54fdb5e83fa374ef6bf8584866ffacb593cfc927..10eeceb1019d5e6d0f6bd15254a08cd9ef16f770 100644
--- a/src/routers/api.ts
+++ b/src/routers/api.ts
@@ -1,8 +1,10 @@
 import express from "express";
-import premiumAlbumRouter from "./premium-album-router";
+import { premiumAlbumRouter } from "./premium-album-router";
+import { authRouter } from "./auth-router";
 
 const apiRouter = express.Router();
 
 apiRouter.use(premiumAlbumRouter);
+apiRouter.use(authRouter);
 
 export default apiRouter;
diff --git a/src/routers/auth-router.ts b/src/routers/auth-router.ts
new file mode 100644
index 0000000000000000000000000000000000000000..baef16a8ba3d68599428da9f1f36117be1e7e7a0
--- /dev/null
+++ b/src/routers/auth-router.ts
@@ -0,0 +1,11 @@
+import express, { Router } from "express";
+import * as AuthController from "../controllers/auth-controller";
+import { handleStandardError } from "../middlewares/handle-standard-error";
+
+const authRouter: Router = express.Router();
+
+authRouter.post("/api/signup", AuthController.signup, handleStandardError);
+
+authRouter.post("/api/login", AuthController.login, handleStandardError);
+
+export { authRouter };
diff --git a/src/routers/premium-album-router.ts b/src/routers/premium-album-router.ts
index f6cb29ae4d75f309e079f5f14feae743b68c9ff5..ad0384d92e8b99f3131a1df96b0f611df07f4309 100644
--- a/src/routers/premium-album-router.ts
+++ b/src/routers/premium-album-router.ts
@@ -1,31 +1,36 @@
-import express from "express";
+import express, { Router } from "express";
 import * as PremiumAlbumController from "../controllers/premium-album-controller";
 import { handleStandardError } from "../middlewares/handle-standard-error";
+import { verifyToken } from "../middlewares/verify-token";
 
-const premiumAlbumRouter = express.Router();
+const premiumAlbumRouter: Router = express.Router();
 
 premiumAlbumRouter.post(
   "/api/premium-album",
+  verifyToken,
   PremiumAlbumController.createPremiumAlbum,
   handleStandardError,
 );
 
 premiumAlbumRouter.get(
   "/api/premium-albums",
+  verifyToken,
   PremiumAlbumController.searchPremiumAlbum,
   handleStandardError,
 );
 
 premiumAlbumRouter.patch(
   "/api/premium-album/:premiumAlbumId",
+  verifyToken,
   PremiumAlbumController.updatePremiumAlbum,
   handleStandardError,
 );
 
 premiumAlbumRouter.delete(
   "/api/premium-album/:premiumAlbumId",
+  verifyToken,
   PremiumAlbumController.deletePremiumAlbum,
   handleStandardError,
 );
 
-export default premiumAlbumRouter;
+export { premiumAlbumRouter };
diff --git a/src/services/auth-service.ts b/src/services/auth-service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5b54ea3679ddaf14708c0435dec9ce3ac42a78f6
--- /dev/null
+++ b/src/services/auth-service.ts
@@ -0,0 +1,68 @@
+import { Prisma } from "@prisma/client";
+import prismaClient from "../cores/db";
+
+import { ErrorType, StandardError } from "../errors/standard-error";
+import { hashPassword, isPasswordValid } from "../utils/password";
+import { generateAccessTokenAndFingerprint } from "../utils/token";
+
+const signup = async (
+  data: Prisma.UserCreateInput,
+): Promise<{ userId: number; username: string }> => {
+  // If username already exists throw error
+  if (
+    (await prismaClient.user.findUnique({
+      where: {
+        username: data.username,
+      },
+    })) !== null
+  ) {
+    throw new StandardError(ErrorType.USERNAME_ALREADY_EXISTS);
+  }
+
+  try {
+    // Hash the password
+    const hashedPassword: string = await hashPassword(data.password);
+
+    return await prismaClient.user.create({
+      data: {
+        username: data.username,
+        password: hashedPassword,
+      },
+      select: {
+        userId: true,
+        username: true,
+      },
+    });
+  } catch (error) {
+    throw error;
+  }
+};
+
+const login = async (data: { username: string; password: string }) => {
+  const user = await prismaClient.user.findUnique({
+    where: {
+      username: data.username,
+    },
+  });
+
+  // If username not found, throw error
+  if (user === null) {
+    throw new StandardError(ErrorType.USER_NOT_FOUND);
+  }
+
+  // If wrong password, throw error
+  if (!(await isPasswordValid(user.password, data.password))) {
+    throw new StandardError(ErrorType.WRONG_PASSWORD);
+  }
+
+  try {
+    return await generateAccessTokenAndFingerprint({
+      userId: user.userId,
+      username: user.username,
+    });
+  } catch (error) {
+    throw error;
+  }
+};
+
+export { signup, login };
diff --git a/src/utils/password.ts b/src/utils/password.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6eda1b60ac08cad019de1794028f24a1f224987a
--- /dev/null
+++ b/src/utils/password.ts
@@ -0,0 +1,23 @@
+import * as argon2 from "argon2";
+import { ErrorType, StandardError } from "../errors/standard-error";
+
+const hashPassword = async (plainPassword: string): Promise<string> => {
+  try {
+    return await argon2.hash(plainPassword);
+  } catch (error) {
+    throw new StandardError(ErrorType.PASSWORD_HASH_FAILURE);
+  }
+};
+
+const isPasswordValid = async (
+  hashedPassword: string,
+  plainPassword: string,
+): Promise<boolean> => {
+  try {
+    return argon2.verify(hashedPassword, plainPassword);
+  } catch (error) {
+    throw new StandardError(ErrorType.PASSWORD_VERIFICATION_FAILURE);
+  }
+};
+
+export { hashPassword, isPasswordValid };
diff --git a/src/utils/response.ts b/src/utils/response.ts
index 653af5c640eea7c32e59681f7d5910032f457ef7..f7195df4c2564fa6b695069c18fcd6a1ef310817 100644
--- a/src/utils/response.ts
+++ b/src/utils/response.ts
@@ -5,9 +5,13 @@ import { StandardError } from "../errors/standard-error";
 const generateResponse = (
   res: Response,
   status: StatusCodes,
-  data: any,
+  data?: any,
 ): void => {
-  res.status(status).json(data ? data : null);
+  if (!data) {
+    res.status(status);
+    return;
+  }
+  res.status(status).json(data);
 };
 
 const generateStandardErrorResponse = (
diff --git a/src/utils/token.ts b/src/utils/token.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b0afdd8f320d1119c3d83c6c8d887e102bff0250
--- /dev/null
+++ b/src/utils/token.ts
@@ -0,0 +1,46 @@
+import jwt from "jsonwebtoken";
+import { ErrorType, StandardError } from "../errors/standard-error";
+import * as crypto from "crypto";
+
+const generateFingerprint = async (numberOfBytes: number): Promise<string> => {
+  const randomBytes: Buffer = crypto.randomBytes(numberOfBytes);
+  return randomBytes.toString("utf8");
+};
+
+const hashFingerprint = async (fgp: string): Promise<string> => {
+  const hash: crypto.Hash = crypto.createHash("sha256");
+  hash.update(fgp, "utf8");
+  return hash.digest().toString("utf8");
+};
+
+const generateAccessTokenAndFingerprint = async (data: {
+  userId: number,
+  username: string,
+}) => {
+  try {
+    const fingerprint: string = await generateFingerprint(64);
+    const hashedFingerprint: string = await hashFingerprint(fingerprint);
+
+    return {
+      accessToken: jwt.sign(
+        {
+          uid: data.userId,
+          usr: data.username,
+          fgp: hashedFingerprint, // User hashed fingerprint
+        },
+        process.env.JWT_SHARED_SECRET as string,
+        {
+          algorithm: "HS256", // Only use HS256 to generate JWTs
+          expiresIn: "15m", // Valid for 15 minutes
+          notBefore: "0ms", // The token is valid right away
+          issuer: "Tonality REST Service",
+        },
+      ),
+      fingerprint: fingerprint,
+    };
+  } catch (error) {
+    throw new StandardError(ErrorType.ACCESS_TOKEN_GENERATION_FAILURE);
+  }
+};
+
+export { generateAccessTokenAndFingerprint, hashFingerprint };