Invitation Lifecycle and Security

Invitation state transitions, token hashing, expiration behavior, and secure operating practices.

AuthOS release 0.8.2 API v1 Latest-only documentation
Updated Jul 15, 2026 Edit this page
On this page

Invitation Lifecycle

Status Flow

pending � accepted (successful acceptance)
pending � rejected (user declined)
pending � cancelled (admin cancelled)
pending � expired (expiration date passed)

Status Descriptions

pending:

  • Initial state when invitation is created
  • User can accept or decline
  • Admin can cancel
  • Automatically expires after 7 days

accepted:

  • User has accepted the invitation
  • Membership has been created
  • Cannot be undone (must remove member instead)

rejected:

  • User has declined the invitation
  • No membership created
  • Cannot be undone (must create new invitation)

cancelled:

  • Admin has cancelled the invitation
  • User can no longer accept
  • Cannot be undone (must create new invitation)

expired:

  • Invitation expiration date has passed
  • User can no longer accept (but can decline)
  • Must create new invitation to re-invite

Security

Token Hashing

Invitation tokens are hashed using SHA256 before storage for security:

Plaintext Token (stored in email): 550e8400-e29b-41d4-a716-446655440000
Hashed Token (stored in DB):        SHA256(plaintext_token)

Security Benefits:

  • Database compromise doesn’t expose usable tokens
  • Tokens cannot be used if database is leaked
  • One-way hashing prevents token recovery

Token Generation:

// Plaintext token (shown to user once)
const token = crypto.randomUUID();

// Hash for storage
const tokenHash = crypto
  .createHash('sha256')
  .update(token)
  .digest('hex');

Token Verification:

// Hash provided token
const providedHash = crypto
  .createHash('sha256')
  .update(providedToken)
  .digest('hex');

// Compare with stored hash
if (providedHash === storedHash) {
  // Valid token
}

Expiration

Default Expiration: 7 days from creation

Expiration Constant:

const INVITATION_EXPIRY_DAYS: i64 = 7;

Expiration Calculation:

let expires_at = Utc::now() + ChronoDuration::days(INVITATION_EXPIRY_DAYS);

Expiration Check:

let expires_at = chrono::DateTime::parse_from_rfc3339(&invitation.expires_at)
    .ok()
    .map(|dt| dt.with_timezone(&Utc))
    .unwrap_or_else(Utc::now);

if expires_at < Utc::now() {
    return Err(AppError::BadRequest("Invitation has expired".to_string()));
}

Best Practices

For Organization Admins:

  • Only invite users with legitimate need for access
  • Assign the minimum required role (principle of least privilege)
  • Review and cancel unused pending invitations regularly
  • Monitor invitation acceptance rates
  • Use descriptive organization names for clarity in emails

For Security:

  • Never share invitation tokens in public channels
  • Tokens should only be sent via secure email
  • Implement rate limiting on invitation endpoints
  • Monitor for suspicious invitation patterns
  • Set up alerts for failed acceptance attempts

For Integration:

  • Always verify invitation tokens before acceptance
  • Handle expired invitations gracefully in UI
  • Provide clear error messages for invalid tokens
  • Log invitation events for audit trails
  • Test email delivery before production use