Comprehensive guide to Neev's authentication system, covering all supported methods and flows.
Neev supports multiple authentication methods:
| Method | Description | Configuration |
|---|---|---|
| Password | Traditional email/password login | Always available |
| Magic Link | Passwordless via email | Always available |
| Passkey/WebAuthn | Biometric or hardware key | Always available |
| OAuth | Social login (Google, GitHub, etc.) | oauth config |
| Tenant SSO | Enterprise SSO (Entra ID, Okta) | Per-tenant/per-team auth settings in DB |
- User submits registration form with name, email, password
- System validates password against rules
- User account is created with email (unverified)
- If teams enabled, personal team is created (skipped for invitation-link signups and verified federated domains)
- Verification email is sent
- User is logged in and redirected
API Example:
curl -X POST https://yourapp.com/neev/register \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"password": "SecurePass123!",
"password_confirmation": "SecurePass123!"
}'Response:
{
"auth_state": "authenticated",
"token": "1|abc123def456...",
"expires_in": 1440,
"mfa_options": null,
"email_verified": false
}- User enters email/username
- System checks if user exists
- User enters password
- System validates password
- If MFA enabled, redirect to MFA verification
- User is logged in and redirected
API Example:
curl -X POST https://yourapp.com/neev/login \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "SecurePass123!"
}'Response (no MFA):
{
"auth_state": "authenticated",
"token": "1|abc123def456...",
"expires_in": 1440,
"mfa_options": null,
"email_verified": true
}Response (with MFA):
{
"auth_state": "mfa_required",
"token": "jwt_mfa_token...",
"expires_in": 30,
"mfa_options": [
"authenticator",
"email"
],
"email_verified": true
}When MFA is required, the returned token is a short-lived JWT (type mfa, expiry set by mfa_jwt_expiry_minutes, default 30). Send it as a Bearer token to the verification endpoint (protected by the neev:login middleware group, which authenticates the MFA JWT):
curl -X POST https://yourapp.com/neev/mfa/otp/verify \
-H "Authorization: Bearer jwt_mfa_token..." \
-H "Content-Type: application/json" \
-d '{
"auth_method": "authenticator",
"otp": "123456"
}'On success, the endpoint returns auth_state: "authenticated" with a regular access token in the {id}|{plaintext} format. expires_in is returned in minutes. Accepted auth_method values are the user's configured MFA methods (authenticator, email) or recovery for recovery codes.
Default password rules (configurable in config/neev.php):
- Minimum 8 characters
- Maximum 72 characters (bcrypt limit)
- Must contain letters
- Must contain uppercase and lowercase
- Must contain numbers
- Must contain special characters
Prevents reusing recent passwords:
// config/neev.php
PasswordHistory::notReused(5) // Cannot reuse last 5 passwordsPrevents using personal information in passwords:
// config/neev.php
PasswordUserData::notContain(['name', 'email'])Configure password aging:
// config/neev.php
'password_expiry_days' => 90, // Days before password expires. 0 = disabled.Enforcement is opt-in: apply the neev:password-not-expired middleware alias (EnsurePasswordNotExpired) to routes that should reject users with expired passwords. Helpers are available on the user: passwordExpiresAt(), isPasswordExpired(), isPasswordExpiringSoon().
Passwordless login via secure email links. Always available — no config toggle.
- User enters email on login page
- Clicks "Send Login Link"
- Receives email with secure link
- Clicks link to authenticate
- Automatically logged in
Request Link:
curl -X POST https://yourapp.com/neev/sendLoginLink \
-H "Content-Type: application/json" \
-d '{"email": "john@example.com"}'Use Link:
curl -X GET "https://yourapp.com/neev/loginUsingLink?id=1&signature=abc123&expires=1234567890"Configure in config/neev.php:
'url_expiry_time' => 60, // MinutesBiometric authentication using fingerprints, face recognition, or hardware security keys.
Configured in config/neev.php:
// Relying Party ID — the domain passkeys are bound to. Defaults to the
// host parsed from APP_URL (e.g. "example.com").
'relying_party_id' => parse_url(config('app.url'), PHP_URL_HOST),
// Origins permitted to complete WebAuthn ceremonies. Add every origin
// (including subdomains/alternate hosts) the browser may report. The
// origin must be reachable on the relying party ID above.
'allowed_origins' => [
config('app.url'),
],For multi-origin setups (e.g. apex domain plus subdomains, or staging plus production), list every allowed origin explicitly:
'allowed_origins' => [
'https://app.example.com',
'https://admin.example.com',
],- User authenticates with password
- Goes to Security settings
- Clicks "Add Passkey"
- Browser prompts for biometric/key
- Passkey is registered and stored
- User enters email
- Sees "Login with Passkey" option
- Browser prompts for biometric/key
- Authenticated immediately
Generate Registration Options:
curl -X GET https://yourapp.com/neev/passkeys/register/options \
-H "Authorization: Bearer {token}"Register Passkey:
curl -X POST https://yourapp.com/neev/passkeys/register \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"attestation": "{...webauthn_response...}",
"name": "MacBook Pro"
}'Login with Passkey:
# Get options
curl -X POST https://yourapp.com/neev/passkeys/login/options \
-d '{"email": "john@example.com"}'
# Authenticate
curl -X POST https://yourapp.com/neev/passkeys/login \
-d '{
"email": "john@example.com",
"assertion": "{...webauthn_assertion...}"
}'// Register Passkey
async function registerPasskey() {
// Get options from server
const optionsRes = await fetch('/neev/passkeys/register/options', {
headers: { 'Authorization': `Bearer ${token}` }
});
const options = await optionsRes.json();
// Decode challenge
options.challenge = base64UrlDecode(options.challenge);
options.user.id = base64UrlDecode(options.user.id);
// Create credential
const credential = await navigator.credentials.create({
publicKey: options
});
// Send to server
await fetch('/neev/passkeys/register', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
attestation: JSON.stringify({
id: credential.id,
rawId: base64UrlEncode(credential.rawId),
type: credential.type,
response: {
clientDataJSON: base64UrlEncode(credential.response.clientDataJSON),
attestationObject: base64UrlEncode(credential.response.attestationObject)
},
challenge: options.challenge
}),
name: 'My Device'
})
});
}Authenticate via third-party providers.
- GitHub
- Microsoft
- Apple
- Enable providers in
config/neev.php:
'oauth' => [
'google',
'github',
],- Configure credentials in
config/services.php:
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('GOOGLE_REDIRECT_URI'),
],- Set environment variables:
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI="${APP_URL}/neev/oauth/google/callback"Warning — OAuth is a complete authentication path that skips the MFA gate.
Password login checks the user's enrolled MFA methods and, when any are active, withholds the session/token until a second factor is verified (
mfa_requiredstate with a temporary JWT on the API; redirect to the OTP page on the web). The OAuth callback does not perform this check: once the provider returns a verified email that matches an account, the user is logged in (web) or issued a full access token (API) immediately — even if that user has TOTP or email MFA enabled. A compromised Google/GitHub/Microsoft/Apple account therefore grants access without the second factor.Password policies are also inapplicable to OAuth-created accounts: they are created without a password, so complexity rules, password history, and password expiry never apply to them, and their email is marked verified automatically (it was verified by the provider).
What this means for enterprise policy: if your compliance posture requires MFA for all users (or organization-controlled credentials), enabling app-wide OAuth providers undermines that guarantee — every enabled provider is an alternate front door that skips your MFA and password controls.
Mitigations:
- Limit or empty the
oauthproviders list inconfig/neev.php. Providers not in the list 404 on both redirect and callback, so this fully disables the path. - Use tenant SSO instead for organizations that need enforced IdP login. Tenant/team SSO is database-configured per organization, and the
neev:ensure-ssomiddleware rejects (API) or redirects (web) any authenticated session that was not established via SSO — including sessions created through app-wide OAuth. See Multi-Tenancy → Enterprise SSO. - Add an application-level step-up check after login if MFA must be universal regardless of login method (Neev does not provide this out of the box).
- User clicks "Login with Google"
- Redirected to Google's consent page
- User authorizes the application
- Redirected back with auth code
- System exchanges code for user info
- User is created or matched
- Logged in and redirected (MFA is skipped)
- Redirect:
GET /neev/oauth/{provider} - Callback:
GET /neev/oauth/{provider}/callback
The /neev prefix is configurable via route_prefix in config/neev.php (env NEEV_ROUTE_PREFIX). Changing it also changes the callback URLs registered with your OAuth providers.
Per-tenant identity provider configuration. There is no config toggle — SSO settings live in the database (tenant_auth_settings for tenants, team_auth_settings for teams) and default to password auth when no row exists.
- entra - Microsoft Entra ID (Azure AD)
- google - Google Workspace
- okta - Okta Identity
Configure SSO for a tenant (or team) via CLI:
php artisan neev:auth:configure --tenant=acme --method=sso \
--sso-provider=entra --sso-client-id=... --sso-client-secret=... --sso-tenant-id=...Or in code:
$tenant->authSettings()->create([
'auth_method' => 'sso',
'sso_provider' => 'entra',
'sso_client_id' => 'your-client-id',
'sso_client_secret' => 'your-client-secret', // encrypted automatically via cast
'sso_tenant_id' => 'your-azure-tenant-id',
'auto_provision' => true,
'auto_provision_role' => 'member',
]);- User accesses tenant URL (e.g.,
acme.yourapp.com) - System detects tenant requires SSO
- User redirected to identity provider
- User authenticates with corporate credentials
- Redirected back with auth token
- User is matched or auto-provisioned
- Logged into tenant
GET /neev/tenant/authReturns tenant auth configuration:
{
"auth_method": "sso",
"sso_enabled": true,
"sso_provider": "entra",
"sso_redirect_url": "https://acme.yourapp.com/neev/sso/redirect"
}Verification emails are always sent on registration. Enforcement is opt-in: apply the neev:verified-email middleware alias (EnsureEmailIsVerified) to routes that should require a verified email — there is no config toggle.
Route::middleware(['neev:api', 'neev:verified-email'])->group(function () {
// Routes that require a verified email
});- User registers or changes email
- Verification email is sent automatically
- User clicks verification link
- Email is marked as verified
- User can access routes protected by
neev:verified-email
Web:
GET /email/sendAPI:
POST /neev/email/send
Authorization: Bearer {token}PUT /email/change
Content-Type: application/x-www-form-urlencoded
email=newemail@example.comGET /neev/sessions
Authorization: Bearer {token}Returns:
{
"data": [
{
"id": 1,
"last_used_at": "2024-01-15T10:00:00Z",
"attempt": {
"ip_address": "192.168.1.1",
"browser": "Chrome",
"platform": "macOS",
"location": "San Francisco, CA"
}
}
]
}POST /neev/logout
Authorization: Bearer {token}Deletes all of the user's login tokens except the current one:
POST /neev/logoutAll
Authorization: Bearer {token}POST /account/logoutSessions
Content-Type: application/x-www-form-urlencoded
session_id=abc123For each login attempt, Neev records:
| Field | Description |
|---|---|
method |
Login method used (password, passkey, sso, etc.) |
multi_factor_method |
MFA method used (if any) |
ip_address |
User's IP address |
platform |
Operating system |
browser |
Browser name |
device |
Device type |
location |
City, country (via GeoIP) |
is_success |
Whether login succeeded |
is_suspicious |
Flagged as suspicious |
| Constant | Value | Description |
|---|---|---|
LoginAttempt::Password |
password |
Password authentication |
LoginAttempt::Passkey |
passkey |
WebAuthn/passkey |
LoginAttempt::MagicAuth |
magic auth |
Magic link |
LoginAttempt::OAuth |
oauth |
Social login |
LoginAttempt::SSO |
sso |
Tenant SSO |
GET /neev/loginAttempts
Authorization: Bearer {token}// config/neev.php
'login_throttle' => [
'delay_after' => 3, // Failed attempts before progressive delay kicks in
'max_delay_seconds' => 300, // Maximum delay (exponential backoff caps here)
],Progressive delay instead of a hard lockout:
- Attempts 1-2: Normal login speed
- Attempt 3 onwards: Exponential backoff between attempts (
2^(attempts - delay_after)seconds), capped atmax_delay_seconds(default 5 minutes)
Delays are keyed per email + IP. A successful login clears the counter.
// config/neev.php
'log_failed_logins' => false, // Failed attempts tracked in cache only
'log_failed_logins' => true, // Also record failed attempts in the databaseFired when a user successfully logs in.
use Ssntpl\Neev\Events\LoggedIn;
class LogSuccessfulLogin
{
public function handle(LoggedIn $event)
{
$user = $event->user;
// Log activity, send notification, etc.
}
}Fired when a user logs out.
use Ssntpl\Neev\Events\LoggedOut;
class LogSuccessfulLogout
{
public function handle(LoggedOut $event)
{
$user = $event->user;
// Cleanup, audit logging, etc.
}
}- Enable MFA for all users, especially administrators
- Use HTTPS in production
- Apply
neev:password-not-expiredmiddleware if password aging is a compliance requirement - Apply
neev:verified-emailmiddleware to prevent unverified accounts from accessing sensitive routes - Monitor login attempts for suspicious activity
- Use session database driver for logout-all-devices functionality
- Keep GeoIP database updated for accurate location tracking
- Be aware that OAuth bypasses MFA and password policies — see the OAuth security warning for mitigations