diff --git a/services/api/.env b/services/api/.env
index 851bbf628..8e0f7f4a1 100644
--- a/services/api/.env
+++ b/services/api/.env
@@ -27,6 +27,13 @@ APP_LOGO_URL=
API_URL=http://localhost:2300
+# How to authenticate (password|link|code)
+AUTH_TYPE=password
+# Link/code send by (email|sms)
+AUTH_CHANNEL=email
+# Allow passkey sign-in
+AUTH_PASSKEY=true
+
DEFAULT_TIME_ZONE=America/New_York
# Uploads (local|gcs)
diff --git a/services/api/src/routes/auth/passkey-disabled.test.js b/services/api/src/routes/auth/passkey-disabled.test.js
new file mode 100644
index 000000000..539f526e5
--- /dev/null
+++ b/services/api/src/routes/auth/passkey-disabled.test.js
@@ -0,0 +1,17 @@
+vi.hoisted(() => {
+ process.env.AUTH_PASSKEY = 'false';
+});
+
+import { request } from '../../utils/testing/index.js';
+
+describe('/1/auth/passkey (disabled)', () => {
+ it('should reject passkey routes', async () => {
+ const response = await request('POST', '/1/auth/passkey/generate-login', {});
+ expect(response).toHaveStatus(403);
+ });
+
+ it('should report passkey as disabled in meta', async () => {
+ const response = await request('GET', '/1/meta', {}, {});
+ expect(response.body.data.auth.passkey).toBe(false);
+ });
+});
diff --git a/services/api/src/routes/auth/passkey.js b/services/api/src/routes/auth/passkey.js
index d1bb6f31b..e69dbd1f7 100644
--- a/services/api/src/routes/auth/passkey.js
+++ b/services/api/src/routes/auth/passkey.js
@@ -1,5 +1,6 @@
import Router from '@koa/router';
import yd from '@bedrockio/yada';
+import config from '@bedrockio/config';
import { validateBody } from '../../utils/middleware/validate.js';
import { authenticate } from '../../utils/middleware/authenticate.js';
@@ -18,6 +19,12 @@ import {
const router = new Router();
router
+ .use(async (ctx, next) => {
+ if (!config.get('AUTH_PASSKEY', 'boolean')) {
+ ctx.throw(403, 'Passkey authentication is disabled.');
+ }
+ await next();
+ })
.post('/generate-login', async (ctx) => {
try {
ctx.body = {
diff --git a/services/api/src/routes/meta.js b/services/api/src/routes/meta.js
index 9210a6008..b56c9d7f2 100644
--- a/services/api/src/routes/meta.js
+++ b/services/api/src/routes/meta.js
@@ -1,4 +1,5 @@
import Router from '@koa/router';
+import config from '@bedrockio/config';
import types from '../lib/notifications/types.js';
import roles from '../roles.json' with { type: 'json' };
@@ -10,6 +11,11 @@ router.get('/', async (ctx) => {
data: {
roles,
notifications: types,
+ auth: {
+ type: config.get('AUTH_TYPE'),
+ channel: config.get('AUTH_CHANNEL'),
+ passkey: config.get('AUTH_PASSKEY', 'boolean'),
+ },
},
};
});
diff --git a/services/api/src/routes/meta.test.js b/services/api/src/routes/meta.test.js
index 8bef71367..0e3c199f6 100644
--- a/services/api/src/routes/meta.test.js
+++ b/services/api/src/routes/meta.test.js
@@ -17,6 +17,21 @@ describe('/1/meta', () => {
type: 'product-updated',
},
],
+ auth: {
+ type: 'password',
+ channel: 'email',
+ passkey: true,
+ },
+ });
+ });
+
+ it('should get app meta without authentication', async () => {
+ const response = await request('GET', '/1/meta', {}, {});
+ expect(response).toHaveStatus(200);
+ expect(response.body.data.auth).toEqual({
+ type: 'password',
+ channel: 'email',
+ passkey: true,
});
});
});
diff --git a/services/web/.env b/services/web/.env
index d9847dc94..cee6393cb 100644
--- a/services/web/.env
+++ b/services/web/.env
@@ -26,13 +26,6 @@ SENTRY_DSN=
# Google Tag Manager Analytics
GTM_CONTAINER_ID=
-# How to authenticate (password|link|code)
-AUTH_TYPE=password
-# Link/code send by (email|sms)
-AUTH_CHANNEL=email
-# Allow passkey?
-AUTH_PASSKEY=
-
# Google Maps and Address Lookup
# https://console.cloud.google.com/apis/library/maps-backend.googleapis.com
# https://console.cloud.google.com/apis/library/places-backend.googleapis.com
diff --git a/services/web/src/components/Auth/Federated.js b/services/web/src/components/Auth/Federated.js
index f32c74d5a..1a68ca6a7 100644
--- a/services/web/src/components/Auth/Federated.js
+++ b/services/web/src/components/Auth/Federated.js
@@ -1,3 +1,5 @@
+import { useSession } from 'stores/session';
+
import { canShowAppleSignin } from 'utils/auth/apple';
import { canShowGoogleSignin } from 'utils/auth/google';
import { canShowPasskey } from 'utils/auth/passkey';
@@ -8,12 +10,13 @@ import PasskeyButton from './PasskeyButton';
export default function Federated(props) {
const { type } = props;
+ const { meta } = useSession();
const isSignup = type === 'signup';
const showApple = canShowAppleSignin();
const showGoogle = canShowGoogleSignin();
- const showPasskey = !isSignup && canShowPasskey();
+ const showPasskey = !isSignup && canShowPasskey(meta);
if (!showApple && !showGoogle && !showPasskey) {
return null;
diff --git a/services/web/src/components/Auth/OptionalPassword.js b/services/web/src/components/Auth/OptionalPassword.js
deleted file mode 100644
index a54a5ff6a..000000000
--- a/services/web/src/components/Auth/OptionalPassword.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { PasswordInput } from '@/components/ui/password-input';
-
-import { AUTH_TYPE } from 'utils/env';
-
-export default function OptionalPassword(props) {
- if (AUTH_TYPE !== 'password') {
- return null;
- }
- return