Concierge
view release on metacpan or search on metacpan
- 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
## 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 )