Skip to content

Latest commit

 

History

History
1377 lines (964 loc) · 20.7 KB

File metadata and controls

1377 lines (964 loc) · 20.7 KB

Auth API

This page documents the public rix/auth API.

Use auth through the unified Rix facade:

#include <rix.hpp>

Then access auth with:

rix.auth

The auth API provides helpers for:

registration
login
password hashing
sessions
tokens
configuration
memory stores
error handling

Package

The auth package is:

rix/auth

For facade usage, install:

vix add rix/rix
vix install

In vix.app:

deps = [
  "rix/rix",
]

For independent package usage, install:

vix add rix/auth
vix install

In vix.app:

deps = [
  "rix/auth",
]

Header

Facade usage:

#include <rix.hpp>

Independent usage:

#include <rix/auth.hpp>

Most application examples should use the facade.

Facade member

The auth facade member is:

rix.auth

Example:

#include <rix.hpp>

int main()
{
  auto auth = rix.auth.memory();

  auto registered = auth.register_user({
      "ada@example.com",
      "correct-password"});

  return registered.ok() ? 0 : 1;
}

Main modules

rix.auth exposes several helper areas:

rix.auth.memory()
rix.auth.password
rix.auth.config
rix.auth.error

Common usage:

auto auth = rix.auth.memory();

auto config = rix.auth.config.development();

auto hashed = rix.auth.password.hash("correct-password");

auto error_text = rix.auth.error.to_string(error);

Create a memory auth service

Use:

auto auth = rix.auth.memory();

This creates an auth service backed by in-memory user and session stores.

Memory auth is useful for:

examples
tests
local development
small demos
temporary applications

Memory auth does not persist users or sessions after the process exits.

Create memory auth with config

auto config = rix.auth.config.development();

config.set_min_password_length(8);
config.set_session_ttl_seconds(60 * 60 * 24 * 7);
config.set_token_ttl_seconds(60 * 15);

auto auth = rix.auth.memory(config);

Use configuration when you want to change password policy, token lifetime, session lifetime, or issuer.

Register a user

Use:

auto registered = auth.register_user({
    "ada@example.com",
    "correct-password"});

register_user returns an explicit result.

Check it before using the user:

if (registered.failed())
{
  rix.debug.eprint(
      "auth error:",
      rix.auth.error.to_string(registered.error()),
      registered.error().message());

  return 1;
}

Then use:

const auto &user = registered.value();

Register request

A register request contains:

email
password

Example:

auto registered = auth.register_user({
    "ada@example.com",
    "correct-password"});

The plain-text password is hashed before storage.

Do not log the plain-text password.

Login

Use:

auto login = auth.login({
    "ada@example.com",
    "correct-password"});

login returns an explicit result.

Check it:

if (login.failed())
{
  rix.debug.eprint(
      "auth error:",
      rix.auth.error.to_string(login.error()),
      login.error().message());

  return 1;
}

Then use:

login.value().user
login.value().session
login.value().token

Login result

A successful login contains:

user
session
token

Example:

rix.debug.print("user:", login.value().user.email());
rix.debug.print("session:", login.value().session.id());
rix.debug.print("issuer:", login.value().token.issuer());

Session ids and token values are sensitive.

Do not print them in production logs.

Authenticate a session

Use:

auto session = auth.authenticate_session(
    login.value().session.id());

Check the result:

if (session.failed())
{
  rix.debug.eprint(
      "auth error:",
      rix.auth.error.to_string(session.error()),
      session.error().message());

  return 1;
}

Then:

rix.debug.print("session user id:", session.value().user_id());

A session is valid when it exists, is not expired, and is not revoked.

Refresh a session

Use:

auto refreshed = auth.refresh_session(session_id);

Check it:

if (refreshed.failed())
{
  rix.debug.eprint(
      "auth error:",
      rix.auth.error.to_string(refreshed.error()),
      refreshed.error().message());

  return 1;
}

Then:

rix.debug.print("new expires at:", refreshed.value().expires_at());

Refreshing a session extends its expiration according to the configured session lifetime.

Logout

Use:

auto status = auth.logout(session_id);

Check it:

if (status.failed())
{
  rix.debug.eprint(
      "auth error:",
      rix.auth.error.to_string(status.error()),
      status.error().message());

  return 1;
}

Logout revokes the session.

A revoked session can remain stored, but it cannot be used for authentication.

Logout all sessions for a user

Use:

auto status = auth.logout_user(user_id);

Example:

