Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions apps/api/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,17 +13,17 @@
"db:generate": "drizzle-kit generate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"auth:generate": "npx auth@1.6.25 generate --config ./src/auth.ts --output ./src/db/auth-schema.ts --yes"
"auth:generate": "npx auth@1.7.1 generate --config ./src/auth.ts --output ./src/db/auth-schema.ts --yes"
},
"keywords": [],
"author": "",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@better-auth/core": "1.6.26",
"@better-auth/expo": "1.6.26",
"@better-auth/i18n": "1.6.26",
"@better-auth/oauth-provider": "1.6.26",
"@better-auth/passkey": "1.6.26",
"@better-auth/core": "1.7.1",
"@better-auth/expo": "1.7.1",
"@better-auth/i18n": "1.7.1",
"@better-auth/oauth-provider": "1.7.1",
"@better-auth/passkey": "1.7.1",
"@fastify/cors": "^11.3.0",
"@fastify/helmet": "^13.1.1",
"@hey-api/client-fetch": "workspace:*",
Expand All@@ -41,7 +41,7 @@
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/exporter-prometheus": "^0.221.0",
"@opentelemetry/sdk-metrics": "^2.10.0",
"better-auth": "1.6.26",
"better-auth": "1.7.1",
"better-auth-harmony": "^1.3.2",
"cached-hafas-client": "^5.1.9",
"db-vendo-client": "^6.11.1",
Expand Down
55 changes: 42 additions & 13 deletions apps/api/src/auth-oauth-provider.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { readFileSync } from "node:fs";
import { getTableColumns } from "drizzle-orm";
import { getTableConfig } from "drizzle-orm/pg-core";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";

