[GH-ISSUE #6527] Unable to access user email inside createAuthMiddleware when using social providers #10542

Closed
opened 2026-04-13 06:45:19 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @david-sfernandes on GitHub (Dec 4, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/6527

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a backend (I'm using Elysia with Bun)
  2. Configure a microsoft social provider
  3. Create a front-end (I'm using Next.js with pnpm)
  4. Add a before hook to access user e-mail on sign-in

I’m trying to implement a before hook in Better Auth to restrict which users are allowed to sign in, following the example in the documentation:
https://www.better-auth.com/docs/concepts/hooks#example-enforce-email-domain-restriction

However, I noticed that this approach only works when signing in with email/password. When using a social provider (in my case, Microsoft), the email field is never present in the ctx.body object as shown in the docs.

Because of that, the before hook can’t access the user’s email, making it impossible to validate whether the sign-in should be allowed.

Is there a different or recommended way to access the user’s email when the sign-in is happening through a social provider inside the createAuthMiddleware before hook?

Current vs. Expected behavior

ctx.body.email should contain the user’s email, just like in the email/password example in the documentation.

What version of Better Auth are you using?

1.4.5

System info

front-end:
{
  "system": {
    "platform": "win32",
    "arch": "x64",
    "version": "Windows 11 Pro",
    "release": "10.0.26200",
    "cpuCount": 6,
    "cpuModel": "Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz",
    "totalMemory": "23.82 GB",
    "freeMemory": "6.74 GB"
  },
  "node": {
    "version": "v24.10.0",
    "env": "development"
  },
  "packageManager": {
    "name": "pnpm",
    "version": "10.19.0"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "16.0.3"
    },
    {
      "name": "react",
      "version": "19.2.0"
    }
  ],
  "databases": null,
  "betterAuth": {
    "version": "^1.4.5",
    "config": null
  }
}

back-end:
{
  "system": {
    "platform": "win32",
    "arch": "x64",
    "version": "Windows 11 Pro",
    "release": "10.0.26200",
    "cpuCount": 6,
    "cpuModel": "Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz",
    "totalMemory": "23.82 GB",
    "freeMemory": "6.74 GB"
  },
  "node": {
    "version": "v24.10.0",
    "env": "development"
  },
  "packageManager": {
    "name": "bun",
    "version": "1.3.0"
  },
  "frameworks": null,
  "databases": [
    {
      "name": "@prisma/client",
      "version": "^6.17.1"
    }
  ],
  "betterAuth": {
    "version": "^1.4.5",
    "config": {
      "emailAndPassword": {
        "enabled": true
      },
      "trustedOrigins": [
        "http://localhost:3010"
      ],
      "socialProviders": {
        "microsoft": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]"
        }
      },
      "hooks": {},
      "plugins": [
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$ERROR_CODES": {
              "FAILED_TO_CREATE_USER": "Failed to create user",
              "USER_ALREADY_EXISTS": "User already exists.",
              "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.",
              "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself",
              "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users",
              "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users",
              "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users",
              "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]",
              "BANNED_USER": "You have been banned from this application",
              "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user",
              "NO_DATA_TO_UPDATE": "No data to update",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users",
              "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself",
              "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": "You are not allowed to set a non-existent role value"
            },
            "schema": {
              "user": {
                "fields": {
                  "role": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banned": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false,
                    "input": false
                  },
                  "banReason": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banExpires": {
                    "type": "date",
                    "required": false,
                    "input": false
                  }
                }
              },
              "session": {
                "fields": {
                  "impersonatedBy": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "options": {
              "ac": {
                "statements": {
                  "client": [
                    "list",
                    "create",
                    "update",
                    "delete"
                  ],
                  "component": [
                    "list",
                    "create",
                    "update",
                    "delete"
                  ],
                  "report": [
                    "list",
                    "create",
                    "update",
                    "delete"
                  ],
                  "recomendations": [
                    "list",
                    "create",
                    "update",
                    "delete"
                  ],
                  "user": [
                    "create",
                    "list",
                    "set-role",
                    "ban",
                    "impersonate",
                    "delete",
                    "set-password",
                    "get",
                    "update"
                  ],
                  "session": [
                    "list",
                    "revoke",
                    "delete"
                  ]
                }
              },
              "defaultRole": "viewer",
              "roles": {
                "admin": {
                  "statements": {
                    "client": [
                      "list",
                      "create",
                      "update",
                      "delete"
                    ],
                    "component": [
                      "list",
                      "create",
                      "update",
                      "delete"
                    ],
                    "report": [
                      "list",
                      "create",
                      "update",
                      "delete"
                    ],
                    "recomendations": [
                      "list",
                      "create",
                      "update",
                      "delete"
                    ],
                    "user": [
                      "create",
                      "list",
                      "set-role",
                      "ban",
                      "impersonate",
                      "delete",
                      "set-password",
                      "get",
                      "update"
                    ],
                    "session": [
                      "list",
                      "revoke",
                      "delete"
                    ]
                  }
                },
                "viewer": {
                  "statements": {
                    "user": [
                      "update"
                    ],
                    "client": [
                      "list"
                    ],
                    "component": [
                      "list"
                    ],
                    "report": [
                      "list"
                    ],
                    "recomendations": [
                      "list"
                    ]
                  }
                }
              }
            }
          }
        },
        {
          "name": "organization",
          "config": {
            "id": "organization",
            "endpoints": {},
            "schema": {
              "organization": {
                "fields": {
                  "name": {
                    "type": "string",
                    "required": true,
                    "sortable": true
                  },
                  "slug": {
                    "type": "string",
                    "required": true,
                    "unique": true,
                    "sortable": true
                  },
                  "logo": {
                    "type": "string",
                    "required": false
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "metadata": {
                    "type": "string",
                    "required": false
                  }
                }
              },
              "member": {
                "fields": {
                  "organizationId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "organization",
                      "field": "id"
                    },
                    "index": true
                  },
                  "userId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "user",
                      "field": "id"
                    },
                    "index": true
                  },
                  "role": {
                    "type": "string",
                    "required": true,
                    "sortable": true,
                    "defaultValue": "member"
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  }
                }
              },
              "invitation": {
                "fields": {
                  "organizationId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "organization",
                      "field": "id"
                    },
                    "index": true
                  },
                  "email": {
                    "type": "string",
                    "required": true,
                    "sortable": true,
                    "index": true
                  },
                  "role": {
                    "type": "string",
                    "required": false,
                    "sortable": true
                  },
                  "status": {
                    "type": "string",
                    "required": true,
                    "sortable": true,
                    "defaultValue": "pending"
                  },
                  "expiresAt": {
                    "type": "date",
                    "required": true
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "inviterId": {
                    "type": "string",
                    "references": {
                      "model": "user",
                      "field": "id"
                    },
                    "required": true
                  }
                }
              },
              "session": {
                "fields": {
                  "activeOrganizationId": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "$Infer": {
              "Organization": {},
              "Invitation": {},
              "Member": {},
              "Team": {},
              "TeamMember": {},
              "ActiveOrganization": {}
            },
            "$ERROR_CODES": {
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION": "You are not allowed to create a new organization",
              "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS": "You have reached the maximum number of organizations",
              "ORGANIZATION_ALREADY_EXISTS": "Organization already exists",
              "ORGANIZATION_SLUG_ALREADY_TAKEN": "Organization slug already taken",
              "ORGANIZATION_NOT_FOUND": "Organization not found",
              "USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "User is not a member of the organization",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION": "You are not allowed to update this organization",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION": "You are not allowed to delete this organization",
              "NO_ACTIVE_ORGANIZATION": "No active organization",
              "USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION": "User is already a member of this organization",
              "MEMBER_NOT_FOUND": "Member not found",
              "ROLE_NOT_FOUND": "Role not found",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM": "You are not allowed to create a new team",
              "TEAM_ALREADY_EXISTS": "Team already exists",
              "TEAM_NOT_FOUND": "Team not found",
              "YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER": "You cannot leave the organization as the only owner",
              "YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER": "You cannot leave the organization without an owner",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER": "You are not allowed to delete this member",
              "YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION": "You are not allowed to invite users to this organization",
              "USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION": "User is already invited to this organization",
              "INVITATION_NOT_FOUND": "Invitation not found",
              "YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION": "You are not the recipient of the invitation",
              "EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION": "Email verification required before accepting or rejecting invitation",
              "YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION": "You are not allowed to cancel this invitation",
              "INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION": "Inviter is no longer a member of the organization",
              "YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE": "You are not allowed to invite a user with this role",
              "FAILED_TO_RETRIEVE_INVITATION": "Failed to retrieve invitation",
              "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS": "You have reached the maximum number of teams",
              "UNABLE_TO_REMOVE_LAST_TEAM": "Unable to remove last team",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER": "You are not allowed to update this member",
              "ORGANIZATION_MEMBERSHIP_LIMIT_REACHED": "Organization membership limit reached",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to create teams in this organization",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to delete teams in this organization",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM": "You are not allowed to update this team",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM": "You are not allowed to delete this team",
              "INVITATION_LIMIT_REACHED": "Invitation limit reached",
              "TEAM_MEMBER_LIMIT_REACHED": "Team member limit reached",
              "USER_IS_NOT_A_MEMBER_OF_THE_TEAM": "User is not a member of the team",
              "YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM": "You are not allowed to list the members of this team",
              "YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM": "You do not have an active team",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER": "You are not allowed to create a new member",
              "YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER": "You are not allowed to remove a team member",
              "YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION": "You are not allowed to access this organization as an owner",
              "YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION": "You are not a member of this organization",
              "MISSING_AC_INSTANCE": "Dynamic Access Control requires a pre-defined ac instance on the server auth plugin. Read server logs for more information",
              "YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE": "You must be in an organization to create a role",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE": "You are not allowed to create a role",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE": "You are not allowed to update a role",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE": "You are not allowed to delete a role",
              "YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE": "You are not allowed to read a role",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE": "You are not allowed to list a role",
              "YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE": "You are not allowed to get a role",
              "TOO_MANY_ROLES": "This organization has too many roles",
              "INVALID_RESOURCE": "The provided permission includes an invalid resource",
              "ROLE_NAME_IS_ALREADY_TAKEN": "That role name is already taken",
              "CANNOT_DELETE_A_PRE_DEFINED_ROLE": "Cannot delete a pre-defined role"
            }
          }
        }
      ]
    }
  }
}