auto status = auth.logout_user(registered.value().id());

if (status.failed())
{
  rix.debug.eprint(
      "auth error:",
      rix.auth.error.to_string(status.error()),
      status.error().message());

  return 1;
}

This revokes all sessions belonging to the user.

Issue a token

Use:

auto token = auth.issue_token(user_id);

Check it:

if (token.failed())
{
  rix.debug.eprint(
      "auth error:",
      rix.auth.error.to_string(token.error()),
      token.error().message());

  return 1;
}

Then read token metadata:

rix.debug.print("user id:", token.value().user_id());
rix.debug.print("issuer:", token.value().issuer());
rix.debug.print("issued at:", token.value().issued_at());
rix.debug.print("expires at:", token.value().expires_at());

Do not log raw token values in production.

Password API

Password helpers are available through:

rix.auth.password

Common helpers:

rix.auth.password.hash(...)
rix.auth.password.verify(...)
rix.auth.password.accepts(...)
rix.auth.password.hasher()

Hash a password

Use:

auto hashed = rix.auth.password.hash("correct-password");

Check it:

if (hashed.failed())
{
  rix.debug.eprint(
      "auth error:",
      rix.auth.error.to_string(hashed.error()),
      hashed.error().message());

  return 1;
}

Then store:

hashed.value()

Do not store plain-text passwords.

Verify a password

Use:

const bool ok = rix.auth.password.verify(
    "correct-password",
    hashed.value());

Wrong passwords return false:

const bool wrong = rix.auth.password.verify(
    "wrong-password",
    hashed.value());

Example:

if (ok && !wrong)
{
  rix.debug.print("password verification works");
}

Check password policy

Use:

const bool accepted = rix.auth.password.accepts("correct-password");

Example:

if (!rix.auth.password.accepts("short"))
{
  rix.debug.eprint("password rejected by policy");
}

hash also checks the password policy.

Create a password hasher

Use:

auto hasher = rix.auth.password.hasher();

Then:

auto hashed = hasher.hash("correct-password");

const bool ok = hasher.verify(
    "correct-password",
    hashed.value());

With configuration:

auto config = rix.auth.config.production();

auto hasher = rix.auth.password.hasher(config);

For most application code, the facade helpers are enough.

Configuration API

Configuration helpers are available through:

rix.auth.config

Common helpers:

rix.auth.config.development()
rix.auth.config.production()

Development configuration

Use:

auto config = rix.auth.config.development();

Development configuration is useful for examples and local development.

It allows simple register and login flows.

Example:

auto auth = rix.auth.memory(
    rix.auth.config.development());

Production configuration

Use:

auto config = rix.auth.config.production();

Production configuration is stricter.

It can require email verification before login.

Example:

auto auth = rix.auth.memory(
    rix.auth.config.production());

Use production configuration when building a real application flow.

Configuration setters

Common configuration setters include:

config.set_min_password_length(...)
config.set_session_ttl_seconds(...)
config.set_token_ttl_seconds(...)
config.set_issuer(...)
config.set_password_hash_iterations(...)
config.set_password_salt_size(...)
config.set_password_hash_size(...)

Example:

auto config = rix.auth.config.development();

config.set_min_password_length(12);
config.set_session_ttl_seconds(60 * 60 * 24 * 7);
config.set_token_ttl_seconds(60 * 15);
config.set_issuer("my-app");

auto auth = rix.auth.memory(config);

Error API

Error helpers are available through:

rix.auth.error

Common helpers:

rix.auth.error.to_string(error)
rix.auth.error.is(error, code)

Example:

if (registered.failed())
{
  const auto &error = registered.error();

  rix.debug.eprint(
      "auth error:",
      rix.auth.error.to_string(error),
      error.message());
}

Auth error codes

Auth error codes include:

rixlib::auth::AuthErrorCode::InvalidInput
rixlib::auth::AuthErrorCode::InvalidEmail
rixlib::auth::AuthErrorCode::InvalidPassword
rixlib::auth::AuthErrorCode::UserNotFound
rixlib::auth::AuthErrorCode::UserAlreadyExists
rixlib::auth::AuthErrorCode::InvalidCredentials
rixlib::auth::AuthErrorCode::InvalidSession
rixlib::auth::AuthErrorCode::SessionExpired
rixlib::auth::AuthErrorCode::InvalidToken
rixlib::auth::AuthErrorCode::TokenExpired
rixlib::auth::AuthErrorCode::InvalidState
rixlib::auth::AuthErrorCode::StoreError
rixlib::auth::AuthErrorCode::Unknown

