Skip to content

Commit

Permalink
refactor: use expiring links for discord login to protect from others…
Browse files Browse the repository at this point in the history
… linking to your account.
  • Loading branch information
zicklag committed Sep 18, 2024
1 parent ae3a089 commit 2cc6f32
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 92 deletions.
19 changes: 9 additions & 10 deletions pnpm-lock.yaml

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

42 changes: 25 additions & 17 deletions src/lib/discord_bot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,24 @@ import Discord, {
InteractionContextType,
ApplicationCommandType
} from 'discord.js';
import { GatewayIntentBits } from 'discord.js';
import { env } from '$env/dynamic/private';
import { env as PublicEnv } from '$env/dynamic/public';
import { getProfileById, setProfileById } from '$lib/leaf/profile';
import { leafClient } from '$lib/leaf';
import { getDiscordUserRauthyId } from '$lib/leaf/discord';
import Keyv from 'keyv';

const discordLoginLinkIds = new Keyv({ namespace: 'discord-login-links' });

export const createDiscordLoginLinkId = async (discordId: string): Promise<string> => {
const linkid = crypto.randomUUID();
// Create a login link that is valid for 10 minutes
await discordLoginLinkIds.set(linkid, discordId, 10 * 60 * 1000);
return linkid;
};

export const getDiscordIdForLoginLink = async (loginLink: string): Promise<string | undefined> => {
return await discordLoginLinkIds.get(loginLink);
};

