mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-22 22:32:01 -05:00
feat: ip and user agent on sessin
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -173,3 +173,5 @@ dist
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
.notes/
|
||||
Binary file not shown.
@@ -85,7 +85,9 @@ export default function Page() {
|
||||
Login with Github
|
||||
</Button>
|
||||
<Button variant="secondary" className="gap-2" onClick={async () => {
|
||||
await authClient.passkey.signIn()
|
||||
await authClient.passkey.signIn({
|
||||
callbackURL: "/"
|
||||
})
|
||||
}}>
|
||||
<Key size={16} />
|
||||
Login with Passkey
|
||||
|
||||
@@ -4,11 +4,18 @@ import { authClient } from "./lib/auth-client";
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const session = await authClient.session({
|
||||
headers: request.headers,
|
||||
options: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
if (!session.data) {
|
||||
return NextResponse.redirect(new URL("/sign-in", request.url));
|
||||
}
|
||||
const canInvite = await authClient.org.hasPermission({
|
||||
permission: {
|
||||
invitation: ["create"],
|
||||
},
|
||||
});
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,17 @@ export const getAuthTables = (options: BetterAuthOptions) => {
|
||||
session: {
|
||||
tableName: options.session?.modelName || "session",
|
||||
fields: {
|
||||
expiresAt: {
|
||||
type: "date",
|
||||
},
|
||||
ipAddress: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
userAgent: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
userId: {
|
||||
type: "string",
|
||||
references: {
|
||||
@@ -57,9 +68,6 @@ export const getAuthTables = (options: BetterAuthOptions) => {
|
||||
onDelete: "cascade",
|
||||
},
|
||||
},
|
||||
expiresAt: {
|
||||
type: "date",
|
||||
},
|
||||
},
|
||||
},
|
||||
account: {
|
||||
|
||||
@@ -38,11 +38,13 @@ export const createInternalAdapter = (
|
||||
});
|
||||
return createdUser;
|
||||
},
|
||||
createSession: async (userId: string) => {
|
||||
const data = {
|
||||
createSession: async (userId: string, request?: Request) => {
|
||||
const data: Session = {
|
||||
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
|
||||
userId,
|
||||
expiresAt: getDate(sessionExpiration),
|
||||
ipAddress: request?.headers.get("x-forwarded-for") || "",
|
||||
userAgent: request?.headers.get("user-agent") || "",
|
||||
};
|
||||
const session = adapter.create<Session>({
|
||||
model: tables.session.tableName,
|
||||
|
||||
@@ -32,6 +32,8 @@ export const sessionSchema = z.object({
|
||||
id: z.string(),
|
||||
userId: z.string(),
|
||||
expiresAt: z.date(),
|
||||
ipAddress: z.string().optional(),
|
||||
userAgent: z.string().optional(),
|
||||
});
|
||||
|
||||
export type User = z.infer<typeof userSchema>;
|
||||
|
||||
@@ -15,8 +15,25 @@ export const csrfMiddleware = createAuthMiddleware(
|
||||
if (
|
||||
ctx.request?.method !== "POST" ||
|
||||
ctx.context.options.advanced?.disableCSRFCheck
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(ctx.request.url);
|
||||
console.log({
|
||||
url: ctx.request.url,
|
||||
});
|
||||
/**
|
||||
* If origin is the same as baseURL or if the
|
||||
* origin is in the trustedOrigins then we
|
||||
* don't need to check the CSRF token.
|
||||
*/
|
||||
if (
|
||||
url.origin === ctx.context.baseURL ||
|
||||
ctx.context.options.trustedOrigins?.includes(url.origin)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const csrfToken = ctx.body?.csrfToken;
|
||||
const csrfCookie = await ctx.getSignedCookie(
|
||||
ctx.context.authCookies.csrfToken.name,
|
||||
|
||||
@@ -95,7 +95,10 @@ export const callbackOAuth = createAuthEndpoint(
|
||||
if (!userId) throw new APIError("INTERNAL_SERVER_ERROR");
|
||||
|
||||
//create session
|
||||
const session = await c.context.internalAdapter.createSession(userId);
|
||||
const session = await c.context.internalAdapter.createSession(
|
||||
userId,
|
||||
c.request,
|
||||
);
|
||||
try {
|
||||
await c.setSignedCookie(
|
||||
c.context.authCookies.sessionToken.name,
|
||||
|
||||
@@ -140,6 +140,7 @@ export const signInCredential = createAuthEndpoint(
|
||||
|
||||
const session = await ctx.context.internalAdapter.createSession(
|
||||
user.user.id,
|
||||
ctx.request,
|
||||
);
|
||||
await ctx.setSignedCookie(
|
||||
ctx.context.authCookies.sessionToken.name,
|
||||
|
||||
@@ -69,6 +69,7 @@ export const signUpCredential = createAuthEndpoint(
|
||||
});
|
||||
const session = await ctx.context.internalAdapter.createSession(
|
||||
createdUser.id,
|
||||
ctx.request,
|
||||
);
|
||||
await ctx.setSignedCookie(
|
||||
ctx.context.authCookies.sessionToken.name,
|
||||
|
||||
@@ -8,13 +8,15 @@ import { addCurrentURL, csrfPlugin, redirectPlugin } from "./fetch-plugins";
|
||||
import { InferRoutes } from "./path-to-object";
|
||||
import { ClientOptions, HasPlugin } from "./type";
|
||||
import { getBaseURL } from "../utils/base-url";
|
||||
import { Plugin } from "../types/plugins";
|
||||
import type { router } from "../api";
|
||||
|
||||
export const createVanillaClient = <Auth extends BetterAuth = never>(
|
||||
options?: ClientOptions,
|
||||
) => {
|
||||
type BAuth = Auth extends never ? BetterAuth : Auth;
|
||||
type API = BAuth["api"];
|
||||
type API = Auth extends never
|
||||
? ReturnType<typeof router>["endpoints"]
|
||||
: BAuth["api"];
|
||||
const $fetch = createFetch({
|
||||
...options,
|
||||
baseURL: getBaseURL(options?.baseURL),
|
||||
@@ -40,6 +42,7 @@ export const createVanillaClient = <Auth extends BetterAuth = never>(
|
||||
$activeOrganization,
|
||||
$listOrganizations,
|
||||
},
|
||||
$fetch,
|
||||
};
|
||||
type HasPasskeyConfig = HasPlugin<"passkey", BAuth>;
|
||||
type HasOrganizationConfig = HasPlugin<"organization", BAuth>;
|
||||
@@ -48,6 +51,7 @@ export const createVanillaClient = <Auth extends BetterAuth = never>(
|
||||
| (HasPasskeyConfig extends true ? "passkey" : never)
|
||||
| (HasOrganizationConfig extends true ? "setActiveOrganization" : never)
|
||||
| "$atoms"
|
||||
| "$fetch"
|
||||
>;
|
||||
const proxy = createDynamicPathProxy(actions, $fetch, {
|
||||
"/create/organization": $listOrg,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BetterFetchResponse } from "@better-fetch/fetch";
|
||||
import { BetterFetchOption, BetterFetchResponse } from "@better-fetch/fetch";
|
||||
import { Context, Endpoint } from "better-call";
|
||||
import {
|
||||
HasRequiredKeys,
|
||||
@@ -24,10 +24,20 @@ type InferCtx<C extends Context<any, any>> = C["body"] extends Record<
|
||||
string,
|
||||
any
|
||||
>
|
||||
? C["body"]
|
||||
? C["body"] & {
|
||||
options?: BetterFetchOption<undefined, C["query"], C["params"]>;
|
||||
}
|
||||
: C["query"] extends Record<string, any>
|
||||
? C["query"]
|
||||
: never;
|
||||
? {
|
||||
query: C["query"];
|
||||
options?: Omit<
|
||||
BetterFetchOption<C["body"], C["query"], C["params"]>,
|
||||
"query"
|
||||
>;
|
||||
}
|
||||
: {
|
||||
options?: BetterFetchOption<C["body"], C["query"], C["params"]>;
|
||||
};
|
||||
|
||||
type MergeRoutes<T> = UnionToIntersection<T>;
|
||||
type InferRoute<API> = API extends {
|
||||
@@ -52,4 +62,12 @@ type InferRoute<API> = API extends {
|
||||
>
|
||||
: never
|
||||
: never;
|
||||
export type InferRoutes<API> = MergeRoutes<InferRoute<API>>;
|
||||
export type InferRoutes<API extends Record<string, Endpoint>> = MergeRoutes<
|
||||
InferRoute<API>
|
||||
>;
|
||||
|
||||
export interface ProxyRequest {
|
||||
options?: BetterFetchOption<any, any>;
|
||||
query?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BetterFetch, BetterFetchOption } from "@better-fetch/fetch";
|
||||
import { PreinitializedWritableAtom } from "nanostores";
|
||||
import { ProxyRequest } from "./path-to-object";
|
||||
|
||||
const knownPathMethods: Record<string, "POST" | "GET"> = {
|
||||
"/sign-out": "POST",
|
||||
@@ -9,12 +10,16 @@ const knownPathMethods: Record<string, "POST" | "GET"> = {
|
||||
"/two-factor/send-otp": "POST",
|
||||
};
|
||||
|
||||
function getMethod(path: string, args?: BetterFetchOption) {
|
||||
function getMethod(path: string, args?: ProxyRequest) {
|
||||
const method = knownPathMethods[path];
|
||||
const { options, query, ...body } = args || {};
|
||||
if (method) {
|
||||
return method;
|
||||
}
|
||||
if (args?.body) {
|
||||
if (options?.method) {
|
||||
return options.method;
|
||||
}
|
||||
if (body && Object.keys(body).length > 0) {
|
||||
return "POST";
|
||||
}
|
||||
return "GET";
|
||||
@@ -49,13 +54,13 @@ export function createDynamicPathProxy<T extends Record<string, any>>(
|
||||
)
|
||||
.join("/");
|
||||
const routePath = `/${path}`;
|
||||
const method = getMethod(routePath, args[0]);
|
||||
const body = args[0]?.body;
|
||||
const query = args[0]?.query;
|
||||
const arg = (args[0] || {}) as ProxyRequest;
|
||||
const method = getMethod(routePath, arg);
|
||||
const { query, options, ...body } = arg;
|
||||
return await client(routePath, {
|
||||
...args[0],
|
||||
...options,
|
||||
body: method === "GET" ? undefined : body,
|
||||
query: method === "POST" ? undefined : query,
|
||||
query: query,
|
||||
method,
|
||||
onSuccess() {
|
||||
const signal = $signal?.[routePath as string];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z, ZodArray, ZodLiteral, ZodObject, ZodOptional } from "zod";
|
||||
import { User } from "../../adapters/schema";
|
||||
import { createAuthEndpoint } from "../../api/call";
|
||||
import { Plugin } from "../../types/plugins";
|
||||
import { BetterAuthPlugin } from "../../types/plugins";
|
||||
import { shimContext } from "../../utils/shim";
|
||||
import {
|
||||
createOrganization,
|
||||
@@ -111,8 +111,8 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
|
||||
hasPermission: createAuthEndpoint(
|
||||
"/org/has-permission",
|
||||
{
|
||||
method: "GET",
|
||||
query: z.object({
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
permission: z.record(z.string(), z.array(z.string())),
|
||||
}) as unknown as ZodObject<{
|
||||
permission: ZodObject<{
|
||||
@@ -200,5 +200,5 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Plugin;
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
verifyAuthenticationResponse,
|
||||
verifyRegistrationResponse,
|
||||
} from "@simplewebauthn/server";
|
||||
import { Plugin } from "../../types/plugins";
|
||||
import { BetterAuthPlugin } from "../../types/plugins";
|
||||
import { APIError } from "better-call";
|
||||
|
||||
export interface PasskeyOptions {
|
||||
@@ -249,45 +249,55 @@ export const passkey = (options: PasskeyOptions) => {
|
||||
});
|
||||
}
|
||||
|
||||
const verification = await verifyRegistrationResponse({
|
||||
response: resp,
|
||||
expectedChallenge,
|
||||
expectedOrigin: origin,
|
||||
expectedRPID: options.rpID,
|
||||
});
|
||||
const { verified, registrationInfo } = verification;
|
||||
if (!verified || !registrationInfo) {
|
||||
try {
|
||||
const verification = await verifyRegistrationResponse({
|
||||
response: resp,
|
||||
expectedChallenge,
|
||||
expectedOrigin: origin,
|
||||
expectedRPID: options.rpID,
|
||||
});
|
||||
const { verified, registrationInfo } = verification;
|
||||
if (!verified || !registrationInfo) {
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
const {
|
||||
credentialID,
|
||||
credentialPublicKey,
|
||||
counter,
|
||||
credentialDeviceType,
|
||||
credentialBackedUp,
|
||||
} = registrationInfo;
|
||||
const pubKey = Buffer.from(credentialPublicKey).toString("base64");
|
||||
const userID = generateRandomString(32, alphabet("a-z", "0-9"));
|
||||
const newPasskey: Passkey = {
|
||||
userId: userData.id,
|
||||
webauthnUserID: userID,
|
||||
id: credentialID,
|
||||
publicKey: pubKey,
|
||||
counter,
|
||||
deviceType: credentialDeviceType,
|
||||
transports: resp.response.transports.join(","),
|
||||
backedUp: credentialBackedUp,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
const newPasskeyRes = await ctx.context.adapter.create<Passkey>({
|
||||
model: "passkey",
|
||||
data: newPasskey,
|
||||
});
|
||||
return ctx.json(newPasskeyRes, {
|
||||
status: 200,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Registration failed",
|
||||
},
|
||||
});
|
||||
}
|
||||
const {
|
||||
credentialID,
|
||||
credentialPublicKey,
|
||||
counter,
|
||||
credentialDeviceType,
|
||||
credentialBackedUp,
|
||||
} = registrationInfo;
|
||||
const pubKey = Buffer.from(credentialPublicKey).toString("base64");
|
||||
const userID = generateRandomString(32, alphabet("a-z", "0-9"));
|
||||
const newPasskey: Passkey = {
|
||||
userId: userData.id,
|
||||
webauthnUserID: userID,
|
||||
id: credentialID,
|
||||
publicKey: pubKey,
|
||||
counter,
|
||||
deviceType: credentialDeviceType,
|
||||
transports: resp.response.transports.join(","),
|
||||
backedUp: credentialBackedUp,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
const newPasskeyRes = await ctx.context.adapter.create<Passkey>({
|
||||
model: "passkey",
|
||||
data: newPasskey,
|
||||
});
|
||||
return ctx.json(newPasskeyRes, {
|
||||
status: 200,
|
||||
});
|
||||
},
|
||||
),
|
||||
verifyPasskeyAuthentication: createAuthEndpoint(
|
||||
@@ -335,67 +345,79 @@ export const passkey = (options: PasskeyOptions) => {
|
||||
},
|
||||
});
|
||||
}
|
||||
const verification = await verifyAuthenticationResponse({
|
||||
response: resp as AuthenticationResponseJSON,
|
||||
expectedChallenge,
|
||||
expectedOrigin: origin,
|
||||
expectedRPID: opts.rpID,
|
||||
authenticator: {
|
||||
credentialID: passkey.id,
|
||||
credentialPublicKey: new Uint8Array(
|
||||
Buffer.from(passkey.publicKey, "base64"),
|
||||
),
|
||||
counter: passkey.counter,
|
||||
transports: passkey.transports?.split(
|
||||
",",
|
||||
) as AuthenticatorTransportFuture[],
|
||||
},
|
||||
});
|
||||
const { verified } = verification;
|
||||
if (!verified)
|
||||
return ctx.json(null, {
|
||||
status: 401,
|
||||
body: {
|
||||
message: "verification failed",
|
||||
try {
|
||||
console.log({ resp });
|
||||
const verification = await verifyAuthenticationResponse({
|
||||
response: resp as AuthenticationResponseJSON,
|
||||
expectedChallenge,
|
||||
expectedOrigin: origin,
|
||||
expectedRPID: opts.rpID,
|
||||
authenticator: {
|
||||
credentialID: passkey.id,
|
||||
credentialPublicKey: new Uint8Array(
|
||||
Buffer.from(passkey.publicKey, "base64"),
|
||||
),
|
||||
counter: passkey.counter,
|
||||
transports: passkey.transports?.split(
|
||||
",",
|
||||
) as AuthenticatorTransportFuture[],
|
||||
},
|
||||
});
|
||||
const { verified } = verification;
|
||||
if (!verified)
|
||||
return ctx.json(null, {
|
||||
status: 401,
|
||||
body: {
|
||||
message: "verification failed",
|
||||
},
|
||||
});
|
||||
|
||||
await ctx.context.adapter.update<Passkey>({
|
||||
model: "passkey",
|
||||
where: [
|
||||
{
|
||||
field: "id",
|
||||
value: passkey.id,
|
||||
await ctx.context.adapter.update<Passkey>({
|
||||
model: "passkey",
|
||||
where: [
|
||||
{
|
||||
field: "id",
|
||||
value: passkey.id,
|
||||
},
|
||||
],
|
||||
update: {
|
||||
counter: verification.authenticationInfo.newCounter,
|
||||
},
|
||||
});
|
||||
const s = await ctx.context.internalAdapter.createSession(
|
||||
passkey.userId,
|
||||
ctx.request,
|
||||
);
|
||||
await ctx.setSignedCookie(
|
||||
ctx.context.authCookies.sessionToken.name,
|
||||
s.id,
|
||||
ctx.context.secret,
|
||||
ctx.context.authCookies.sessionToken.options,
|
||||
);
|
||||
if (callbackURL) {
|
||||
return ctx.json({
|
||||
url: callbackURL,
|
||||
redirect: true,
|
||||
session: s,
|
||||
});
|
||||
}
|
||||
return ctx.json(
|
||||
{
|
||||
session: s,
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
ctx.context.logger.error(e);
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Authentication failed",
|
||||
},
|
||||
],
|
||||
update: {
|
||||
counter: passkey.counter + 1,
|
||||
},
|
||||
});
|
||||
const s = await ctx.context.internalAdapter.createSession(
|
||||
passkey.userId,
|
||||
);
|
||||
await ctx.setSignedCookie(
|
||||
ctx.context.authCookies.sessionToken.name,
|
||||
s.id,
|
||||
ctx.context.secret,
|
||||
ctx.context.authCookies.sessionToken.options,
|
||||
);
|
||||
if (callbackURL) {
|
||||
return ctx.json({
|
||||
url: callbackURL,
|
||||
redirect: true,
|
||||
session: s,
|
||||
});
|
||||
}
|
||||
return ctx.json(
|
||||
{
|
||||
session: s,
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
@@ -436,5 +458,5 @@ export const passkey = (options: PasskeyOptions) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Plugin;
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { BetterAuthPlugin } from "../../types/plugins";
|
||||
|
||||
export const rememberMePlugin = async () => {
|
||||
return {} satisfies BetterAuthPlugin;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { createAuthEndpoint } from "../../api/call";
|
||||
import { Plugin } from "../../types/plugins";
|
||||
import { BetterAuthPlugin } from "../../types/plugins";
|
||||
import { totp2fa } from "./totp";
|
||||
import { TwoFactorOptions, UserWithTwoFactor } from "./types";
|
||||
import {
|
||||
@@ -113,5 +113,5 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Plugin;
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
@@ -94,8 +94,6 @@ export const otp2fa = (options?: OTPOptions) => {
|
||||
Buffer.from(ctx.context.secret),
|
||||
parseInt(randomNumber),
|
||||
);
|
||||
console.log(toCheckOtp, ctx.body.code);
|
||||
|
||||
if (toCheckOtp === ctx.body.code) {
|
||||
ctx.setCookie(cookie.name, "", {
|
||||
path: "/",
|
||||
|
||||
@@ -160,4 +160,8 @@ export interface BetterAuthOptions {
|
||||
*/
|
||||
sendVerificationEmail?: (email: string, url: string) => Promise<void>;
|
||||
};
|
||||
/**
|
||||
* List of trusted origins.
|
||||
*/
|
||||
trustedOrigins?: string[];
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export type PluginSchema = {
|
||||
};
|
||||
};
|
||||
|
||||
export type Plugin = {
|
||||
export type BetterAuthPlugin = {
|
||||
id: LiteralString;
|
||||
endpoints: {
|
||||
[key: string]: AuthEndpoint;
|
||||
|
||||
7579
pnpm-lock.yaml
generated
7579
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user