Use error codes for programmatic decisions.

Use messages for diagnostics.

Check a specific error

if (rix.auth.error.is(
        login.error(),
        rixlib::auth::AuthErrorCode::InvalidCredentials))
{
  rix.debug.eprint("login failed:", "invalid credentials");
}

This lets your app react differently depending on the failure.

Result pattern

Auth APIs use explicit results.

For operations that return a value:

auto result = auth.login({
    "ada@example.com",
    "correct-password"});

if (result.failed())
{
  rix.debug.eprint(
      "auth error:",
      rix.auth.error.to_string(result.error()),
      result.error().message());

  return 1;
}

const auto &login = result.value();

Never call value() before checking success.

Status pattern

Some operations return a status.

Example:

auto status = auth.logout(session_id);

if (status.failed())
{
  rix.debug.eprint(
      "auth error:",
      rix.auth.error.to_string(status.error()),
      status.error().message());

  return 1;
}

Use:

status.ok()
status.failed()
status.error()

User API

A user value exposes user information.

Common methods include:

user.id()
user.email()
user.active()
user.email_verified()

Example:

const auto &user = registered.value();

rix.debug.print("id:", user.id());
rix.debug.print("email:", user.email());

Do not print password hashes in application output.

Session API

A session value exposes session information.

Common methods include:

session.id()
session.user_id()
session.created_at()
session.expires_at()
session.revoked()
session.expired(...)
session.usable(...)

Example:

const auto &session = login.value().session;

rix.debug.print("user id:", session.user_id());
rix.debug.print("expires at:", session.expires_at());

Session ids are sensitive.

Avoid logging raw session ids in production.

Token API

A token value exposes token information.

Common methods include:

token.value()
token.user_id()
token.issuer()
token.issued_at()
token.expires_at()
token.revoked()
token.revoke()
token.expired(...)
token.usable(...)
token.belongs_to(...)
token.issued_by(...)

Example:

const auto &token = login.value().token;

rix.debug.print("issuer:", token.issuer());
rix.debug.print("expires at:", token.expires_at());

The raw token value is sensitive.

Avoid logging:

token.value()

in production.

Complete register and login example

#include <rix.hpp>

int main()
{
  auto auth = rix.auth.memory();

  auto registered = auth.register_user({
      "ada@example.com",
      "correct-password"});

  if (registered.failed())
  {
    rix.debug.eprint(
        "auth error:",
        rix.auth.error.to_string(registered.error()),
        registered.error().message());

    return 1;
  }

  auto login = auth.login({
      "ada@example.com",
      "correct-password"});

  if (login.failed())
  {
    rix.debug.eprint(
        "auth error:",
        rix.auth.error.to_string(login.error()),
        login.error().message());

    return 1;
  }

  auto session = auth.authenticate_session(
      login.value().session.id());

  if (session.failed())
  {
    rix.debug.eprint(
        "auth error:",
        rix.auth.error.to_string(session.error()),
        session.error().message());

    return 1;
  }

  rix.debug.print("authenticated user id:", session.value().user_id());
  return 0;
}

Run:

vix run auth.cpp

Complete password hashing example

#include <rix.hpp>

int main()
{
  auto hashed = rix.auth.password.hash("correct-password");

  if (hashed.failed())
  {
    rix.debug.eprint(
        "auth error:",
        rix.auth.error.to_string(hashed.error()),
        hashed.error().message());

    return 1;
  }

  const bool valid = rix.auth.password.verify(
      "correct-password",
      hashed.value());

  const bool invalid = rix.auth.password.verify(
      "wrong-password",
      hashed.value());

  if (!valid || invalid)
  {
    return 1;
  }

  rix.debug.print("password verification successful");
  return 0;
}

Complete session example

#include <rix.hpp>

int main()
{
  auto auth = rix.auth.memory();

  auto registered = auth.register_user({
      "grace@example.com",
      "correct-password"});

  if (registered.failed())
  {
    return 1;
  }

  auto login = auth.login({
      "grace@example.com",
      "correct-password"});

  if (login.failed())
  {
    return 1;
  }

  const auto session_id = login.value().session.id();

  auto refreshed = auth.refresh_session(session_id);

  if (refreshed.failed())
  {
    return 1;
  }

  auto logout = auth.logout(session_id);

  if (logout.failed())
  {
    return 1;
  }

  auto after_logout = auth.authenticate_session(session_id);

  return after_logout.failed() ? 0 : 1;
}

