Concierge

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

    - Desk::Setup: added %AUTH_BACKENDS catalog (currently 'pwd' ->
      Concierge::Auth::Pwd); build_desk() and validate_setup_config()
      resolve/validate auth.backend through it. Documented (POD +
      commented-out catalog entries) that additional backends -- e.g.
      OAuth, SAML, or a non-Concierge::-namespaced class -- can be added
      as one-entry additions; the backend class is not required to live
      under the Concierge:: namespace.
    - Concierge.pm: six call sites (add_user, remove_user, verify_user,
      login_user, verify_password, reset_password) converted from the
      old Auth primitives to the new authenticate/is_id_known/enroll/
      change_credentials/revoke verbs.
    - Fixed three call sites (admit_visitor, checkin_guest,
      Desk::User::enable_user) that called Concierge::Auth as a class
      method for ID generation -- broken outright under the new factory,
      which has no class methods; now call the configured backend
      instance or Concierge::Auth::Generators directly.
    - BREAKING: Desk::Setup's build_desk() config is reshaped: the
      former storage => { base_dir, sessions_dir, users_dir, auth_dir }
      block is gone. base_dir is now a top-level setting (it was the
      only entry left in storage once each component's own directory
      moved into its own block), and each component's storage location

README.md  view on Meta::CPAN

## Components

Concierge ships with a complete identity core out of the box, and the same
component pattern that powers it extends to anything else your
application needs to manage.

### Identity Core (built in)

#### Authentication — Concierge::Auth

- **Argon2** password hashing and verification; no plaintext credentials
  written to disk
- Random value generators: hex IDs, alphanumeric tokens, UUIDs (v4),
  word-passphrases from a system dictionary
- Designed for substitution: swap in any replacement that implements the
  same method contract (`enroll`, `authenticate`, `is_id_known`,
  `change_credentials`, `revoke`) for LDAP, OAuth, or other schemes

#### Sessions — Concierge::Sessions

- **Multiple backends**: SQLite (recommended) or flat-file
- Every session lives in memory first; data is only written to whichever
  backend is configured when `->save()` is called. Some sessions never call
  `save()` at all and exist purely for in-process continuity.
- Sessions carry arbitrary key/value data (shopping carts, wizard state,
  preferences, etc.)
- Configurable timeout per session; expired sessions cleaned up automatically

lib/Concierge.pm  view on Meta::CPAN

# === COMPONENT MODULES ===
use Concierge::Desk::User;
use Concierge::Desk::Component;
use Concierge::Auth;
use Concierge::Sessions;
use Concierge::Users;

# === PARAMETER FILTERS ===
# Shared filters for secure data segregation

# Auth filter - ONLY credentials (user_id + password)
our $auth_data_filter = make_filter(
    [qw(user_id password)],                   # required credentials
    [],                                        # accepted - nothing else
    [],                                        # excluded - not needed
);

# User data filter - everything EXCEPT credentials
# Handles both minimal input (user_id, moniker) and
# rich input (user_id, moniker, email, phone, bio, etc.)
our $user_data_filter = make_filter(
    [qw(user_id moniker)],                    # required minimum
    ['*'],                                    # accept ALL other fields, except:
    [qw(password confirm_password)],          # excluded - security boundary
);

# Session data filter - for populating session with initial data
# Accepts user_id (required for new_session) plus any session fields
# Excludes credentials (never stored in session data)
our $session_data_filter = make_filter(
    [qw(user_id)],                            # required for new_session
    ['*'],                                    # accept all other fields, except:
    [qw(password confirm_password)],          # excluded - security boundary
);

# User update filter - for updating existing user records
# No required fields (user_id passed separately as parameter)
# Excludes user_id (identity field), password (use reset_password instead)
our $user_update_filter = make_filter(

lib/Concierge.pm  view on Meta::CPAN

        return {
            success  => 1,
            message  => 'Guest restored',
            user     => $user,
            is_guest => 1,
        };
    }
}

