Skip to content

Commit

Permalink
Header authentication (#180)
Browse files Browse the repository at this point in the history
* Use BaseSetting for default group

* Add header authentication

* Add instructions for proxy auth

* Global flag to enable header authentication

* First user goes to setup-wizard regardless of proxy auth

* Update readme regarding proxy authentication

* Add function for creating a user

* Fix issue with bolean value of HEADER_AUTH_ENABLED

* Disable password change for proxy users

* format code

* fix lint errors

* Default value for proxy headers

* Ensure HEADER_USERNAME is set when using proxy auth

* Add error logging for header authentication
  • Loading branch information
albinmedoc authored Dec 4, 2024
1 parent 5492817 commit 8742015
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 68 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,19 @@ In this mode, the suggested item is automatically approved and added to the wish
### SMTP

SMTP does not need to be configured for the app to function. SMTP enables inviting users via email and the forgot password flow. Without SMTP, you can still manually generate invite links and forgot password links.

### Proxy authentication

> [!WARNING]
> When header authentication is enabled, Wishlist makes no assumptions about the validity of the headers. It is up to you to have your proxy properly configured. An improperly configured proxy **could allow anyone** to gain access to the application by forging the headers.
If you have a reverse proxy you want to use to login your users, you do it via our proxy authentication method. To configure this method, your proxy must send HTTP headers containing the name, username and email for the logged in user.
You configure this using environment variables.

`HEADER_AUTH`: Enable proxy authentication

`HEADER_USERNAME`: The name of the headers that contains the username of the user

`HEADER_NAME`: The name of the headers that contains the full name of the user

`HEADER_EMAIL`: The name of the headers that contains the email of the user
1 change: 0 additions & 1 deletion src/lib/components/admin/SettingsForm/DefaultGroup.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import BaseSetting from "./BaseSetting.svelte";
type Group = {
id: string;
name: string;
Expand Down
71 changes: 71 additions & 0 deletions src/lib/server/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { client } from "$lib/server/prisma";
import { Role } from "$lib/schema";
import { getConfig } from "$lib/server/config";
import { LegacyScrypt } from "lucia";
import type { User } from "@prisma/client";

type UserMinimal = Pick<User, "username" | "email" | "name">;

export const createUser = async (user: UserMinimal, role: Role, password: string, signupTokenId?: string) => {
const config = await getConfig();
const userCount = await client.user.count();

let groupId = config.defaultGroup;
if (signupTokenId) {
groupId = await client.signupToken
.findUnique({
where: {
id: signupTokenId
},
select: {
groupId: true
}
})
.then((data) => data?.groupId);
} else if (userCount === 0) {
groupId = (
await client.group.findFirst({
select: {
id: true
}
})
)?.id;
}
const hashedPassword = await new LegacyScrypt().hash(password);

const newUser = await client.user.create({
select: {
id: true
},
data: {
username: user.username,
name: user.name,
email: user.email,
roleId: role,
hashedPassword
}
});

if (groupId) {
await client.userGroupMembership.create({
data: {
groupId: groupId,
userId: newUser.id,
active: true
}
});
}

if (signupTokenId) {
await client.signupToken.update({
where: {
id: signupTokenId
},
data: {
redeemed: true
}
});
}

return newUser;
};
11 changes: 9 additions & 2 deletions src/routes/account/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { env } from "$env/dynamic/private";
import { auth } from "$lib/server/auth";
import { client } from "$lib/server/prisma";
import { getResetPasswordSchema } from "$lib/validations";
Expand All @@ -8,14 +9,20 @@ import type { PrismaClientKnownRequestError } from "@prisma/client/runtime/libra
import { createImage, tryDeleteImage } from "$lib/server/image-util";
import { LegacyScrypt } from "lucia";

export const load: PageServerLoad = async ({ locals }) => {
export const load: PageServerLoad = async ({ locals, request }) => {
const user = locals.user;
if (!user) {
redirect(302, `/login?ref=/account`);
}

const isProxyUser =
(env.HEADER_AUTH_ENABLED ?? "false") == "true" &&
!!env.HEADER_USERNAME &&
!!request.headers.get(env.HEADER_USERNAME);

return {
user
user,
isProxyUser
};
};

Expand Down
6 changes: 4 additions & 2 deletions src/routes/account/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

<TabGroup>
<Tab name="Profile" value={0} bind:group={tabSet}>Profile</Tab>
<Tab name="Security" value={1} bind:group={tabSet}>Security</Tab>
{#if !data.isProxyUser}
<Tab name="Security" value={1} bind:group={tabSet}>Security</Tab>
{/if}
{#snippet panel()}
{#if tabSet === 0}
<div class="flex w-fit flex-col items-center">
Expand Down Expand Up @@ -49,7 +51,7 @@

<EditProfile user={data.user} />
</div>
{:else if tabSet === 1}
{:else if tabSet === 1 && !data.isProxyUser}
<ChangePassword />
{/if}
{/snippet}
Expand Down
68 changes: 64 additions & 4 deletions src/routes/login/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { fail, redirect } from "@sveltejs/kit";
import { env } from "$env/dynamic/private";
import { auth } from "$lib/server/auth";
import type { PageServerLoad, Actions } from "./$types";
import { loginSchema } from "$lib/validations";
import { getConfig } from "$lib/server/config";
import { Role } from "$lib/schema";
import { client } from "$lib/server/prisma";
import { LegacyScrypt } from "lucia";
import type { PageServerLoad, Actions } from "./$types";
import { createUser } from "$lib/server/user";

export const load: PageServerLoad = async ({ locals, request }) => {
export const load: PageServerLoad = async ({ locals, request, cookies }) => {
const config = await getConfig();
const ref = new URL(request.url).searchParams.get("ref");
if (locals.user) {
const ref = new URL(request.url).searchParams.get("ref");
redirect(302, ref || "/");
}

Expand All @@ -17,7 +21,63 @@ export const load: PageServerLoad = async ({ locals, request }) => {
redirect(302, "/setup-wizard");
}

const config = await getConfig();
/* Header authentication */
if ((env.HEADER_AUTH_ENABLED ?? "false") == "true" && !!env.HEADER_USERNAME) {
const username = request.headers.get(env.HEADER_USERNAME);
if (username) {
let user = await client.user.findUnique({
select: {
id: true
},
where: {
username: username
}
});

if (!user) {
if (!env.HEADER_EMAIL || !env.HEADER_NAME) {
console.error("Missing required environment variables for header authentication");
return fail(400, { username: username, password: "", incorrect: true });
}
const name = request.headers.get(env.HEADER_NAME);
const email = request.headers.get(env.HEADER_EMAIL);
if (!name || !email) {
console.error("Missing required headers for header authentication");
return fail(400, { username: username, password: "", incorrect: true });
}
const userCount = await client.user.count();
user = await createUser(
{
username: username,
email: email,
name: name
},
userCount > 0 ? Role.USER : Role.ADMIN,
""
);

if (config.defaultGroup) {
await client.userGroupMembership.create({
data: {
groupId: config.defaultGroup,
userId: user.id,
active: true
}
});
}
}

const session = await auth.createSession(user.id, {});
const sessionCookie = auth.createSessionCookie(session.id);
cookies.set(sessionCookie.name, sessionCookie.value, {
path: "/",
...sessionCookie.attributes
});
redirect(302, ref || "/");
}
}
/* End header authentication */

return { enableSignup: config.enableSignup };
};

Expand Down
69 changes: 10 additions & 59 deletions src/routes/signup/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { error, fail, redirect } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";
import { hashToken } from "$lib/server/token";
import { getConfig } from "$lib/server/config";
import { Role } from "$lib/schema";
import { env } from "$env/dynamic/private";
import { LegacyScrypt } from "lucia";
import { createUser } from "$lib/server/user";
import { Role } from "$lib/schema";

export const load: PageServerLoad = async ({ locals, request }) => {
if (locals.user) redirect(302, "/");
Expand Down Expand Up @@ -43,7 +43,6 @@ export const load: PageServerLoad = async ({ locals, request }) => {

export const actions: Actions = {
default: async ({ request, cookies }) => {
const config = await getConfig();
const formData = Object.fromEntries(await request.formData());
const signupSchema = await getSignupSchema();
const signupData = signupSchema.safeParse(formData);
Expand All @@ -60,72 +59,24 @@ export const actions: Actions = {
}

const userCount = await client.user.count();
let groupId: string | undefined;
if (signupData.data.tokenId) {
groupId = await client.signupToken
.findUnique({
where: {
id: signupData.data.tokenId
},
select: {
groupId: true
}
})
.then((data) => data?.groupId);
} else if (userCount === 0) {
groupId = (
await client.group.findFirst({
select: {
id: true
}
})
)?.id;
} else if (config.defaultGroup) {
groupId = config.defaultGroup;
}

try {
const hashedPassword = await new LegacyScrypt().hash(signupData.data.password);
const user = await client.user.create({
select: {
id: true
},
data: {
const user = await createUser(
{
username: signupData.data.username,
email: signupData.data.email,
hashedPassword,
name: signupData.data.name,
roleId: userCount > 0 ? Role.USER : Role.ADMIN
}
});
name: signupData.data.name
},
userCount > 0 ? Role.USER : Role.ADMIN,
signupData.data.password,
signupData.data.tokenId
);

const session = await auth.createSession(user.id, {});
const sessionCookie = auth.createSessionCookie(session.id);
cookies.set(sessionCookie.name, sessionCookie.value, {
path: "/",
...sessionCookie.attributes
});

if (groupId) {
await client.userGroupMembership.create({
data: {
groupId: groupId,
userId: user.id,
active: true
}
});
}

if (signupData.data.tokenId) {
await client.signupToken.update({
where: {
id: signupData.data.tokenId
},
data: {
redeemed: true
}
});
}
return { success: true };
} catch {
return fail(400, {
Expand Down

0 comments on commit 8742015

Please sign in to comment.