Complete token example

#include <rix.hpp>

int main()
{
  auto auth = rix.auth.memory();

  auto registered = auth.register_user({
      "linus@example.com",
      "correct-password"});

  if (registered.failed())
  {
    return 1;
  }

  auto token = auth.issue_token(
      registered.value().id());

  if (token.failed())
  {
    return 1;
  }

  rix.debug.print("issuer:", token.value().issuer());
  rix.debug.print("expires at:", token.value().expires_at());

  return 0;
}

Use in a Vix route

#include <vix.hpp>
#include <rix.hpp>

int main()
{
  vix::App app;

  auto auth = rix.auth.memory();

  app.post("/register", [&](vix::Request &, vix::Response &res) {
    auto registered = auth.register_user({
        "ada@example.com",
        "correct-password"});

    if (registered.failed())
    {
      res.status(400).json({
          "ok", false,
          "error", rix.auth.error.to_string(registered.error()),
          "message", registered.error().message()});

      return;
    }

    res.json({
        "ok", true,
        "email", registered.value().email()});
  });

  app.run();

  return 0;
}

For durable applications, use database-backed stores instead of memory stores.

Use in a Vix project

Create a Vix app:

vix new rix-auth-api --app
cd rix-auth-api

Add Rix:

vix add rix/rix
vix install

Make sure vix.app contains:

deps = [
  "rix/rix",
]

Use in src/main.cpp:

#include <rix.hpp>

Build and run:

vix build
vix run

Single-file usage

Create a file:

mkdir -p ~/rix-auth-api
cd ~/rix-auth-api
touch auth.cpp

Add:

#include <rix.hpp>

int main()
{
  auto auth = rix.auth.memory();
  return 0;
}

Run:

vix run auth.cpp

If Rix is not available globally:

vix install -g rix/rix
vix run auth.cpp

Independent usage

Install only auth:

vix add rix/auth
vix install

In vix.app:

deps = [
  "rix/auth",
]

Then include:

#include <rix/auth.hpp>

Use independent package APIs when you intentionally do not want the unified facade.

For most documentation and application examples, prefer:

#include <rix.hpp>

and:

rix.auth

Security notes

Do not log:

plain-text passwords
password hashes
raw token values
session ids

Use HTTPS in production.

Use durable stores for real user accounts.

Use stricter configuration for production.

Keep token lifetimes short.

Keep session lifetimes intentional.

Require email verification when your application needs it.

Common mistakes

Forgetting to install Rix

If your code uses:

#include <rix.hpp>

install:

vix add rix/rix
vix install

Putting Rix in packages

Wrong:

packages = [
  "rix/rix",
]

Correct:

deps = [
  "rix/rix",
]

deps is for Vix Registry packages.

packages is for CMake package discovery.

Calling value() before checking success

Wrong:

auto login = auth.login({
    "ada@example.com",
    "correct-password"});

rix.debug.print(login.value().user.email());

Correct:

auto login = auth.login({
    "ada@example.com",
    "correct-password"});

if (login.failed())
{
  return 1;
}

rix.debug.print(login.value().user.email());

Printing sensitive values

Avoid printing:

passwords
password hashes
tokens
session ids

Expecting memory auth to persist

Memory auth is not durable.

When the process exits, users and sessions are gone.

Use database-backed stores for real applications.

Using production config without email verification flow

Production config can require email verification before login.

For simple examples, use:

auto auth = rix.auth.memory();

or:

auto auth = rix.auth.memory(
    rix.auth.config.development());

Treating tokens and sessions as the same thing

A session is server-side authentication state.

A token is a short-lived value issued for a user.

They have separate lifetimes:

config.set_session_ttl_seconds(...);
config.set_token_ttl_seconds(...);

What you should remember

Create auth:

auto auth = rix.auth.memory();

Register:

auto registered = auth.register_user({
    "ada@example.com",
    "correct-password"});

Login:

auto login = auth.login({
    "ada@example.com",
    "correct-password"});

Authenticate session:

auto session = auth.authenticate_session(
    login.value().session.id());

Hash password:

auto hashed = rix.auth.password.hash("correct-password");

Verify password:

const bool ok = rix.auth.password.verify(
    "correct-password",
    hashed.value());

Issue token:

auto token = auth.issue_token(user_id);

Always check failed() before using value().

For project usage:

vix add rix/rix
vix install

and keep:

deps = [
  "rix/rix",
]

Next step

Continue with the CSV API.

Next: CSV API