Skip to content

Authentication

Table of Contents

Overview

Phenix authentication in this application skeleton is based on personal access tokens, not server-side sessions.

The login flow has two steps:

  1. The client submits email and password to request a login OTP.
  2. The client submits the OTP to receive an API token.

Protected endpoints expect the token in the Authorization header:

http
Authorization: Bearer <access_token>

The plain token is only returned when it is created or refreshed. The database stores only the SHA-256 hash of that token.

How the flow works

The authentication journey is built around OTP-backed actions and bearer tokens:

  • POST /register creates a user and sends an email verification OTP.
  • POST /verify-email verifies the email address with email + otp.
  • POST /login validates credentials and sends a login OTP.
  • POST /login/authorize exchanges email + otp for a bearer token.
  • Authenticated requests use Authorization: Bearer <access_token>.
  • POST /token/refresh rotates the current token and expires the previous one.
  • POST /logout revokes only the current token.
  • POST /reset-password changes the password and revokes all existing tokens.

Guest-only auth routes reject requests that are already authenticated with a valid bearer token. Requests with missing, malformed, expired, revoked, or invalid tokens are still treated as guest requests, so clients can re-authenticate or complete recovery flows.

Guest-only endpoints

These routes are available only when the request is not already authenticated by a valid bearer token:

  • POST /register
  • POST /verify-email
  • POST /resend-verification-otp
  • POST /forgot-password
  • POST /reset-password
  • POST /login
  • POST /login/authorize
  • POST /register/cancel

Register

Creates a new user and sends an email verification OTP.

http
POST /register
Content-Type: application/json
json
{
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "password": "P@ssw0rd12",
  "password_confirmation": "P@ssw0rd12"
}

Successful response:

json
{
  "id": 1,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "email_verified_at": null,
  "created_at": "2026-03-25 12:00:00",
  "updated_at": null
}

Notes:

  • Registration does not return an API token.
  • The user must verify the email before login is allowed.

Verify email

Completes email verification with the OTP sent during registration.

http
POST /verify-email
Content-Type: application/json
json
{
  "email": "ada@example.com",
  "otp": "123456"
}

Successful response:

json
{
  "message": "Your email has been verified."
}

If the OTP is missing, expired, already used, or belongs to another scope, the endpoint returns 404 with an invalid OTP message.

Resend verification OTP

Sends a new verification OTP to an unverified user.

http
POST /resend-verification-otp
Content-Type: application/json
json
{
  "email": "ada@example.com"
}

Successful response:

json
{
  "message": "A new verification code has been sent."
}

Login

Validates credentials for a verified user and sends a login OTP.

http
POST /login
Content-Type: application/json
json
{
  "email": "ada@example.com",
  "password": "P@ssw0rd12"
}

Successful response:

json
{
  "message": "A login verification code has been sent."
}

Notes:

  • This step does not return a token yet.
  • Unverified users fail validation.
  • Invalid passwords return 401 Unauthorized.

Login authorization

Exchanges a valid login OTP for a bearer token.

http
POST /login/authorize
Content-Type: application/json
json
{
  "email": "ada@example.com",
  "otp": "123456"
}

Successful response:

json
{
  "access_token": "5b2f8b8d..._checksum",
  "expires_at": "2026-03-26 00:00:00",
  "token_type": "Bearer"
}

This is the main token payload contract used by the authentication system:

json
{
  "access_token": "string",
  "expires_at": "YYYY-MM-DD HH:MM:SS",
  "token_type": "Bearer"
}

Forgot password

Requests a reset-password OTP for a verified user.

http
POST /forgot-password
Content-Type: application/json
json
{
  "email": "ada@example.com"
}

Successful response:

json
{
  "message": "If the account exists, a password reset code has been sent."
}

This endpoint intentionally returns the same success response even when:

  • the email does not exist
  • the user is not verified
  • the hourly reset OTP cap has already been reached

Reset password

Resets the password with a valid reset OTP.

http
POST /reset-password
Content-Type: application/json
json
{
  "email": "ada@example.com",
  "otp": "123456",
  "password": "N3wP@ssw0rd1",
  "password_confirmation": "N3wP@ssw0rd1"
}

Successful response:

json
{
  "message": "Your password has been reset successfully."
}

Important behavior:

  • The OTP is marked as used.
  • The password is updated.
  • All existing personal access tokens are revoked.

Cancel registration

Deletes an unverified user and any pending verification OTP records.

http
POST /register/cancel
Content-Type: application/json
json
{
  "email": "ada@example.com"
}

Successful response:

json
{
  "message": "Registration cancelled successfully."
}

Authenticated endpoints

These routes require a valid bearer token:

  • POST /logout
  • GET /tokens
  • POST /token/refresh
  • DELETE /tokens/{id}

Requests without a valid token return 401 Unauthorized.

Call an authenticated endpoint

http
GET /tokens
Authorization: Bearer <access_token>

Logout

Revokes only the token used in the current request.

http
POST /logout
Authorization: Bearer <access_token>

Successful response:

json
{
  "message": "Logged out successfully."
}

List active tokens

Returns the authenticated user's non-expired tokens.

http
GET /tokens
Authorization: Bearer <access_token>

Example response:

