Skip to content

Authentication

Metricis supports multiple authentication methods for different use cases.

Portal Authentication (JWT)

Researchers and administrators use JWT-based authentication.

Login

Request:

POST /api/auth/login
Content-Type: application/x-www-form-urlencoded

username=admin@metricis.app&password=your-password

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "expires_in": 86400
}

Using the Token

Include token in Authorization header:

GET /api/studies
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Token Expiration

Tokens expire after 24 hours by default. Configure in server settings:

ACCESS_TOKEN_EXPIRE_MINUTES=1440

Refresh Token (Coming Soon)

POST /api/auth/refresh
Authorization: Bearer <refresh-token>

Patients and caregivers use passwordless magic link authentication.

Request:

POST /api/auth/magic-link
Content-Type: application/json

{
  "email": "participant@example.com"
}

Response:

{
  "message": "Magic link sent to participant@example.com"
}

Email contains link:

https://app.metricis.app/auth?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

When user clicks link, frontend verifies token:

const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');

// Store token
localStorage.setItem('auth_token', token);

// Fetch user info
const user = await api.getCurrentUser();

API Key Authentication (Coming Soon)

For programmatic access and integrations:

GET /api/participants
X-API-Key: sk_live_abc123def456

Session Authentication

Assessment sessions use session IDs for data submission:

Start Session:

POST /api/session/start
Content-Type: application/json

{
  "participant_id": "P001",
  "battery_id": "battery_1"
}

Response:

{
  "session_id": "sess_abc123"
}

Use Session ID:

POST /api/submit
X-Session-ID: sess_abc123
Content-Type: application/json

{
  "session_id": "sess_abc123",
  "task_summaries": {...},
  "trials": [...]
}

Security Best Practices

Storing Tokens

Browser:

// Store in localStorage (acceptable for public clients)
localStorage.setItem('auth_token', token);

// Or use sessionStorage (cleared on tab close)
sessionStorage.setItem('auth_token', token);

Server-side:

# Store in secure, httpOnly cookies
response.set_cookie(
    key="auth_token",
    value=token,
    httponly=True,
    secure=True,  # HTTPS only
    samesite="lax"
)

Token Validation

Server validates JWT tokens on each request:

from jose import JWTError, jwt
from app.config import get_settings

settings = get_settings()

def verify_token(token: str) -> dict:
    try:
        payload = jwt.decode(
            token,
            settings.JWT_SECRET_KEY.get_secret_value(),
            algorithms=[settings.JWT_ALGORITHM]
        )
        return payload
    except JWTError:
        raise HTTPException(status_code=401, detail="Invalid token")

Password Security

Passwords are hashed using bcrypt:

from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# Hash password
hashed = pwd_context.hash("user-password")

# Verify password
is_valid = pwd_context.verify("user-password", hashed)

Role-Based Access Control

Users have roles that determine permissions:

class UserRole(str, Enum):
    ADMIN = "admin"           # Full system access
    RESEARCHER = "researcher" # Study management
    COORDINATOR = "coordinator" # Participant management
    VIEWER = "viewer"         # Read-only access

Permission checks:

from fastapi import Depends, HTTPException
from app.utils.auth import get_current_user, require_role

@router.post("/studies")
async def create_study(
    study: StudyCreate,
    current_user: User = Depends(require_role(UserRole.ADMIN))
):
    # Only admins can create studies
    ...

OAuth2 Integration (Coming Soon)

Support for institutional SSO:

  • SAML 2.0
  • OpenID Connect
  • Azure AD / Entra ID
  • Google Workspace

Two-Factor Authentication (Coming Soon)

Optional 2FA for enhanced security:

  • TOTP (Time-based One-Time Password)
  • SMS verification
  • Email verification

Example: Full Auth Flow

Portal Login

// Login
const response = await fetch('http://localhost:8000/api/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    username: 'admin@metricis.app',
    password: 'admin123',
  }),
});

const { access_token } = await response.json();
localStorage.setItem('auth_token', access_token);

// Use token for requests
const studies = await fetch('http://localhost:8000/api/studies', {
  headers: { 'Authorization': `Bearer ${access_token}` },
});
// Request magic link
await fetch('http://localhost:8000/api/auth/magic-link', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'participant@example.com' }),
});

// User clicks link in email: https://app.metricis.app/auth?token=...

// Frontend extracts and stores token
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
localStorage.setItem('auth_token', token);

// Redirect to home page
window.location.href = '/';

Troubleshooting

Token Expired

Error:

{
  "error": {
    "code": "TOKEN_EXPIRED",
    "message": "JWT token has expired"
  }
}

Solution: Re-authenticate to get new token

Invalid Token

Error:

{
  "error": {
    "code": "INVALID_TOKEN",
    "message": "Invalid authentication token"
  }
}

Solution: Verify token format and check for corruption

Rate Limit Exceeded

Error:

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many authentication attempts"
  }
}

Solution: Wait 1 minute before retrying

Next Steps