Skip to content

Commit

Permalink
chore: refactor old storage stuff and remove unused routes (#618)
Browse files Browse the repository at this point in the history
  • Loading branch information
BlankParticle authored Jul 28, 2024
1 parent d9ab8ce commit beeeaa6
Show file tree
Hide file tree
Showing 31 changed files with 856 additions and 1,550 deletions.
10 changes: 9 additions & 1 deletion apps/platform/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ const createCachedStorage = <T extends StorageValue = StorageValue>(
});

export const storage = {
auth: createCachedStorage('auth', ms('5 minutes')),
passkeyChallenges: createCachedStorage<PasskeyChallenges>(
'passkey-challenges',
ms('5 minutes')
),
twoFactorLoginChallenges: createCachedStorage<TwoFactorLoginChallenges>(
'two-factor-login-challenges',
ms('5 minutes')
Expand All @@ -44,6 +47,11 @@ export const storage = {
)
};

type PasskeyChallenges = {
type: 'registration' | 'authentication';
challenge: string;
};

type TwoFactorLoginChallenges = {
account: {
id: number;
Expand Down
2 changes: 0 additions & 2 deletions apps/platform/trpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { contactsRouter } from './routers/contactRouter/contactRouter';
import { twoFactorRouter } from './routers/authRouter/twoFactorRouter';
import { passwordRouter } from './routers/authRouter/passwordRouter';
import { recoveryRouter } from './routers/authRouter/recoveryRouter';
import { defaultsRouter } from './routers/userRouter/defaultsRouter';
import { securityRouter } from './routers/userRouter/securityRouter';
import { teamsRouter } from './routers/orgRouter/users/teamsRouter';
import { passkeyRouter } from './routers/authRouter/passkeyRouter';
Expand All @@ -31,7 +30,6 @@ const trpcPlatformAuthRouter = router({
});

const trpcPlatformAccountRouter = router({
defaults: defaultsRouter,
profile: profileRouter,
addresses: addressRouter,
security: securityRouter
Expand Down
16 changes: 8 additions & 8 deletions apps/platform/trpc/routers/authRouter/passkeyRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,17 @@ import {
publicProcedure
} from '~platform/trpc/trpc';
import { createAuthenticator } from '~platform/utils/auth/passkeyUtils';
import { deleteCookie, getCookie, setCookie } from '@u22n/hono/helpers';
import { COOKIE_PASSKEY_CHALLENGE } from '~platform/utils/cookieNames';
import { typeIdGenerator, typeIdValidator } from '@u22n/utils/typeid';
import { createLuciaSessionCookie } from '~platform/utils/session';
import { nanoIdToken, zodSchemas } from '@u22n/utils/zodSchemas';
import { getCookie, setCookie } from '@u22n/hono/helpers';
import { ratelimiter } from '~platform/trpc/ratelimit';
import { validateUsername } from './signupRouter';
import { accounts } from '@u22n/database/schema';
import { datePlus } from '@u22n/utils/ms';
import { TRPCError } from '@trpc/server';
import { eq } from '@u22n/database/orm';
import { ms } from '@u22n/utils/ms';
import { env } from '~platform/env';
import { z } from 'zod';

Expand Down Expand Up @@ -160,17 +161,15 @@ export const passkeyRouter = router({
.use(
ratelimiter({ limit: 20, namespace: 'signIn.passkey.generateChallenge' })
)
.input(z.object({}))
.query(async ({ ctx }) => {
.mutation(async ({ ctx }) => {
const { event } = ctx;

const authChallengeId = nanoIdToken();

setCookie(event, 'unauth-challenge', authChallengeId, {
setCookie(event, COOKIE_PASSKEY_CHALLENGE, authChallengeId, {
httpOnly: true,
secure: env.NODE_ENV === 'production',
sameSite: 'Strict',
maxAge: ms('5 minutes'),
expires: datePlus('5 minutes'),
domain: env.PRIMARY_DOMAIN
});
const passkeyOptions = await generateAuthenticationOptions({
Expand All @@ -195,7 +194,7 @@ export const passkeyRouter = router({
const verificationResponse =
input.verificationResponseRaw as AuthenticationResponseJSON;

const challengeCookie = getCookie(event, 'unauth-challenge');
const challengeCookie = getCookie(event, COOKIE_PASSKEY_CHALLENGE);
if (!challengeCookie) {
throw new TRPCError({
code: 'BAD_REQUEST',
Expand Down Expand Up @@ -246,6 +245,7 @@ export const passkeyRouter = router({
});
}

deleteCookie(ctx.event, COOKIE_PASSKEY_CHALLENGE);
await createLuciaSessionCookie(ctx.event, {
accountId: account.id,
username: account.username,
Expand Down
194 changes: 5 additions & 189 deletions apps/platform/trpc/routers/authRouter/passwordRouter.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import {
router,
accountProcedure,
turnstileProcedure,
publicProcedure
} from '~platform/trpc/trpc';
import { setCookie, getCookie, deleteCookie } from '@u22n/hono/helpers';
import { COOKIE_TWO_FACTOR_LOGIN_CHALLENGE } from '~platform/utils/cookieNames';
import { createLuciaSessionCookie } from '~platform/utils/session';
import { nanoIdToken, zodSchemas } from '@u22n/utils/zodSchemas';
import { strongPasswordSchema } from '@u22n/utils/password';
import { ratelimiter } from '~platform/trpc/ratelimit';
import { typeIdGenerator } from '@u22n/utils/typeid';
import { validateUsername } from './signupRouter';
import { accounts } from '@u22n/database/schema';
import { lucia } from '~platform/utils/auth';
import { setCookie } from '@u22n/hono/helpers';
import { storage } from '~platform/storage';
import { decodeHex } from 'oslo/encoding';
import { TOTPController } from 'oslo/otp';
import { datePlus } from '@u22n/utils/ms';
import { Argon2id } from 'oslo/password';
import { TRPCError } from '@trpc/server';
import { eq } from '@u22n/database/orm';
import { ms } from '@u22n/utils/ms';
import { env } from '~platform/env';
import { z } from 'zod';

Expand Down Expand Up @@ -68,101 +65,7 @@ export const passwordRouter = router({

return { success: true };
}),
signUpWithPassword2FA: publicProcedure
.unstable_concat(turnstileProcedure)
.use(ratelimiter({ limit: 10, namespace: 'signUp.password' }))
.input(
z.object({
username: zodSchemas.username(),
password: strongPasswordSchema,
twoFactorCode: z.string().min(6).max(6)
})
)
.mutation(async ({ ctx, input }) => {
const { username, password, twoFactorCode } = input;
const { db, event } = ctx;

const twoFaChallengeCookie = getCookie(event, 'un-2fa-challenge');
if (!twoFaChallengeCookie) {
return {
success: false,
error: '2FA cookie not found or expired, Please try to setup new 2FA'
};
}

const authStorage = storage.auth;
const twoFaChallenge = await authStorage.getItem(
`un2faChallenge:${username}-${twoFaChallengeCookie}`
);

if (typeof twoFaChallenge !== 'string') {
return {
success: false,
error:
'2FA challenge was invalid or expired, Please try to setup new 2FA'
};
}

const secret = decodeHex(twoFaChallenge);
const isValid = await new TOTPController().verify(twoFactorCode, secret);

if (!isValid) {
return {
success: false,
error: 'Invalid 2FA code'
};
}

const { accountId, publicId, recoveryCode } = await db.transaction(
async (tx) => {
try {
// making sure someone doesn't bypass the client side validation
const { available, error } = await validateUsername(tx, username);
if (!available) {
throw new TRPCError({
code: 'FORBIDDEN',
message: `Username Error : ${error}`
});
}

const passwordHash = await new Argon2id().hash(password);
const publicId = typeIdGenerator('account');

const recoveryCode = nanoIdToken();
const hashedRecoveryCode = await new Argon2id().hash(recoveryCode);

const newUser = await tx.insert(accounts).values({
username,
publicId,
passwordHash,
twoFactorEnabled: true,
twoFactorSecret: twoFaChallenge,
recoveryCode: hashedRecoveryCode
});

return {
accountId: Number(newUser.insertId),
publicId,
recoveryCode
};
} catch (err) {
tx.rollback();
console.error(err);
throw err;
}
}
);

await createLuciaSessionCookie(event, {
accountId,
username,
publicId
});

deleteCookie(event, 'un-2fa-challenge');

return { success: true, error: null, recoveryCode };
}),
signIn: publicProcedure
.unstable_concat(turnstileProcedure)
.use(ratelimiter({ limit: 20, namespace: 'signIn.password' }))
Expand Down Expand Up @@ -241,10 +144,10 @@ export const passwordRouter = router({

setCookie(
ctx.event,
'two-factor-login-challenge',
COOKIE_TWO_FACTOR_LOGIN_CHALLENGE,
twoFactorChallengeId,
{
maxAge: ms('5 minutes'),
expires: datePlus('5 minutes'),
domain: env.PRIMARY_DOMAIN,
httpOnly: true
}
Expand All @@ -264,92 +167,5 @@ export const passwordRouter = router({
defaultOrgShortcode: userResponse.orgMemberships[0]?.org.shortcode
};
}
}),

updateUserPassword: accountProcedure
.input(
z.object({
oldPassword: z.string().min(8),
newPassword: strongPasswordSchema,
otp: zodSchemas.nanoIdToken(),
invalidateAllSessions: z.boolean().default(false)
})
)
.mutation(async ({ ctx, input }) => {
const { db, account, event } = ctx;
const accountId = account.id;

const accountData = await db.query.accounts.findFirst({
where: eq(accounts.id, accountId),
columns: {
publicId: true,
username: true,
passwordHash: true,
twoFactorSecret: true
}
});

if (!accountData) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'User not found'
});
}

if (!accountData.passwordHash) {
throw new TRPCError({
code: 'METHOD_NOT_SUPPORTED',
message: 'Password sign-in is not enabled'
});
}

const oldPasswordValid = await new Argon2id().verify(
accountData.passwordHash,
input.oldPassword
);

if (!oldPasswordValid) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Incorrect old password'
});
}

if (!accountData.twoFactorSecret) {
throw new TRPCError({
code: 'METHOD_NOT_SUPPORTED',
message: '2FA is not enabled on this account, contact support'
});
}
const secret = decodeHex(accountData.twoFactorSecret);
const otpValid = await new TOTPController().verify(input.otp, secret);
if (!otpValid) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Invalid 2FA code'
});
}

const passwordHash = await new Argon2id().hash(input.newPassword);

await db
.update(accounts)
.set({
passwordHash
})
.where(eq(accounts.id, accountId));

// Invalidate all sessions if requested
if (input.invalidateAllSessions) {
await lucia.invalidateUserSessions(accountId);
}

await createLuciaSessionCookie(event, {
accountId,
username: accountData.username,
publicId: accountData.publicId
});

return { success: true };
})
});
Loading

0 comments on commit beeeaa6

Please sign in to comment.