const LOGIN_CMD = 'weird-login';
const IMPORT_LINKS_CMD = 'Import links to Weird profile';
Expand Down Expand Up @@ -54,11 +67,9 @@ client.on('interactionCreate', async (interaction) => {

client.on('interactionCreate', async function (interaction) {
if (interaction.isChatInputCommand() && interaction.commandName === LOGIN_CMD) {
interaction.user.send(
`Please go to this link to authenticate: ${PublicEnv.PUBLIC_URL}/app/discord_bot_authenticator?q=${interaction.user.id}`
);
const linkId = await createDiscordLoginLinkId(interaction.user.id);
interaction.reply({
content: 'I have sent you a DM with the link to authenticate.',
content: `Please go to this link to authenticate: ${PublicEnv.PUBLIC_URL}/connect/to/discord/${linkId}`,
ephemeral: true
});
} else if (
Expand All @@ -75,24 +86,21 @@ client.on('interactionCreate', async function (interaction) {
});
return;
}
const discord_tokens = JSON.parse(
(await leafClient.get_local_secret('discord_tokens')) ?? '{}'
);
const userId = Object.keys(discord_tokens).find(
(key) => discord_tokens[key] === interaction.user.id
);
const sendNeedsAuthenticateMessage = () =>
const userId = await getDiscordUserRauthyId(interaction.user.id);
if (!userId) {
interaction.reply({
content: 'You need to authenticate first. Use the command `/weird_auth` to authenticate.',
content: 'You need to authenticate first. Use the command `/weird-login` to authenticate.',
ephemeral: true
});
if (!userId) {
sendNeedsAuthenticateMessage();
return;
}
let profile = await getProfileById(userId);
if (!profile) {
sendNeedsAuthenticateMessage();
interaction.reply({
content:
'The Weird user linked to your Discord account no longer exists. Use the `/weird-login` command to login to a new Weird account.',
ephemeral: true
});
return;
}
let edited = false;
Expand Down
37 changes: 37 additions & 0 deletions src/lib/leaf/discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { BorshSchema, Component, type ExactLink, type PathSegment } from 'leaf-proto';
import { CommonMark } from 'leaf-proto/components';
import { instance_link, leafClient } from '.';

export const DISCORD_PREFIX: PathSegment = { String: 'discord_users' };

export class RauthyUserId extends Component {
value: string = '';
constructor(s: string) {
super();
this.value = s;
}
static componentName(): string {
return 'RauthyUserId';
}
static borshSchema(): BorshSchema {
return BorshSchema.String;
}
static specification(): Component[] {
return [new CommonMark('The Rauthy auth server user ID associated to this entity.')];
}
}

export function discordUserLinkById(id: string): ExactLink {
return instance_link([DISCORD_PREFIX, { String: id }]);
}

export async function setDiscordUserRauthyId(discordId: string, rauthyId: string) {
const discordLink = discordUserLinkById(discordId);
leafClient.add_components(discordLink, [new RauthyUserId(rauthyId)]);
}

export async function getDiscordUserRauthyId(discordId: string): Promise<string | undefined> {
const discordLink = discordUserLinkById(discordId);
const ent = await leafClient.get_components(discordLink, [RauthyUserId]);
return ent?.get(RauthyUserId)?.value;
}
2 changes: 1 addition & 1 deletion src/lib/leaf/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BorshSchema, Component, type ExactLink, type PathSegment } from 'leaf-p
import { CommonMark, Description, RawImage, Name } from 'leaf-proto/components';
import { instance_link, leafClient } from '.';
import { env } from '$env/dynamic/public';
import _, { last } from 'underscore';
import _ from 'underscore';

export const PROFILE_PREFIX: PathSegment = { String: 'profiles' };

Expand Down
32 changes: 32 additions & 0 deletions src/routes/(app)/connect/to/discord/[linkId]/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getSession } from '$lib/rauthy/server';
import { redirect, type ServerLoad, fail } from '@sveltejs/kit';
import { setDiscordUserRauthyId } from '$lib/leaf/discord.js';
import { getDiscordIdForLoginLink } from '$lib/discord_bot/index.js';

export const load: ServerLoad = async ({ params, fetch, request }) => {
let { userInfo } = await getSession(fetch, request);
if (!userInfo) {
// TODO: hook up the login form so that it redirects back to this discord
// authentication once you are logged in.
return redirect(307, '/auth/v1/account');
}
return { ...params };
};

export const actions = {
default: async ({ fetch, request }) => {
let { userInfo } = await getSession(fetch, request);
if (!userInfo) {
return fail(403, { error: 'You are not logged in' });
}
const data = await request.formData();
const linkId = data.get('link_id')?.toString();
if (!linkId) return fail(400, { error: 'Missing link ID' });
const discordId = await getDiscordIdForLoginLink(linkId);
if (!discordId) return fail(400, { error: 'The login link has expired.' });

await setDiscordUserRauthyId(discordId, userInfo.id);

return { success: true };
}
};
33 changes: 33 additions & 0 deletions src/routes/(app)/connect/to/discord/[linkId]/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts">
import type { PageData, ActionData } from './$types';
const { data, form }: { data: PageData; form: ActionData } = $props();
const linkId = data.linkId;
</script>

<main class="flex flex-col items-center">
<div class="card mt-8 max-w-[600px] p-4">
<h2 class="text-2xl font-bold">Connect to Discord</h2>
<p class="my-4">Continuing will connect your Discord account to your Weird account.</p>
<form method="post">
<input type="hidden" name="link_id" value={linkId} />
<div class="flex justify-end">
{#if form}
<div class="p-7 text-xl">
{#if form.success}
<p class="text-green-700">Successfully connected account. You may close this tab.</p>
{:else if form.error}
<p class="text-red-500">Error logging in: {form.error}</p>
{/if}
</div>
{/if}

{#if !form?.success}
<button class="my-4 rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
>Grant Access</button
>
{/if}
</div>
</form>
</div>
</main>
21 changes: 0 additions & 21 deletions src/routes/(helper)/app/discord_bot_authenticator/+page.server.ts

This file was deleted.

43 changes: 0 additions & 43 deletions src/routes/(helper)/app/discord_bot_authenticator/+page.svelte

This file was deleted.

0 comments on commit 2cc6f32

Please sign in to comment.