json
[
  {
    "id": "8bb7d0ef-236a-4f54-8df3-1b1d5a8b1f52",
    "tokenable_type": "App\\Models\\User",
    "tokenable_id": 1,
    "name": "auth_token",
    "abilities": "[\"*\"]",
    "last_used_at": "2026-03-25 12:30:00",
    "expires_at": "2026-03-26 00:00:00",
    "created_at": "2026-03-25 12:00:00",
    "updated_at": "2026-03-25 12:30:00"
  }
]

Notes:

  • Expired tokens are filtered out.
  • The stored token hash is hidden and is not included in the response.

Refresh token

Creates a new token for the current user and expires the current token immediately.

http
POST /token/refresh
Authorization: Bearer <access_token>

Successful response:

json
{
  "access_token": "a5cd4fe1..._checksum",
  "expires_at": "2026-03-26 00:30:00",
  "token_type": "Bearer"
}

Important behavior:

  • The previous token stops being valid after refresh.
  • Clients should replace the stored token immediately.

Revoke a specific token

Deletes one token by ID, as long as it belongs to the authenticated user.

http
DELETE /tokens/8bb7d0ef-236a-4f54-8df3-1b1d5a8b1f52
Authorization: Bearer <access_token>

Successful response:

json
[]

If the token does not belong to the authenticated user, the endpoint returns 404.

Main authentication flow

Here is the most common end-to-end flow for API clients:

  1. Register a new user with POST /register.
  2. Verify the email with POST /verify-email.
  3. Submit credentials to POST /login.
  4. Submit the login OTP to POST /login/authorize.
  5. Store the returned access_token securely on the client.
  6. Send the token as Authorization: Bearer <access_token> on protected routes.
  7. Rotate the token with POST /token/refresh when needed.
  8. Revoke the current token with POST /logout when the session should end.

Token lifecycle

The token system is implemented with the HasApiTokens trait and the authentication middleware in the framework.

Token creation

When a token is created:

  • a cryptographically random plain-text token is generated
  • a checksum suffix is appended to the token value
  • the SHA-256 hash is stored in personal_access_tokens
  • the plain token is returned once in the HTTP response

By default, tokens are created with the auth_token name in this application.

Token validation

For authenticated routes, the Authenticated middleware:

  • requires an Authorization header that starts with Bearer
  • extracts the plain token
  • hashes it with SHA-256
  • loads the matching non-expired record from personal_access_tokens
  • updates last_used_at
  • attaches the authenticated user to the request

If validation fails, the request returns 401 Unauthorized.

Refresh behavior

Refreshing a token creates a new token and sets the previous token expiration to the current time. After that, clients must use the new token.

Revocation behavior

Revocation behavior differs by endpoint:

  • POST /logout deletes only the current token
  • DELETE /tokens/{id} deletes a selected token that belongs to the current user
  • POST /reset-password deletes all tokens for the user

OTP-backed flows

OTP records are stored separately from personal access tokens and are scoped by purpose.

The application uses these OTP scopes:

  • email verification
  • login authorization
  • password reset

Each OTP is:

  • a 6-digit numeric code
  • stored as a SHA-256 hash
  • valid for a limited time
  • single-use

An OTP becomes invalid when:

  • it does not exist
  • it belongs to another scope
  • it has already been used
  • it has expired

These rules are enforced in the email verification, login authorization, and password reset flows.

Configuration defaults

The default authentication configuration in config/auth.php is:

  • OTP expiration: 10 minutes
  • personal access token expiration: 720 minutes
  • token validation rate limit: 5 attempts per 300 seconds per client IP
  • token prefix: empty by default

The token model used by the app is Phenix\Auth\PersonalAccessToken, and the user model is App\Models\User.

Rate limits and safeguards

The auth flow has several independent protections.

Endpoint-specific request rate limits

The API routes apply request throttling to these endpoints:

  • POST /verify-email: 6 requests per minute
  • POST /resend-verification-otp: 2 requests per minute
  • POST /forgot-password: 2 requests per minute
  • POST /reset-password: 5 requests per minute
  • POST /login: 5 requests per minute
  • POST /login/authorize: 5 requests per minute

OTP issuance caps

The application also caps OTP generation per user and scope:

  • login OTPs: maximum 5 per hour
  • verification OTPs: maximum 5 per hour when resending
  • reset-password OTPs: maximum 5 per hour

Token validation rate limiting

The framework includes token validation attempt tracking by client IP.

When a client repeatedly sends invalid bearer tokens, the token validation middleware can block additional attempts for the configured window. The default limit is 5 failed token validation attempts in 300 seconds.

Framework appendix

Most API consumers only need the HTTP contract above, but the framework pieces below explain how the behavior is implemented.

HasApiTokens

The HasApiTokens trait is responsible for:

  • creating token records
  • returning the plain token to the caller
  • exposing the current token through currentAccessToken()
  • listing a model's tokens through tokens()
  • rotating the current token through refreshToken()

Authenticated middleware

The Authenticated middleware protects routes by:

  • reading the bearer token from the Authorization header
  • validating the token hash and expiration
  • recording token usage
  • binding the authenticated user to the request

TokenRateLimit middleware

The TokenRateLimit middleware guards against repeated invalid bearer token attempts from the same client IP. When the configured limit is exceeded, it returns 429 Too Many Requests.

Console commands

The framework ships with two auth-related console commands:

  • tokens:table generates the migration for the personal_access_tokens table
  • tokens:purge deletes expired personal access tokens