Skip to content

Commit

Permalink
generate safe token in frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
itsbekas committed Jan 17, 2025
1 parent d31d8e7 commit b0a1b58
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 8 deletions.
16 changes: 15 additions & 1 deletion backend/lifehub/core/user/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
UserCreateRequest,
UserLoginRequest,
UserResponse,
UserSensitiveLoginRequest,
UserTokenResponse,
UserVerifyRequest,
)
Expand All @@ -27,13 +28,26 @@ async def user_login(
return user_token


@router.post("/login/sensitive")
async def user_login_sensitive(
user_data: UserSensitiveLoginRequest,
user_service: UserServiceDep,
) -> None:
try:
print(user_data)
except UserServiceException as e:
raise HTTPException(status_code=401, detail=str(e))


@router.post("/signup")
async def user_signup(
user_data: UserCreateRequest,
user_service: UserServiceDep,
) -> None:
try:
user_service.create_user(user_data.username, user_data.email, user_data.password, user_data.name)
user_service.create_user(
user_data.username, user_data.email, user_data.password, user_data.name
)
except UserServiceException as e:
raise HTTPException(status_code=403, detail=str(e))

Expand Down
7 changes: 7 additions & 0 deletions backend/lifehub/core/user/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ class UserLoginRequest:
password: str


@dataclass
class UserSensitiveLoginRequest:
username: str
password: str
public_key: str


@dataclass
class UserCreateRequest:
username: str
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ services:
environment:
- VITE_BACKEND_URL=http://lifehub-backend:8000
- VITE_SESSION_SECRET=${SESSION_SECRET}
- VITE_SAFE_SESSION_SECRET=${SAFE_SESSION_SECRET}
- VITE_NODE_ENV=development
ports:
- "80:5173"
depends_on:
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ services:
container_name: lifehub-frontend
environment:
- VITE_BACKEND_URL=lifehub-backend
- VITE_NODE_ENV=production
ports:
- "80:5173"
depends_on:
Expand Down
46 changes: 40 additions & 6 deletions frontend/app/routes/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ import {
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { Form, redirect, useActionData } from "react-router";
import { getSession, commitSession } from "~/utils/session";
import {
getSession,
getSafeSession,
commitSession,
commitSafeSession,
} from "~/utils/session";
import type { ActionFunction } from "react-router";
import { data } from "react-router";
import type { Route } from "./+types/login";
import { generateKeyPairAndEncryptHash } from "~/utils/auth";

export async function loader({ request }: Route.LoaderArgs) {
const session = await getSession(request.headers.get("Cookie"));
Expand Down Expand Up @@ -54,15 +60,43 @@ export const action: ActionFunction = async ({ request }) => {

const { access_token, expires_at } = await response.json();

const { publicKey, encryptedHash } = await generateKeyPairAndEncryptHash(
username,
password
);

const sensitiveResponse = await fetch(
`${import.meta.env.VITE_BACKEND_URL}/api/v0/user/login/sensitive`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username,
password,
public_key: publicKey,
}),
}
);

if (!sensitiveResponse.ok) {
const errorData = await sensitiveResponse.json();
return { error: errorData.message }; // Return error to the client
}

// Create a session and store the token
const session = await getSession();
session.set("access_token", access_token);
const accessCookie = await commitSession(session);

return new Response(null, {
headers: {
"Set-Cookie": await commitSession(session),
},
});
const safeSession = await getSafeSession();
safeSession.set("access_token", encryptedHash);
const safeCookie = await commitSafeSession(safeSession);

const headers = new Headers();
headers.append("Set-Cookie", accessCookie);
headers.append("Set-Cookie", safeCookie);

return new Response(null, { headers });
} catch (error) {
console.error(error);
return { error: "An unexpected error occurred." };
Expand Down
69 changes: 69 additions & 0 deletions frontend/app/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import argon2 from "argon2";
import crypto from "crypto"; // Node.js crypto for MD5 or SHA-1

export async function hashPassword(
password: string,
username: string
): Promise<string> {
// Hash the username to ensure it is the correct size for the salt
const usernameHash = crypto
.createHash("sha1")
.update(username)
.digest("base64");

// Use the username as the salt
const salt = Buffer.from(usernameHash, "utf-8");

const passwordHash = await argon2.hash(password, {
salt,
type: argon2.argon2id, // Use Argon2id for balanced security
timeCost: 3,
memoryCost: 32768, // 32MB
parallelism: 1,
});

return passwordHash;
}

export async function generateKeyPairAndEncryptHash(
username: string,
password: string
): Promise<{ publicKey: string; encryptedHash: string }> {
// Generate the key pair
const keyPair = await crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-256", // Use P-256 for lightweight elliptic curve
},
true,
["sign", "verify"]
);

// Export public key
const publicKey = await crypto.subtle.exportKey("spki", keyPair.publicKey);

// Hash the password with the username as the salt
const passwordHash = await hashPassword(password, username);

// Optionally hash the encodedHash with MD5 or SHA-1
const sha1Hash = crypto
.createHash("sha1")
.update(passwordHash)
.digest("base64");

// Encrypt the SHA-1 hash with the private key
const encodedHashBytes = new TextEncoder().encode(sha1Hash);
const encryptedHash = await crypto.subtle.sign(
{
name: "ECDSA",
hash: { name: "SHA-256" },
},
keyPair.privateKey,
encodedHashBytes
);

return {
publicKey: Buffer.from(publicKey).toString("base64"),
encryptedHash: Buffer.from(encryptedHash).toString("base64"),
};
}
19 changes: 18 additions & 1 deletion frontend/app/utils/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,28 @@ export const { getSession, commitSession, destroySession } =
createCookieSessionStorage<SessionData, SessionFlashData>({
cookie: {
name: "access_token",
secure: process.env.NODE_ENV === "production",
secure: import.meta.env.VITE_NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 7, // 7 days
secrets: [import.meta.env.VITE_SESSION_SECRET as string],
},
});

// Configure safe token storage
export const {
getSession: getSafeSession,
commitSession: commitSafeSession,
destroySession: destroySafeSession,
} = createCookieSessionStorage<SessionData, SessionFlashData>({
cookie: {
name: "safe_token",
secure: import.meta.env.VITE_NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 7, // 7 days
secrets: [import.meta.env.VITE_SAFE_SESSION_SECRET as string],
},
});
45 changes: 45 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@react-router/node": "^7.1.1",
"@react-router/serve": "^7.1.1",
"@tabler/icons-react": "^3.28.1",
"argon2": "^0.41.1",
"isbot": "^5.1.17",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down

0 comments on commit b0a1b58

Please sign in to comment.