diff --git a/docs/authentication/overview.mdx b/docs/authentication/overview.mdx
index 42ff167b9..7dd30f7ee 100644
--- a/docs/authentication/overview.mdx
+++ b/docs/authentication/overview.mdx
@@ -201,3 +201,43 @@ API Keys can be enabled on auth collections. These are particularly useful when
### Custom Strategies
There are cases where these may not be enough for your application. Payload is extendable by design so you can wire up your own strategy when you need to. [More details](./custom-strategies).
+
+### Access Control
+
+Default auth fields including `email`, `username`, and `password` can be overridden by defining a custom field with the same name in your collection config. This allows you to customize the field — including access control — while preserving the underlying auth functionality. For example, you might want to restrict the `email` field from being updated once it is created, or only allow it to be read by certain user roles. You can achieve this by redefining the field and setting access rules accordingly.
+
+Here's an example of how to restrict access to default auth fields:
+
+```ts
+import type { CollectionConfig } from 'payload'
+
+export const Auth: CollectionConfig = {
+ slug: 'users',
+ auth: true,
+ fields: [
+ {
+ name: 'email', // or 'username'
+ type: 'text',
+ access: {
+ create: () => true,
+ read: () => false,
+ update: () => false,
+ },
+ },
+ {
+ name: 'password', // this will be applied to all password-related fields including new password, confirm password.
+ type: 'text',
+ hidden: true, // needed only for the password field to prevent duplication in the Admin panel
+ access: {
+ update: () => false,
+ },
+ },
+ ],
+}
+```
+
+**Note:**
+
+- Access functions will apply across the application — I.e. if `read` access is disabled on `email`, it will not appear in the Admin panel UI or API.
+- Restricting `read` on the `email` or `username` disables the **Unlock** action in the Admin panel as this function requires access to a user-identifying field.
+- When overriding the `password` field, you may need to include `hidden: true` to prevent duplicate fields being displayed in the Admin panel.
diff --git a/packages/ui/src/elements/EmailAndUsername/index.scss b/packages/ui/src/elements/EmailAndUsername/index.scss
new file mode 100644
index 000000000..2407ecf6f
--- /dev/null
+++ b/packages/ui/src/elements/EmailAndUsername/index.scss
@@ -0,0 +1,9 @@
+@import '../../scss/styles.scss';
+
+@layer payload-default {
+ .login-fields {
+ display: flex;
+ flex-direction: column;
+ gap: var(--base);
+ }
+}
diff --git a/packages/ui/src/elements/EmailAndUsername/index.tsx b/packages/ui/src/elements/EmailAndUsername/index.tsx
index 061ad6102..c41017bae 100644
--- a/packages/ui/src/elements/EmailAndUsername/index.tsx
+++ b/packages/ui/src/elements/EmailAndUsername/index.tsx
@@ -3,12 +3,14 @@
import type { TFunction } from '@payloadcms/translations'
import type { LoginWithUsernameOptions, SanitizedFieldPermissions } from 'payload'
-import { email, username } from 'payload/shared'
+import { email, getFieldPermissions, username } from 'payload/shared'
import React from 'react'
import { EmailField } from '../../fields/Email/index.js'
import { TextField } from '../../fields/Text/index.js'
+import './index.scss'
+const baseClass = 'login-fields'
type RenderEmailAndUsernameFieldsProps = {
className?: string
loginWithUsername?: false | LoginWithUsernameOptions
@@ -23,47 +25,92 @@ type RenderEmailAndUsernameFieldsProps = {
}
export function EmailAndUsernameFields(props: RenderEmailAndUsernameFieldsProps) {
- const { className, loginWithUsername, readOnly, t } = props
+ const {
+ className,
+ loginWithUsername,
+ operation: operationFromProps,
+ permissions,
+ readOnly,
+ t,
+ } = props
+
+ function getAuthFieldPermission(fieldName: string, operation: 'read' | 'update') {
+ const permissionsResult = getFieldPermissions({
+ field: { name: fieldName, type: 'text' },
+ operation: operationFromProps === 'create' ? 'create' : operation,
+ parentName: '',
+ permissions,
+ })
+ return permissionsResult.operation
+ }
+
+ const hasEmailFieldOverride =
+ typeof permissions === 'object' && 'email' in permissions && permissions.email
+ const hasUsernameFieldOverride =
+ typeof permissions === 'object' && 'username' in permissions && permissions.username
+
+ const emailPermissions = hasEmailFieldOverride
+ ? {
+ read: getAuthFieldPermission('email', 'read'),
+ update: getAuthFieldPermission('email', 'update'),
+ }
+ : {
+ read: true,
+ update: true,
+ }
+
+ const usernamePermissions = hasUsernameFieldOverride
+ ? {
+ read: getAuthFieldPermission('username', 'read'),
+ update: getAuthFieldPermission('username', 'update'),
+ }
+ : {
+ read: true,
+ update: true,
+ }
const showEmailField =
- !loginWithUsername || loginWithUsername?.requireEmail || loginWithUsername?.allowEmailLogin
+ (!loginWithUsername || loginWithUsername?.requireEmail || loginWithUsername?.allowEmailLogin) &&
+ emailPermissions.read
- const showUsernameField = Boolean(loginWithUsername)
+ const showUsernameField = Boolean(loginWithUsername) && usernamePermissions.read
- return (
-