generalize auth provider stuff
This commit is contained in:
parent
e0594df7c8
commit
5770f07039
|
@ -4,24 +4,71 @@
|
|||
// file lmao. also hopefully i will rewrite this to be better at extending to
|
||||
// other oauth providers in the future
|
||||
|
||||
import type {AuthProviderImplementation} from '.';
|
||||
|
||||
import {env} from '$env/dynamic/private';
|
||||
|
||||
/** The redirect URI for Discord to send the user back to. */
|
||||
const discordRedirectURI = `${env.HOST}/auth/discord/callback`;
|
||||
const redirectURI = `${env.HOST}/auth/discord/callback`;
|
||||
const scopes = ['identify'];
|
||||
|
||||
/** The base of the URI that starts the OAuth flow. State is attached later. */
|
||||
// Long URIs suck
|
||||
const discordAuthURIBase = 'https://discordapp.com/api/oauth2/authorize'
|
||||
+ `?client_id=${env.DISCORD_CLIENT_ID}`
|
||||
+ '&response_type=code'
|
||||
+ `&redirect_uri=${encodeURIComponent(discordRedirectURI)}`
|
||||
+ `&scope=${encodeURIComponent(['identify', 'guilds'].join(' '))}`
|
||||
+ '&prompt=consent'; // Special Discord-only thing to always show prompt
|
||||
export default {
|
||||
redirectURI,
|
||||
buildAuthURI: state =>
|
||||
'https://discordapp.com/api/oauth2/authorize'
|
||||
+ `?client_id=${env.DISCORD_CLIENT_ID}`
|
||||
+ '&response_type=code'
|
||||
+ `&redirect_uri=${encodeURIComponent(redirectURI)}`
|
||||
+ `&scope=${encodeURIComponent(scopes.join(' '))}`
|
||||
+ '&prompt=consent' // Special Discord-only thing to always show prompt
|
||||
+ `&state=${encodeURIComponent(state)}`,
|
||||
async getUserIdentifier (code, {fetch}) {
|
||||
// exchange code for tokens
|
||||
const tokenResponse = await fetch(
|
||||
'https://discordapp.com/api/oauth2/token',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData({
|
||||
client_id: env.DISCORD_CLIENT_ID,
|
||||
client_secret: env.DISCORD_CLIENT_SECRET,
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: redirectURI,
|
||||
// Discord-specific: scope is required here and must match
|
||||
scope: scopes.join(' '),
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (tokenResponse.status !== 200) {
|
||||
throw new Error(`error requesting tokens: ${tokenResponse.status}`);
|
||||
}
|
||||
const tokensData = await tokenResponse.json();
|
||||
if (tokensData.error) {
|
||||
throw new Error(`error requesting tokens: ${tokensData.error}`);
|
||||
}
|
||||
|
||||
/** Generates an auth URI to redirect the user to given a state. */
|
||||
function authURI (state: string) {
|
||||
return `${discordAuthURIBase}&state=${encodeURIComponent(state)}`;
|
||||
}
|
||||
// use access token to get user info
|
||||
const userInfoResponse = await fetch(
|
||||
'https://discordapp.com/api/v6/users/@me',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokensData.access_token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (userInfoResponse.status !== 200) {
|
||||
throw new Error(
|
||||
`error requesting user info: ${tokenResponse.status}`,
|
||||
);
|
||||
}
|
||||
const userInfo = await userInfoResponse.json();
|
||||
|
||||
// return the user's service-unique ID
|
||||
return userInfo.id;
|
||||
},
|
||||
} satisfies AuthProviderImplementation;
|
||||
|
||||
/** Generates a formdata body from key-value pairs. */
|
||||
function formData (content: Record<string, string>) {
|
||||
|
@ -29,114 +76,3 @@ function formData (content: Record<string, string>) {
|
|||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an avatar URL for the user. Discord avatars are weird so I split
|
||||
* this out.
|
||||
* @see https://discordapp.com/developers/docs/reference#image-formatting
|
||||
*/
|
||||
export function discordAvatarURL (userInfo: {
|
||||
id: string;
|
||||
avatar?: string;
|
||||
discriminator: string;
|
||||
}) {
|
||||
if (userInfo.avatar) {
|
||||
return `https://cdn.discordapp.com/avatars/${userInfo.id}/${userInfo.avatar}.png`;
|
||||
}
|
||||
return `https://cdn.discordapp.com/embed/avatars/${
|
||||
parseInt(userInfo.discriminator, 10) % 5
|
||||
}.png`;
|
||||
}
|
||||
|
||||
/** OAuth tokens returned by Discord. */
|
||||
export interface DiscordTokens {
|
||||
/** The access token used to authorize to Discord */
|
||||
accessToken: string;
|
||||
/** The refresh token used to get a new access token when the current one expires */
|
||||
refreshToken: string;
|
||||
/** The string "Bearer"; other token types aren't relevant here */
|
||||
tokenType: string;
|
||||
/** space-separated list of authorized scopes */
|
||||
scope: string;
|
||||
/** Expiration date of the access token */
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
/** Exchanges a code for an access/refresh token pair. */
|
||||
export async function fetchDiscordTokens (
|
||||
code: string,
|
||||
): Promise<DiscordTokens> {
|
||||
const response = await fetch('https://discordapp.com/api/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData({
|
||||
client_id: env.DISCORD_CLIENT_ID,
|
||||
client_secret: env.DISCORD_CLIENT_SECRET,
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: discordRedirectURI,
|
||||
scope: 'identify', // Discord-specific: scope is required here too and must match
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(
|
||||
`Discord gave non-200 response status when requesting tokens: ${response.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
throw new Error(
|
||||
`Discord gave an error when requesting tokens: ${data.error}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
tokenType: data.token_type,
|
||||
scope: data.scope,
|
||||
expiresAt: new Date(Date.now() + data.expires_in * 1000),
|
||||
};
|
||||
}
|
||||
|
||||
/** Information about a Discord user. */
|
||||
export interface DiscordUserInfo {
|
||||
id: string;
|
||||
username: string;
|
||||
discriminator: string;
|
||||
avatarURL: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches information about the user given their access token.
|
||||
* @param {string} accessToken
|
||||
* @returns {Promise<object>} Object has keys `name`, `avatarURL`, and
|
||||
* `created`.
|
||||
*/
|
||||
export async function fetchDiscordUserInfo (
|
||||
fetch: any,
|
||||
accessToken: string,
|
||||
): Promise<DiscordUserInfo> {
|
||||
const response = await fetch('https://discordapp.com/api/v6/users/@me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(
|
||||
`Discord gave non-200 status when fetching user info: ${response.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
username: data.username,
|
||||
discriminator: data.discriminator,
|
||||
id: data.id,
|
||||
avatarURL: discordAvatarURL(data),
|
||||
};
|
||||
}
|
||||
|
|
22
src/lib/server/auth/index.ts
Normal file
22
src/lib/server/auth/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import type {ServerLoadEvent} from '@sveltejs/kit';
|
||||
import DiscordAuthImplementation from './discord';
|
||||
|
||||
export enum AuthProvider {
|
||||
DISCORD,
|
||||
}
|
||||
|
||||
export interface AuthProviderImplementation {
|
||||
redirectURI: string;
|
||||
buildAuthURI: (state: string) => string;
|
||||
getUserIdentifier: (
|
||||
code: string,
|
||||
event: ServerLoadEvent,
|
||||
) => Promise<string>;
|
||||
}
|
||||
|
||||
export const authProviderImplementations: Record<
|
||||
AuthProvider,
|
||||
AuthProviderImplementation
|
||||
> = {
|
||||
[AuthProvider.DISCORD]: DiscordAuthImplementation,
|
||||
};
|
|
@ -1,11 +1,8 @@
|
|||
import {BeforeInsert, Column, Entity, ManyToOne, PrimaryColumn} from 'typeorm';
|
||||
|
||||
import {AuthProvider} from '$lib/server/auth';
|
||||
import {User} from '$lib/server/entity/User';
|
||||
import {ulidMonotonic} from '$lib/server/ulid';
|
||||
import {User} from './User';
|
||||
|
||||
export enum AuthProvider {
|
||||
DISCORD,
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class AuthMethod {
|
||||
|
|
|
@ -1,51 +1,19 @@
|
|||
import {BeforeInsert, Column, Entity, ManyToOne, PrimaryColumn} from 'typeorm';
|
||||
import {EncryptionTransformer} from 'typeorm-encrypted';
|
||||
import {BeforeInsert, Entity, ManyToOne, PrimaryColumn} from 'typeorm';
|
||||
|
||||
import {AuthProvider} from '$lib/server/entity/AuthMethod';
|
||||
import {AuthMethod} from '$lib/server/entity/AuthMethod';
|
||||
import {ulidMonotonic} from '$lib/server/ulid';
|
||||
|
||||
import {env} from '$env/dynamic/private';
|
||||
|
||||
@Entity()
|
||||
export class AuthSession {
|
||||
/** The ID of the session */
|
||||
// TODO should we be using ulids for this or something totally random
|
||||
// TODO should we be using ulids for this or something crypt-random
|
||||
@PrimaryColumn({type: 'varchar', length: 26, nullable: false})
|
||||
id = ulidMonotonic();
|
||||
|
||||
/** The authentication provider used to start this session */
|
||||
@Column({type: 'enum', enum: AuthProvider, nullable: false})
|
||||
provider!: AuthProvider;
|
||||
@ManyToOne(() => AuthMethod, authMethod => authMethod.id, {nullable: false})
|
||||
authMethod!: AuthMethod;
|
||||
|
||||
/** The access token */
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 300,
|
||||
nullable: false,
|
||||
transformer: new EncryptionTransformer({
|
||||
algorithm: 'aes-256-cbc',
|
||||
key: env.ENCRYPTION_KEY!,
|
||||
ivLength: 16,
|
||||
}),
|
||||
})
|
||||
accessToken!: string;
|
||||
|
||||
/** The refresh token */
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 300,
|
||||
nullable: true,
|
||||
transformer: new EncryptionTransformer({
|
||||
algorithm: 'aes-256-cbc',
|
||||
key: env.ENCRYPTION_KEY!,
|
||||
ivLength: 16,
|
||||
}),
|
||||
})
|
||||
refreshToken!: string;
|
||||
|
||||
/** When the access token expires */
|
||||
@Column({type: 'timestamptz', nullable: true})
|
||||
accessTokenExpiresAt!: Date;
|
||||
// TODO expiration of sessions?
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert () {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as crypto from 'node:crypto';
|
||||
import {BeforeInsert, Column, Entity, ManyToOne, PrimaryColumn} from 'typeorm';
|
||||
|
||||
import {AuthProvider} from '$lib/server/entity/AuthMethod';
|
||||
import {AuthProvider} from '$lib/server/auth';
|
||||
import {ulidMonotonic} from '$lib/server/ulid';
|
||||
|
||||
@Entity()
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {discordAvatarURL} from '$lib/server/auth/discord';
|
||||
import {getDataSource} from '$lib/server/db';
|
||||
import {AuthSession} from '$lib/server/entity/AuthSession';
|
||||
|
||||
|
@ -14,38 +13,22 @@ async function findSession (sessionID?: string) {
|
|||
|
||||
export const load: LayoutServerLoad = async ({cookies}) => {
|
||||
const sessionID = cookies.get('sessionid');
|
||||
let session = await findSession(sessionID);
|
||||
if (!session) {
|
||||
return {};
|
||||
}
|
||||
console.log('i am happening');
|
||||
|
||||
const response = await fetch('https://discordapp.com/api/v6/users/@me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log(await response.text());
|
||||
return {
|
||||
error: {
|
||||
code: 'user_info_fetch_failed',
|
||||
description:
|
||||
`Discord gave non-200 status when fetching user info: ${response.status}`,
|
||||
const dataSource = await getDataSource();
|
||||
const sessionsRepo = dataSource.getRepository(AuthSession);
|
||||
const session = sessionID
|
||||
? await sessionsRepo.findOne({
|
||||
where: {id: sessionID},
|
||||
relations: {
|
||||
authMethod: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('have data', data.username);
|
||||
})
|
||||
: null;
|
||||
|
||||
return {
|
||||
user: {
|
||||
username: data.username,
|
||||
discriminator: data.discriminator,
|
||||
id: data.id,
|
||||
avatarURL: discordAvatarURL(data),
|
||||
},
|
||||
user: session
|
||||
? JSON.parse(JSON.stringify(session.authMethod.user))
|
||||
: null,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
|
||||
{#if data.user}
|
||||
<h1>Success!</h1>
|
||||
<p>You are {data.user.username}#{data.user.discriminator} and your ID is {data.user.id}</p>
|
||||
<img src={data.user.avatarURL} alt="Discord avatar of {data.user.username}">
|
||||
<p>You are {data.user.name} and your ID is {data.user.id}</p>
|
||||
{:else}
|
||||
<h1>Hello</h1>
|
||||
<p>I do not know you</p>
|
||||
|
|
38
src/routes/auth/[provider]/+page.server.ts
Normal file
38
src/routes/auth/[provider]/+page.server.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import {redirect} from '@sveltejs/kit';
|
||||
import * as crypto from 'node:crypto';
|
||||
|
||||
import {getDataSource} from '$lib/server/db';
|
||||
import {AuthState} from '$lib/server/entity/AuthState';
|
||||
|
||||
import {env} from '$env/dynamic/private';
|
||||
import {
|
||||
AuthProvider,
|
||||
type AuthProviderImplementation,
|
||||
authProviderImplementations,
|
||||
} from '$lib/server/auth';
|
||||
import type {PageServerLoad} from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({cookies, params}) => {
|
||||
// figure out what auth provider the user wanted to try
|
||||
let provider: AuthProvider;
|
||||
if (params.provider === 'discord') {
|
||||
provider = AuthProvider.DISCORD;
|
||||
} else {
|
||||
// idk thats not a valid provider, uhh,
|
||||
throw redirect(302, '/');
|
||||
}
|
||||
|
||||
const dataSource = await getDataSource();
|
||||
const providerImpl = authProviderImplementations[provider];
|
||||
|
||||
// store a state for this login attempt
|
||||
const authStatesRepo = await dataSource.getRepository(AuthState);
|
||||
const state = authStatesRepo.create({provider});
|
||||
await authStatesRepo.save(state);
|
||||
|
||||
// set the state ID as a cookie so we can retrieve it later and compare
|
||||
cookies.set('stateid', state.id, {path: '/auth/discord'});
|
||||
|
||||
// redirect to the provider with the state
|
||||
throw redirect(302, providerImpl.buildAuthURI(state.state));
|
||||
};
|
108
src/routes/auth/[provider]/callback/+page.server.ts
Normal file
108
src/routes/auth/[provider]/callback/+page.server.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
import {getDataSource} from '$lib/server/db';
|
||||
import {AuthSession} from '$lib/server/entity/AuthSession';
|
||||
import {AuthState} from '$lib/server/entity/AuthState';
|
||||
import {redirect} from '@sveltejs/kit';
|
||||
import type {PageServerLoad} from './$types';
|
||||
|
||||
import {AuthProvider, authProviderImplementations} from '$lib/server/auth';
|
||||
import {AuthMethod} from '$lib/server/entity/AuthMethod';
|
||||
import {User} from '$lib/server/entity/User';
|
||||
|
||||
export const load: PageServerLoad = async event => {
|
||||
// figure out what auth provider has redirected the user here
|
||||
let provider: AuthProvider;
|
||||
if (event.params.provider === 'discord') {
|
||||
provider = AuthProvider.DISCORD;
|
||||
} else {
|
||||
// idk thats not a valid provider, uhh,
|
||||
throw redirect(302, '/');
|
||||
}
|
||||
|
||||
// check for errors from the provider
|
||||
// TODO: this is still technically provider-specific and should be split out
|
||||
// into the provider implementations since different providers can call back
|
||||
// with different parameters
|
||||
const errorCode = event.url.searchParams.get('error');
|
||||
const errorDescription = event.url.searchParams.get('error_description');
|
||||
|
||||
// if the user cancelled the login, redirect home gracefully
|
||||
if (errorCode === 'access_denied') {
|
||||
throw redirect(302, '/');
|
||||
}
|
||||
|
||||
// if another error was encountered, return the error information only
|
||||
if (errorCode) {
|
||||
return {
|
||||
error: {
|
||||
code: errorCode,
|
||||
description: errorDescription ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const providerImpl = authProviderImplementations[provider];
|
||||
|
||||
// retrieve the state we stored for this session and compare against the
|
||||
// state we received from the provider
|
||||
const dataSource = await getDataSource();
|
||||
const statesRepo = dataSource.getRepository(AuthState);
|
||||
|
||||
const stateID = event.cookies.get('stateid');
|
||||
let storedState: AuthState | null = null;
|
||||
if (stateID) {
|
||||
storedState = await statesRepo.findOne({where: {id: stateID}});
|
||||
}
|
||||
|
||||
const receivedState = event.url.searchParams.get('state');
|
||||
|
||||
if (!storedState || !receivedState || storedState.state !== receivedState) {
|
||||
return {
|
||||
error: {
|
||||
code: 'consumer_state_mismatch',
|
||||
description:
|
||||
`Expected state ${storedState?.state}, received ${receivedState}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const code = event.url.searchParams.get('code');
|
||||
if (!code) {
|
||||
return {
|
||||
error: {
|
||||
code: 'no_code',
|
||||
description: 'No code was received',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// trade in the code for a user identifier
|
||||
const userIdentifier = await providerImpl.getUserIdentifier(code, event);
|
||||
|
||||
// see if any users have registered matching authentication methods
|
||||
const authMethodsRepo = dataSource.getRepository(AuthMethod);
|
||||
let authMethod = await authMethodsRepo.findOne({
|
||||
where: {
|
||||
provider,
|
||||
userIdentifier,
|
||||
},
|
||||
});
|
||||
|
||||
// if there is not yet anyone registered using this auth method, create a
|
||||
// new user and a new auth method for that user
|
||||
if (!authMethod) {
|
||||
throw redirect(302, '/'); // TODO
|
||||
}
|
||||
|
||||
// Create a new auth session for this auth method and save it
|
||||
const authSessionRepo = dataSource.getRepository(AuthSession);
|
||||
const authSession = authSessionRepo.create({authMethod});
|
||||
await authSessionRepo.save(authSession);
|
||||
event.cookies.set('sessionid', authSession.id, {path: '/'});
|
||||
|
||||
// remove the state we were using now that we're done with it
|
||||
await statesRepo.remove(storedState);
|
||||
event.cookies.delete('stateid');
|
||||
|
||||
// Woo we did it, redirect on to wherever we were trying to go before
|
||||
throw redirect(302, '/');
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
import {redirect} from '@sveltejs/kit';
|
||||
import * as crypto from 'node:crypto';
|
||||
|
||||
import {getDataSource} from '$lib/server/db';
|
||||
import {AuthState} from '$lib/server/entity/AuthState';
|
||||
|
||||
import {env} from '$env/dynamic/private';
|
||||
import {AuthProvider} from '$lib/server/entity/AuthMethod.js';
|
||||
|
||||
/**
|
||||
* The redirect URI for Discord to send the user back to after auth. This must
|
||||
* match what's configured for the OAuth application in the dev applications
|
||||
* panel in order to fetch tokens.
|
||||
*/
|
||||
const discordRedirectURI = `${env.HOST}/auth/discord/callback`;
|
||||
|
||||
/** The base of the URI that starts the OAuth flow. State is attached later. */
|
||||
const authURIBase = 'https://discordapp.com/api/oauth2/authorize'
|
||||
+ `?client_id=${env.DISCORD_CLIENT_ID}`
|
||||
+ '&response_type=code'
|
||||
+ `&redirect_uri=${encodeURIComponent(discordRedirectURI)}`
|
||||
+ `&scope=${encodeURIComponent(['identify', 'guilds'].join(' '))}`
|
||||
+ '&prompt=consent'; // Special Discord-only thing to always show prompt
|
||||
|
||||
/** Generates a complete auth URI given a state. */
|
||||
const buildAuthURI = (state: string) =>
|
||||
`${authURIBase}&state=${encodeURIComponent(state)}`;
|
||||
|
||||
export async function load ({cookies}) {
|
||||
const dataSource = await getDataSource();
|
||||
|
||||
const authStatesRepo = await dataSource.getRepository(AuthState);
|
||||
const state = authStatesRepo.create({
|
||||
provider: AuthProvider.DISCORD,
|
||||
});
|
||||
await authStatesRepo.save(state);
|
||||
|
||||
cookies.set('stateid', state.id, {path: '/auth/discord'});
|
||||
throw redirect(302, buildAuthURI(state.state));
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
import {
|
||||
discordAvatarURL,
|
||||
type DiscordUserInfo,
|
||||
fetchDiscordTokens,
|
||||
fetchDiscordUserInfo,
|
||||
} from '$lib/server/auth/discord';
|
||||
import {getDataSource} from '$lib/server/db';
|
||||
import {AuthProvider} from '$lib/server/entity/AuthMethod';
|
||||
import {AuthSession} from '$lib/server/entity/AuthSession';
|
||||
import {AuthState} from '$lib/server/entity/AuthState';
|
||||
import {redirect} from '@sveltejs/kit';
|
||||
import type {PageServerLoad} from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({cookies, url, fetch}) => {
|
||||
const errorCode = url.searchParams.get('error');
|
||||
const errorDescription = url.searchParams.get('error_description');
|
||||
|
||||
// if the user cancelled the login, redirect home gracefully
|
||||
if (errorCode === 'access_denied') {
|
||||
throw redirect(302, '/');
|
||||
}
|
||||
|
||||
// if another error was encountered, return the error information only
|
||||
if (errorCode) {
|
||||
return {
|
||||
error: {
|
||||
code: errorCode,
|
||||
description: errorDescription ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// retrieve the state we stored for this session and compare against the
|
||||
// state we received from the provider
|
||||
const dataSource = await getDataSource();
|
||||
const statesRepo = dataSource.getRepository(AuthState);
|
||||
|
||||
const stateID = cookies.get('stateid');
|
||||
let storedState: AuthState | null = null;
|
||||
if (stateID) {
|
||||
storedState = await statesRepo.findOne({where: {id: stateID}});
|
||||
}
|
||||
|
||||
const receivedState = url.searchParams.get('state');
|
||||
|
||||
if (!storedState || !receivedState || storedState.state !== receivedState) {
|
||||
return {
|
||||
error: {
|
||||
code: 'consumer_state_mismatch',
|
||||
description:
|
||||
`Expected state ${storedState?.state}, received ${receivedState}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// everything checks out - exchange code for tokens
|
||||
const code = url.searchParams.get('code');
|
||||
if (!code) {
|
||||
return {
|
||||
error: {
|
||||
code: 'no_code',
|
||||
description: 'No code was received',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let tokens;
|
||||
try {
|
||||
tokens = await fetchDiscordTokens(code);
|
||||
} catch (tokenError) {
|
||||
return {
|
||||
error: {
|
||||
code: 'token_exchange_failed',
|
||||
description:
|
||||
`Failed to exchange code for tokens: ${tokenError}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Create a new auth session from the tokens and save it
|
||||
const authSessionRepo = dataSource.getRepository(AuthSession);
|
||||
const authSession = authSessionRepo.create({
|
||||
provider: AuthProvider.DISCORD,
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
accessTokenExpiresAt: tokens.expiresAt,
|
||||
});
|
||||
await authSessionRepo.save(authSession);
|
||||
cookies.set('sessionid', authSession.id, {path: '/'});
|
||||
|
||||
// remove the state we were using now that we're done with it
|
||||
await statesRepo.remove(storedState);
|
||||
cookies.delete('stateid');
|
||||
|
||||
// Woo we did it, redirect on to wherever we were trying to go before
|
||||
throw redirect(302, '/');
|
||||
};
|
Loading…
Reference in a new issue