Which area(s) are affected? (Select all that apply)

Backend, Client

Auth config (if applicable)

import { APIError, betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { admin as adminPlugin, createAuthMiddleware, organization } from "better-auth/plugins";
import prisma from "./db";
import { ac, admin, viewer } from "./permissions";

export const auth = betterAuth({
	emailAndPassword: {
		enabled: true
	},
	trustedOrigins: ["http://localhost:3010"],
	database: prismaAdapter(prisma, { provider: "sqlserver" }),
	socialProviders: {
		microsoft: {
			clientId: process.env.MICROSOFT_CLIENT_ID || "",
			clientSecret: process.env.MICROSOFT_CLIENT_SECRET || ""
		},
	},
hooks: {
	before: createAuthMiddleware(async (ctx) => {
		console.info("Hit before hook on path: ", ctx.path)
		if (!["/sign-in/email", "/sign-in/social"].includes(ctx.path)) {
			return;
		}
		console.log(ctx.body)
		// const allowed = ["david.fernandes@terabyte.com.br"]
		const allowed = ["admin@email.com.br"]
		const isAllowed = allowed.includes(ctx.body?.email)
		console.log("isAllowed:", isAllowed)
		if (!allowed.includes(ctx.body?.email)) {
			throw new APIError("BAD_REQUEST", {
				message: "Email not allowed",
			});
		}
	})
},
	plugins: [
		adminPlugin({
			ac,
			defaultRole: "viewer",
			roles: { admin, viewer }
		}),
		organization(),
	],
});

Additional context

No response

Originally created by @david-sfernandes on GitHub (Dec 4, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/6527 ### Is this suited for github? - [ ] Yes, this is suited for github ### To Reproduce 1. Create a backend (I'm using Elysia with Bun) 2. Configure a microsoft social provider 3. Create a front-end (I'm using Next.js with pnpm) 4. Add a before hook to access user e-mail on sign-in I’m trying to implement a before hook in Better Auth to restrict which users are allowed to sign in, following the example in the documentation: https://www.better-auth.com/docs/concepts/hooks#example-enforce-email-domain-restriction However, I noticed that this approach only works when signing in with email/password. When using a social provider (in my case, Microsoft), the email field is never present in the ctx.body object as shown in the docs. Because of that, the before hook can’t access the user’s email, making it impossible to validate whether the sign-in should be allowed. Is there a different or recommended way to access the user’s email when the sign-in is happening through a social provider inside the createAuthMiddleware before hook? ### Current vs. Expected behavior `ctx.body.email` should contain the user’s email, just like in the email/password example in the documentation. ### What version of Better Auth are you using? 1.4.5 ### System info ```bash front-end: { "system": { "platform": "win32", "arch": "x64", "version": "Windows 11 Pro", "release": "10.0.26200", "cpuCount": 6, "cpuModel": "Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz", "totalMemory": "23.82 GB", "freeMemory": "6.74 GB" }, "node": { "version": "v24.10.0", "env": "development" }, "packageManager": { "name": "pnpm", "version": "10.19.0" }, "frameworks": [ { "name": "next", "version": "16.0.3" }, { "name": "react", "version": "19.2.0" } ], "databases": null, "betterAuth": { "version": "^1.4.5", "config": null } } back-end: { "system": { "platform": "win32", "arch": "x64", "version": "Windows 11 Pro", "release": "10.0.26200", "cpuCount": 6, "cpuModel": "Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz", "totalMemory": "23.82 GB", "freeMemory": "6.74 GB" }, "node": { "version": "v24.10.0", "env": "development" }, "packageManager": { "name": "bun", "version": "1.3.0" }, "frameworks": null, "databases": [ { "name": "@prisma/client", "version": "^6.17.1" } ], "betterAuth": { "version": "^1.4.5", "config": { "emailAndPassword": { "enabled": true }, "trustedOrigins": [ "http://localhost:3010" ], "socialProviders": { "microsoft": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]" } }, "hooks": {}, "plugins": [ { "name": "admin", "config": { "id": "admin", "hooks": { "after": [ {} ] }, "endpoints": {}, "$ERROR_CODES": { "FAILED_TO_CREATE_USER": "Failed to create user", "USER_ALREADY_EXISTS": "User already exists.", "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.", "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself", "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role", "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions", "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users", "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users", "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions", "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users", "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]", "BANNED_USER": "You have been banned from this application", "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user", "NO_DATA_TO_UPDATE": "No data to update", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users", "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself", "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": "You are not allowed to set a non-existent role value" }, "schema": { "user": { "fields": { "role": { "type": "string", "required": false, "input": false }, "banned": { "type": "boolean", "defaultValue": false, "required": false, "input": false }, "banReason": { "type": "string", "required": false, "input": false }, "banExpires": { "type": "date", "required": false, "input": false } } }, "session": { "fields": { "impersonatedBy": { "type": "string", "required": false } } } }, "options": { "ac": { "statements": { "client": [ "list", "create", "update", "delete" ], "component": [ "list", "create", "update", "delete" ], "report": [ "list", "create", "update", "delete" ], "recomendations": [ "list", "create", "update", "delete" ], "user": [ "create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update" ], "session": [ "list", "revoke", "delete" ] } }, "defaultRole": "viewer", "roles": { "admin": { "statements": { "client": [ "list", "create", "update", "delete" ], "component": [ "list", "create", "update", "delete" ], "report": [ "list", "create", "update", "delete" ], "recomendations": [ "list", "create", "update", "delete" ], "user": [ "create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update" ], "session": [ "list", "revoke", "delete" ] } }, "viewer": { "statements": { "user": [ "update" ], "client": [ "list" ], "component": [ "list" ], "report": [ "list" ], "recomendations": [ "list" ] } } } } } }, { "name": "organization", "config": { "id": "organization", "endpoints": {}, "schema": { "organization": { "fields": { "name": { "type": "string", "required": true, "sortable": true }, "slug": { "type": "string", "required": true, "unique": true, "sortable": true }, "logo": { "type": "string", "required": false }, "createdAt": { "type": "date", "required": true }, "metadata": { "type": "string", "required": false } } }, "member": { "fields": { "organizationId": { "type": "string", "required": true, "references": { "model": "organization", "field": "id" }, "index": true }, "userId": { "type": "string", "required": true, "references": { "model": "user", "field": "id" }, "index": true }, "role": { "type": "string", "required": true, "sortable": true, "defaultValue": "member" }, "createdAt": { "type": "date", "required": true } } }, "invitation": { "fields": { "organizationId": { "type": "string", "required": true, "references": { "model": "organization", "field": "id" }, "index": true }, "email": { "type": "string", "required": true, "sortable": true, "index": true }, "role": { "type": "string", "required": false, "sortable": true }, "status": { "type": "string", "required": true, "sortable": true, "defaultValue": "pending" }, "expiresAt": { "type": "date", "required": true }, "createdAt": { "type": "date", "required": true }, "inviterId": { "type": "string", "references": { "model": "user", "field": "id" }, "required": true } } }, "session": { "fields": { "activeOrganizationId": { "type": "string", "required": false } } } }, "$Infer": { "Organization": {}, "Invitation": {}, "Member": {}, "Team": {}, "TeamMember": {}, "ActiveOrganization": {} }, "$ERROR_CODES": { "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION": "You are not allowed to create a new organization", "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS": "You have reached the maximum number of organizations", "ORGANIZATION_ALREADY_EXISTS": "Organization already exists", "ORGANIZATION_SLUG_ALREADY_TAKEN": "Organization slug already taken", "ORGANIZATION_NOT_FOUND": "Organization not found", "USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "User is not a member of the organization", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION": "You are not allowed to update this organization", "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION": "You are not allowed to delete this organization", "NO_ACTIVE_ORGANIZATION": "No active organization", "USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION": "User is already a member of this organization", "MEMBER_NOT_FOUND": "Member not found", "ROLE_NOT_FOUND": "Role not found", "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM": "You are not allowed to create a new team", "TEAM_ALREADY_EXISTS": "Team already exists", "TEAM_NOT_FOUND": "Team not found", "YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER": "You cannot leave the organization as the only owner", "YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER": "You cannot leave the organization without an owner", "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER": "You are not allowed to delete this member", "YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION": "You are not allowed to invite users to this organization", "USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION": "User is already invited to this organization", "INVITATION_NOT_FOUND": "Invitation not found", "YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION": "You are not the recipient of the invitation", "EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION": "Email verification required before accepting or rejecting invitation", "YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION": "You are not allowed to cancel this invitation", "INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION": "Inviter is no longer a member of the organization", "YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE": "You are not allowed to invite a user with this role", "FAILED_TO_RETRIEVE_INVITATION": "Failed to retrieve invitation", "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS": "You have reached the maximum number of teams", "UNABLE_TO_REMOVE_LAST_TEAM": "Unable to remove last team", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER": "You are not allowed to update this member", "ORGANIZATION_MEMBERSHIP_LIMIT_REACHED": "Organization membership limit reached", "YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to create teams in this organization", "YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to delete teams in this organization", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM": "You are not allowed to update this team", "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM": "You are not allowed to delete this team", "INVITATION_LIMIT_REACHED": "Invitation limit reached", "TEAM_MEMBER_LIMIT_REACHED": "Team member limit reached", "USER_IS_NOT_A_MEMBER_OF_THE_TEAM": "User is not a member of the team", "YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM": "You are not allowed to list the members of this team", "YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM": "You do not have an active team", "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER": "You are not allowed to create a new member", "YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER": "You are not allowed to remove a team member", "YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION": "You are not allowed to access this organization as an owner", "YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION": "You are not a member of this organization", "MISSING_AC_INSTANCE": "Dynamic Access Control requires a pre-defined ac instance on the server auth plugin. Read server logs for more information", "YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE": "You must be in an organization to create a role", "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE": "You are not allowed to create a role", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE": "You are not allowed to update a role", "YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE": "You are not allowed to delete a role", "YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE": "You are not allowed to read a role", "YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE": "You are not allowed to list a role", "YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE": "You are not allowed to get a role", "TOO_MANY_ROLES": "This organization has too many roles", "INVALID_RESOURCE": "The provided permission includes an invalid resource", "ROLE_NAME_IS_ALREADY_TAKEN": "That role name is already taken", "CANNOT_DELETE_A_PRE_DEFINED_ROLE": "Cannot delete a pre-defined role" } } } ] } } } ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) ```typescript import { APIError, betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { admin as adminPlugin, createAuthMiddleware, organization } from "better-auth/plugins"; import prisma from "./db"; import { ac, admin, viewer } from "./permissions"; export const auth = betterAuth({ emailAndPassword: { enabled: true }, trustedOrigins: ["http://localhost:3010"], database: prismaAdapter(prisma, { provider: "sqlserver" }), socialProviders: { microsoft: { clientId: process.env.MICROSOFT_CLIENT_ID || "", clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "" }, }, hooks: { before: createAuthMiddleware(async (ctx) => { console.info("Hit before hook on path: ", ctx.path) if (!["/sign-in/email", "/sign-in/social"].includes(ctx.path)) { return; } console.log(ctx.body) // const allowed = ["david.fernandes@terabyte.com.br"] const allowed = ["admin@email.com.br"] const isAllowed = allowed.includes(ctx.body?.email) console.log("isAllowed:", isAllowed) if (!allowed.includes(ctx.body?.email)) { throw new APIError("BAD_REQUEST", { message: "Email not allowed", }); } }) }, plugins: [ adminPlugin({ ac, defaultRole: "viewer", roles: { admin, viewer } }), organization(), ], }); ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-13 06:45:19 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Dec 4, 2025):

Hi @david-sfernandes! I'm Dosu and I’m helping the better-auth team.

This is a known limitation in Better Auth: during social provider sign-in (like Microsoft), the user's email is not available in the before hook because the OAuth flow only retrieves the user's email after the provider completes authentication and returns user info. The before hook only sees the initial request payload, which doesn't include provider-derived data like the user's email. In contrast, email/password sign-in includes the email in the initial payload, so it's available in the before hook see code reference.

The recommended way to enforce email-based restrictions for social providers is to use an after hook, where the user session and user data (including email) are available. For example, you can check ctx.context.newSession.user.email in an after hook and throw an error or clear the session if the email is not allowed example.

If you need to restrict sign-up or sign-in at the database level, you can also use a "before create user" database hook to abort user creation if the email is not allowed see discussion.

If you want to map additional fields from the provider (like custom claims), use the mapProfileToUser option in your provider config. This makes those fields available on the user object in hooks and middleware see discussion.

There is no workaround to access the user's email in the before hook for social sign-in due to the OAuth flow design and when user info becomes available. All email-based restrictions for social providers should be enforced in after hooks or user provisioning/database hooks.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3612435138 --> @dosubot[bot] commented on GitHub (Dec 4, 2025): <!-- Greeting --> Hi @david-sfernandes! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known limitation in Better Auth: during social provider sign-in (like Microsoft), the user's email is not available in the `before` hook because the OAuth flow only retrieves the user's email after the provider completes authentication and returns user info. The `before` hook only sees the initial request payload, which doesn't include provider-derived data like the user's email. In contrast, email/password sign-in includes the email in the initial payload, so it's available in the `before` hook [see code reference](https://github.com/better-auth/better-auth/blob/85409f80848f1f96b9634c320a5014b334601d88/packages/better-auth/src/api/routes/sign-in.ts#L231-L557). The recommended way to enforce email-based restrictions for social providers is to use an `after` hook, where the user session and user data (including email) are available. For example, you can check `ctx.context.newSession.user.email` in an `after` hook and throw an error or clear the session if the email is not allowed [example](https://github.com/better-auth/better-auth/issues/4972). If you need to restrict sign-up or sign-in at the database level, you can also use a "before create user" database hook to abort user creation if the email is not allowed [see discussion](https://github.com/better-auth/better-auth/issues/5724). If you want to map additional fields from the provider (like custom claims), use the `mapProfileToUser` option in your provider config. This makes those fields available on the user object in hooks and middleware [see discussion](https://github.com/better-auth/better-auth/discussions/5677). There is no workaround to access the user's email in the `before` hook for social sign-in due to the OAuth flow design and when user info becomes available. All email-based restrictions for social providers should be enforced in `after` hooks or user provisioning/database hooks. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/fc31425e-75c8-43b7-bd18-386040ffe869?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/fc31425e-75c8-43b7-bd18-386040ffe869?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/fc31425e-75c8-43b7-bd18-386040ffe869?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/fc31425e-75c8-43b7-bd18-386040ffe869?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/fc31425e-75c8-43b7-bd18-386040ffe869?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/fc31425e-75c8-43b7-bd18-386040ffe869?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/fc31425e-75c8-43b7-bd18-386040ffe869?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20known%20limitation%20in%20Better%20Auth%3A%20during%20social%20provider%20sign-in%20%28like%20Microsoft%29%2C%20the%20user%27s%20email%20is%20not%20available%20in%20the%20%60before%60%20hook%20because%20the%20OAuth%20flow%20only%20retrieves%20the%20user%27s%20email%20after%20the%20provider%20completes%20authentication%20and%20returns%20user%20info.%20The%20%60before%60%20hook%20only%20sees%20the%20initial%20request%20payload%2C%20which%20doesn%27t%20include%20provider-derived%20data%20like%20the%20user%27s%20email.%20In%20contrast%2C%20email/password%20sign-in%20includes%20the%20email%20in%20the%20initial%20payload%2C%20so%20it%27s%20available%20in%20the%20%60before%60%20hook%C2%A0%5Bsee%20code%20reference%5D%28https%3A//github.com/better-auth/better-auth/blob/85409f80848f1f96b9634c320a5014b334601d88/packages/better-auth/src/api/routes/sign-in.ts%23L231-L557%29.%0A%0AThe%20recommended%20way%20to%20enforce%20email-based%20restrictions%20for%20social%20providers%20is%20to%20use%20an%20%60after%60%20hook%2C%20where%20the%20user%20session%20and%20user%20data%20%28including%20email%29%20are%20available.%20For%20example%2C%20you%20can%20check%20%60ctx.context.newSession.user.email%60%20in%20an%20%60after%60%20hook%20and%20throw%20an%20error%20or%20clear%20the%20session%20if%20the%20email%20is%20not%20allowed%C2%A0%5Bexample%5D%28https%3A//github.com/better-auth/better-auth/issues/4972%29.%0A%0AIf%20you%20need%20to%20restrict%20sign-up%20or%20sign-in%20at%20the%20database%20level%2C%20you%20can%20also%20use%20a%20%22before%20create%20user%22%20database%20hook%20to%20abort%20user%20creation%20if%20the%20email%20is%20not%20allowed%C2%A0%5Bsee%20discussion%5D%28https%3A//github.com/better-auth/better-auth/issues/5724%29.%0A%0AIf%20you%20want%20to%20map%20additional%20fields%20from%20the%20provider%20%28like%20custom%20claims%29%2C%20use%20the%20%60mapProfileToUser%60%20option%20in%20your%20provider%20config.%20This%20makes%20those%20fields%20available%20on%20the%20user%20object%20in%20hooks%20and%20middleware%C2%A0%5Bsee%20discussion%5D%28https%3A//github.com/better-auth/better-auth/discussions/5677%29.%0A%0AThere%20is%20no%20workaround%20to%20access%20the%20user%27s%20email%20in%20the%20%60before%60%20hook%20for%20social%20sign-in%20due%20to%20the%20OAuth%20flow%20design%20and%20when%20user%20info%20becomes%20available.%20All%20email-based%20restrictions%20for%20social%20providers%20should%20be%20enforced%20in%20%60after%60%20hooks%20or%20user%20provisioning/database%20hooks.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/6527)
Author
Owner

@david-sfernandes commented on GitHub (Dec 4, 2025):

I tested the suggestion of using the after hook, but I still couldn’t access the user’s email there — at least not during the sign-in route. I noticed that there is a callback route where the email becomes available, but I wasn’t able to properly enforce access restrictions at that stage.

Right now I’m experimenting with the databaseHook during user creation. It works, but when an error occurs the flow redirects the user to an internal error page from the auth server itself. So I’ll need to figure out how to customize or handle that page so it fits better with my application.

One additional comment: I believe the documentation for the hooks (specifically the one I referenced earlier) could be updated to avoid this kind of confusion, since the current example suggests that email filtering works the same for both email/password and social provider flows, which isn’t the case.

<!-- gh-comment-id:3612758250 --> @david-sfernandes commented on GitHub (Dec 4, 2025): I tested the suggestion of using the after hook, but I still couldn’t access the user’s email there — at least not during the sign-in route. I noticed that there is a callback route where the email becomes available, but I wasn’t able to properly enforce access restrictions at that stage. Right now I’m experimenting with the databaseHook during user creation. It works, but when an error occurs the flow redirects the user to an internal error page from the auth server itself. So I’ll need to figure out how to customize or handle that page so it fits better with my application. One additional comment: I believe the documentation for the hooks (specifically the one I referenced earlier) could be updated to avoid this kind of confusion, since the current example suggests that email filtering works the same for both email/password and social provider flows, which isn’t the case.
Author
Owner

@ping-maxwell commented on GitHub (Jan 8, 2026):

The flow of oauth looks roughly like this:
/sign-in/social -> redirected to oauth provider login page -> /callback/<provider_id>

You're using a before hook looking at the /sign-in/social stage which does not have any context of the user's email, only after the user has reached the callback endpoint which we would then know the email.

In your case, specifically for social logins, I recommend using a database hook to check if the user's email is allowed before creating a user.

https://www.better-auth.com/docs/concepts/database#database-hooks

<!-- gh-comment-id:3722847480 --> @ping-maxwell commented on GitHub (Jan 8, 2026): The flow of oauth looks roughly like this: `/sign-in/social` -> redirected to oauth provider login page -> `/callback/<provider_id>` You're using a before hook looking at the `/sign-in/social` stage which does not have any context of the user's email, only after the user has reached the callback endpoint which we would then know the email. In your case, specifically for social logins, I recommend using a database hook to check if the user's email is allowed before creating a user. https://www.better-auth.com/docs/concepts/database#database-hooks
Author
Owner

@ping-maxwell commented on GitHub (Jan 8, 2026):

Right now I’m experimenting with the databaseHook during user creation. It works, but when an error occurs the flow redirects the user to an internal error page from the auth server itself. So I’ll need to figure out how to customize or handle that page so it fits better with my application.

Try something like this to redirect to a custom error page:

const headers = new Headers();
headers.set(
	"location",
	"http://my-domain.com/my-error-page?error=invalid_email_origin"
);
throw new APIError("FOUND", undefined, headers);
<!-- gh-comment-id:3722854967 --> @ping-maxwell commented on GitHub (Jan 8, 2026): > Right now I’m experimenting with the databaseHook during user creation. It works, but when an error occurs the flow redirects the user to an internal error page from the auth server itself. So I’ll need to figure out how to customize or handle that page so it fits better with my application. Try something like this to redirect to a custom error page: ```ts const headers = new Headers(); headers.set( "location", "http://my-domain.com/my-error-page?error=invalid_email_origin" ); throw new APIError("FOUND", undefined, headers); ```
Author
Owner

@david-sfernandes commented on GitHub (Jan 14, 2026):

Thanks @ping-maxwell

<!-- gh-comment-id:3747159426 --> @david-sfernandes commented on GitHub (Jan 14, 2026): Thanks @ping-maxwell
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10542