feat: add credentials authentication (#1)

This commit is contained in:
Meier Lukas
2023-12-10 17:12:20 +01:00
committed by GitHub
parent 41e54d940b
commit 3cedb7fba5
53 changed files with 890 additions and 2105 deletions

1
packages/auth/client.ts Normal file
View File

@@ -0,0 +1 @@
export { signIn, signOut } from "next-auth/react";

View File

@@ -0,0 +1,76 @@
import { cookies } from "next/headers";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { db } from "@alparr/db";
import { credentialsConfiguration } from "./providers/credentials";
import { EmptyNextAuthProvider } from "./providers/empty";
import { expireDateAfter, generateSessionToken } from "./session";
const adapter = DrizzleAdapter(db);
const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
export const createConfiguration = (isCredentialsRequest: boolean) =>
NextAuth({
adapter,
providers: [Credentials(credentialsConfiguration), EmptyNextAuthProvider()],
callbacks: {
session: ({ session, user }) => ({
...session,
user: {
...session.user,
id: user.id,
name: user.name,
},
}),
signIn: async ({ user }) => {
if (!isCredentialsRequest) return true;
if (!user) return true;
const sessionToken = generateSessionToken();
const sessionExpiry = expireDateAfter(sessionMaxAgeInSeconds);
// https://github.com/nextauthjs/next-auth/issues/6106
if (!adapter?.createSession) {
return false;
}
await adapter.createSession({
sessionToken: sessionToken,
userId: user.id,
expires: sessionExpiry,
});
cookies().set("next-auth.session-token", sessionToken, {
path: "/",
expires: sessionExpiry,
httpOnly: true,
sameSite: "lax",
secure: true,
});
return true;
},
},
session: {
strategy: "database",
maxAge: sessionMaxAgeInSeconds,
},
pages: {
signIn: "/auth/login",
error: "/auth/login",
},
jwt: {
encode() {
const cookie = cookies().get("next-auth.session-token")?.value;
return cookie ?? "";
},
decode() {
return null;
},
},
});

View File

@@ -3,8 +3,6 @@ import { z } from "zod";
export const env = createEnv({
server: {
AUTH_DISCORD_ID: z.string().min(1),
AUTH_DISCORD_SECRET: z.string().min(1),
AUTH_SECRET:
process.env.NODE_ENV === "production"
? z.string().min(1)
@@ -19,8 +17,6 @@ export const env = createEnv({
},
client: {},
runtimeEnv: {
AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID,
AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET,
AUTH_SECRET: process.env.AUTH_SECRET,
AUTH_URL: process.env.AUTH_URL,
},

View File

@@ -1,12 +1,6 @@
/* eslint-disable @typescript-eslint/unbound-method */
/* @see https://github.com/nextauthjs/next-auth/pull/8932 */
import Discord from "@auth/core/providers/discord";
import type { DefaultSession } from "@auth/core/types";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import NextAuth from "next-auth";
import { db, tableCreator } from "@alparr/db";
import { createConfiguration } from "./configuration";
export type { Session } from "next-auth";
@@ -18,21 +12,8 @@ declare module "next-auth" {
}
}
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
adapter: DrizzleAdapter(db, tableCreator),
providers: [Discord],
callbacks: {
session: ({ session, user }) => ({
...session,
user: {
...session.user,
id: user.id,
},
}),
},
});
export * from "./security";
export const createHandlers = (isCredentialsRequest: boolean) =>
createConfiguration(isCredentialsRequest);
export const { auth } = createConfiguration(false);

View File

@@ -16,6 +16,8 @@
"@auth/core": "^0.18.4",
"@auth/drizzle-adapter": "^0.3.9",
"@t3-oss/env-nextjs": "^0.7.1",
"bcrypt": "^5.1.1",
"cookies": "^0.8.0",
"next": "^14.0.3",
"next-auth": "5.0.0-beta.4",
"react": "18.2.0",
@@ -26,6 +28,9 @@
"@alparr/eslint-config": "workspace:^0.2.0",
"@alparr/prettier-config": "workspace:^0.1.0",
"@alparr/tsconfig": "workspace:^0.1.0",
"@alparr/validation": "workspace:^0.1.0",
"@types/bcrypt": "5.0.2",
"@types/cookies": "0.7.10",
"eslint": "^8.53.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3"
@@ -37,4 +42,4 @@
]
},
"prettier": "@alparr/prettier-config"
}
}

View File

@@ -0,0 +1,49 @@
import type Credentials from "@auth/core/providers/credentials";
import bcrypt from "bcrypt";
import { db, eq } from "@alparr/db";
import { users } from "@alparr/db/schema/sqlite";
import { signInSchema } from "@alparr/validation";
type CredentialsConfiguration = Parameters<typeof Credentials>[0];
export const credentialsConfiguration = {
type: "credentials",
name: "Credentials",
credentials: {
name: {
label: "Username",
type: "text",
},
password: {
label: "Password",
type: "password",
},
},
async authorize(credentials) {
const data = await signInSchema.parseAsync(credentials);
const user = await db.query.users.findFirst({
where: eq(users.name, data.name),
});
if (!user?.password) {
return null;
}
console.log(`user ${user.name} is trying to log in. checking password...`);
const isValidPassword = await bcrypt.compare(data.password, user.password);
if (!isValidPassword) {
console.log(`password for user ${user.name} was incorrect`);
return null;
}
console.log(`user ${user.name} successfully authorized`);
return {
id: user.id,
name: user.name,
};
},
} satisfies CredentialsConfiguration;

View File

@@ -0,0 +1,16 @@
import type { OAuthConfig } from "next-auth/providers";
export function EmptyNextAuthProvider(): OAuthConfig<unknown> {
return {
id: "empty",
name: "Empty",
type: "oauth",
profile: () => {
throw new Error(
"EmptyNextAuthProvider can not be used and is only a placeholder because credentials authentication can not be used as session authentication without additional providers.",
);
},
issuer: "empty",
authorization: new URL("https://example.empty"),
};
}

View File

@@ -0,0 +1,9 @@
import bcrypt from "bcrypt";
export const createSalt = async () => {
return bcrypt.genSalt(10);
};
export const hashPassword = async (password: string, salt: string) => {
return bcrypt.hash(password, salt);
};

9
packages/auth/session.ts Normal file
View File

@@ -0,0 +1,9 @@
import { randomUUID } from "crypto";
export const expireDateAfter = (seconds: number) => {
return new Date(Date.now() + seconds * 1000);
};
export const generateSessionToken = () => {
return randomUUID();
};