# Login user: authenticate, create session, assign user_key and store external_key mapping
sub login_user ($self, $credentials, $session_opts={}) {
    # Step 0: Get credentials
    my $auth_data = $auth_data_filter->($credentials);
    return { success => 0, message => 'Missing user_id or password' }
        unless $auth_data;

    my $user_id = $auth_data->{user_id};
    my $password = $auth_data->{password};

    # Step 1: Get user from database
    my $user_result = $self->users->get_user($user_id);
    return { success => 0, message => 'User not found' }
        unless $user_result->{success};

lib/Concierge.pm  view on Meta::CPAN

sub reset_password ($self, $user_id, $new_password) {
    # Changes user password using Auth component
    # Application is responsible for verifying user identity and old password if needed

    return { success => 0, message => 'user_id is required' }
        unless defined $user_id && length($user_id);

    return { success => 0, message => 'new_password is required' }
        unless defined $new_password && length($new_password);

    # Reset existing password using Auth's change_credentials
    # Pass through Auth's messages (Auth provides detailed error messages)
    my $changed = $self->auth->change_credentials($user_id, $new_password);
    my ($reset_ok, $reset_msg) = ($changed->{success}, $changed->{message});

    return {
        success => $reset_ok ? 1 : 0,
        message => $reset_msg || ($reset_ok ? 'Password reset successful' : 'Password reset failed'),
        user_id => $user_id,
    };
}

# Logout user: delete session and remove from concierge mapping

lib/Concierge.pm  view on Meta::CPAN

the identity core, or handoff to whatever additional components a desk
is configured with.

The identity core -- Auth, Sessions, and Users -- ships with every
installation and is described in detail below. An added component is
just as much a capability of the suite once it's attached to a desk; see
L</Additional Components> for how those are configured, loaded, and
reached.

B<Authentication> (L<Concierge::Auth>): Argon2id password hashing and
verification; no plaintext credentials are ever written to disk. Also
provides random token, UUID, word-passphrase, and hex-ID generators. The
component is substitutable: any replacement implementing the same method
contract (C<authenticate>, C<enroll>, C<change_credentials>, etc. -- see
L<Concierge::Auth::Base>) can replace it for LDAP, OAuth, or any other
scheme.

B<Sessions> (L<Concierge::Sessions>): Full session lifecycle -- creation,
retrieval, expiry, and cleanup -- with SQLite, file, or in-memory storage.
Sessions carry arbitrary key/value data. A single-session-per-user policy
is enforced: creating a new session automatically removes any prior session
for that user. Expired sessions are cleaned up each time a desk is opened.

B<User Records> (L<Concierge::Users>): User data store with a configurable

lib/Concierge.pm  view on Meta::CPAN

Assigned a unique identifier only. No session, no stored data. Suitable for
anonymous tracking (e.g., cookies).

=item B<Guest> -- C<checkin_guest()>

Assigned an identifier and a session. Can store temporary data (e.g., a
shopping cart). No authentication or persistent user record.

=item B<Logged-in user> -- C<login_user()>

Authenticated with credentials. Has a session, persistent user data, and
full access to the User object's data methods.

=back

A guest can be converted to a logged-in user with C<login_guest()>,
transferring any session data accumulated during the guest session.

=head2 User Keys

Each active user (guest or logged-in) is tracked by a I<user_key> -- a

lib/Concierge.pm  view on Meta::CPAN


    my $result = $concierge->checkin_guest(\%session_opts);
    my $user = $result->{user};    # Concierge::Desk::User (guest)

Creates a guest with a generated identifier and a session. The optional
C<%session_opts> hashref may include C<timeout> (in seconds; defaults to
1800).

=head3 login_user

    my $result = $concierge->login_user(\%credentials, \%session_opts);
    my $user = $result->{user};    # Concierge::Desk::User (logged-in)

Authenticates C<user_id> and C<password> from C<%credentials>, retrieves
the user's data record, creates a session, and returns a fully-equipped
User object. If the user already has an active session, the previous
session is replaced.

=head3 restore_user

    my $result = $concierge->restore_user($user_key);
    my $user = $result->{user};    # Concierge::Desk::User (guest or logged-in)

Reconstructs a User object from a C<user_key> (typically stored in a cookie

lib/Concierge.pm  view on Meta::CPAN


If the session has expired, the stale mapping entry is cleaned up and the
method returns failure. The application can then redirect to login or create
a new guest as appropriate.

Returns C<< { success => 1, user => $user } >> on success. Guest restores
also include C<< is_guest => 1 >>.

=head3 login_guest

    my $result = $concierge->login_guest(\%credentials, $guest_user_key);
    my $user = $result->{user};    # Concierge::Desk::User (logged-in)

Converts a guest to a logged-in user. If C<%credentials>' C<user_id> does
not already belong to a known user, registers a new account first (same
requirements as C<add_user()>); if it does belong to a known user (e.g. a
returning customer who browsed as a guest before logging in to pay),
registration is skipped and the existing account is used instead.
Authenticates with C<%credentials>, transfers any data from the guest's
session to the new session, then deletes the guest session and removes
the guest's user_key mapping.

Returns failure if C<user_id> exists in only one of the Auth/Users
components (an inconsistent state) rather than attempting either path.

=head3 logout_user

    my $result = $concierge->logout_user($session_id);

lib/Concierge.pm  view on Meta::CPAN

=item C<$auth_data_filter> -- extracts only C<user_id> and C<password>

=item C<$user_data_filter> -- extracts everything except C<password>

=item C<$session_data_filter> -- extracts C<user_id> plus non-credential fields

=item C<$user_update_filter> -- excludes C<user_id> and C<password> from updates

=back

These ensure that credentials never leak into user data stores and that
identity fields cannot be changed via update operations.

=head1 EXTENSIBILITY

See L</CONCEPTS> for the ideas behind extensibility, service-layer
guarantees, and orchestration that this section implements.

=head2 Component Substitution

Each identity core component can be replaced with a drop-in alternative as
long as the replacement implements the methods Concierge calls on it.

B<Auth> -- Concierge calls:

    $auth->authenticate($user_id, $credential)
    $auth->is_id_known($user_id)
    $auth->enroll($user_id, $credential, \%opts?)
    $auth->change_credentials($user_id, $new_credential)
    $auth->revoke($user_id)

A substitute backend must implement this contract -- see
L<Concierge::Auth::Base>, which also provides working default
L<Concierge::Auth::Generators> methods (used for visitor/guest
identifiers, independent of authentication) to any backend that
inherits from it; a substitute only needs to override these if it
wants different generation logic.

B<Sessions> -- Concierge calls:

t/04-user-operations.t  view on Meta::CPAN


subtest 'list_users with include_data' => sub {
    my $result = $concierge->list_users('', { include_data => 1 });

    ok $result->{success}, 'list_users with data succeeds';
    ref_ok $result->{users}, 'HASH', 'users is hash';
    ok exists $result->{users}{alice}, 'alice data included';
    is $result->{users}{alice}{moniker}, 'Alice', 'alice data correct';
};

subtest 'verify_password checks credentials' => sub {
    my $result = $concierge->verify_password('alice', 'secret123');
    ok $result->{success}, 'correct password verified';

    $result = $concierge->verify_password('alice', 'wrongpass');
    ok !$result->{success}, 'incorrect password rejected';
};

subtest 'reset_password changes password' => sub {
    my $result = $concierge->reset_password('alice', 'newsecret456');
    ok $result->{success}, 'reset_password succeeds';



( run in 1.637 second using v1.01-cache-2.11-cpan-0fb53d1c279 )