const ORIGINAL_SECRET = process.env.BETTER_AUTH_SECRET;
Expand DownExpand Up@@ -29,15 +30,29 @@ function policyIdentity(id: string, role: string) {

beforeAll(() => {
vi.stubEnv("BETTER_AUTH_SECRET", "test-secret-at-least-thirty-two-characters");
vi.stubGlobal(
"fetch",
vi.fn(async () =>
Response.json({
issuer: "https://www.openstreetmap.org",
authorization_endpoint: "https://www.openstreetmap.org/oauth2/authorize",
token_endpoint: "https://www.openstreetmap.org/oauth2/token",
userinfo_endpoint: "https://api.openstreetmap.org/api/0.6/user/details.json",
jwks_uri: "https://www.openstreetmap.org/oauth2/jwks",
id_token_signing_alg_values_supported: ["RS256"],
}),
),
);
});

afterAll(() => {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
if (ORIGINAL_SECRET !== undefined) process.env.BETTER_AUTH_SECRET = ORIGINAL_SECRET;
});

describe("managed OAuth provider policy", () => {
it("pins every runtime Better Auth family to 1.6.26 and the schema CLI to 1.6.25", () => {
it("pins every runtime Better Auth package and the schema CLI to 1.7.1", () => {
const apiManifest = JSON.parse(
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
) as { dependencies: Record<string, string>; scripts: Record<string, string> };
Expand DownExpand Up@@ -68,27 +83,42 @@ describe("managed OAuth provider policy", () => {
const webPackages = ["@better-auth/core", "@better-auth/passkey", "better-auth"];

for (const packageName of apiPackages) {
expect(apiManifest.dependencies[packageName]).toBe("1.6.26");
expect(apiManifest.dependencies[packageName]).toBe("1.7.1");
const lockName = packageName.startsWith("@") ? `'${packageName}'` : packageName;
expect(apiLock).toContain(`${lockName}:\n specifier: 1.6.26`);
expect(apiLock).toContain(`${lockName}:\n specifier: 1.7.1`);
}
for (const packageName of corePackages) {
expect(coreManifest.dependencies[packageName]).toBe("1.6.26");
expect(coreManifest.dependencies[packageName]).toBe("1.7.1");
const lockName = packageName.startsWith("@") ? `'${packageName}'` : packageName;
expect(coreLock).toContain(`${lockName}:\n specifier: 1.6.26`);
expect(coreLock).toContain(`${lockName}:\n specifier: 1.7.1`);
}
for (const packageName of webPackages) {
expect(webManifest.dependencies[packageName]).toBe("1.6.26");
expect(webManifest.dependencies[packageName]).toBe("1.7.1");
const lockName = packageName.startsWith("@") ? `'${packageName}'` : packageName;
expect(webLock).toContain(`${lockName}:\n specifier: 1.6.26`);
expect(webLock).toContain(`${lockName}:\n specifier: 1.7.1`);
}
expect(apiManifest.scripts["auth:generate"]).toContain("auth@1.6.25 generate");
expect(apiManifest.scripts["auth:generate"]).toContain("auth@1.7.1 generate");
for (const importer of [apiLock, coreLock, webLock]) {
expect(importer).not.toContain("@better-auth/core@1.6.25");
expect(importer).not.toContain("specifier: ^1.6.25");
expect(importer).not.toContain("specifier: 1.6.");
}
});

it("scopes account identity by required issuer and provider account ID", async () => {
const { account } = await import("./db/schema");
const columns = getTableColumns(account);
const config = getTableConfig(account);

expect(columns.issuer).toMatchObject({ notNull: true });
expect(
config.indexes.some(
(index) =>
index.config.unique &&
index.config.columns.map((column) => ("name" in column ? column.name : "")).join(",") ===
"issuer,account_id",
),
).toBe(true);
});

it("exposes every provider table through the application Drizzle schema", async () => {
const schema = await import("./db/schema");

Expand All@@ -103,7 +133,7 @@ describe("managed OAuth provider policy", () => {
);
});

it("keeps the exact 1.6.26 two-factor lockout columns emitted by the pinned generator", async () => {
it("keeps the generated two-factor lockout columns", async () => {
const { twoFactor } = await import("./db/schema");
const columns = getTableColumns(twoFactor);

Expand DownExpand Up@@ -144,8 +174,7 @@ describe("managed OAuth provider policy", () => {
expect(managedOAuthProviderOptions).not.toHaveProperty("cachedTrustedClients");
expect(managedOAuthProviderOptions).not.toHaveProperty("disableJwtPlugin");
expect(managedOAuthProviderOptions).not.toHaveProperty("storeClientSecret");
// Better Auth 1.6's resource-indicator implementation is safe from
// cross-audience escalation only with its single default audience.
// The provider exposes only its first-party resource configuration.
expect(managedOAuthProviderOptions).not.toHaveProperty("validAudiences");
expect(managedOAuthProviderOptions).not.toHaveProperty("customAccessTokenClaims");
});
Expand Down
19 changes: 15 additions & 4 deletions apps/api/src/auth.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,9 +67,9 @@ async function fetchProviderImage(
* the sync's own guard absorbs that.
*/
const providerAvatarSync = createProviderAvatarSync({
async resolveAccessToken(providerId, userId) {
async resolveAccessToken(accountId, userId) {
try {
const result = await auth.api.getAccessToken({ body: { providerId, userId } });
const result = await auth.api.getAccessToken({ body: { accountId, userId } });
return result.accessToken ?? undefined;
} catch {
// Revoked, unrefreshable, or undecryptable. The person simply relinks.
Expand DownExpand Up@@ -142,14 +142,14 @@ const authOptions = {
account: {
create: {
after: async (account) => {
await providerAvatarSync.onAccountCreated(account.providerId, account.userId);
await providerAvatarSync.onAccountCreated(account.id, account.providerId, account.userId);
},
},
update: {
after: async (account) => {
// Better Auth updates the account on each OAuth sign-in and token
// refresh; re-read the picture so a changed provider avatar follows.
await providerAvatarSync.onAccountUpdated(account.providerId, account.userId);
await providerAvatarSync.onAccountUpdated(account.id, account.providerId, account.userId);
},
},
},
Expand DownExpand Up@@ -251,6 +251,17 @@ const authOptions = {
{
providerId: "openstreetmap",
discoveryUrl: getOsmConfig().discoveryUrl,
// Keep the account namespace and core OAuth endpoints available
// when OSM discovery is temporarily unreachable. These values come
// from the same deployment-validated OSM origin; profile identity is
// still proven by the access token against OSM's user-details API.
accountIssuer: new URL(getOsmConfig().webBase).origin,
authorizationUrl: getOsmConfig().webUrl("oauth2/authorize"),
tokenUrl: getOsmConfig().webUrl("oauth2/token"),
// The profile comes from OSM's authenticated user-details endpoint,
// not from Better Auth's local user mapping. Pin its immutable OSM
// numeric ID explicitly so the 1.7 account subject cannot drift.
accountSubject: ({ profile }) => String(profile.id),
clientId: envString("OSM_CLIENT_ID", ""),
clientSecret: envString("OSM_CLIENT_SECRET", ""),
// Ordinary sign-in stays minimal. Contribution write scopes are
Expand Down
Loading