Concierge-Auth
view release on metacpan or search on metacpan
examples/1-custom-backend-ldap.pl view on Meta::CPAN
#!/usr/bin/env perl
=head1 NAME
1-custom-backend-ldap.pl - Sketch of a minimal Concierge::Auth::LDAP backend
=head1 DESCRIPTION
Concierge::Auth::Base defines a small, domain-level contract (five methods:
C<new>, C<authenticate>, C<is_id_known>, C<enroll>, C<change_credentials>,
C<revoke>) that any backend must satisfy. The built-in C<Concierge::Auth::Pwd>
backend satisfies it using a flat password file; this example sketches what
a I<directory-backed> implementation looks like instead, given the
connection details a developer would normally supply (host, bind DN, bind
password, base DN, and the attribute holding each user's identifier).
This is a sketch for documentation purposes, not a shipped backend: it
requires L<Net::LDAP> (not a dependency of this distribution) and a real
directory server to actually run against. The point is to show how little
code is needed to satisfy the contract once you have the required
connection info -- the shape of each method, not a production-hardened
LDAP client.
=cut
use v5.36;
package Concierge::Auth::LDAP {
use Carp qw/croak/;
use parent qw/Concierge::Auth::Base/;
# Swap this for `use Net::LDAP;` to run against a real directory.
# Kept as a soft require so this example loads/documents cleanly even
# without Net::LDAP installed.
my $HAVE_NET_LDAP = eval { require Net::LDAP; 1 };
## new: connect and bind with the *service* account used to search the
## directory (as opposed to the end user's own credentials, which are
## only used transiently inside authenticate()).
## Required args: host, bind_dn, bind_password, base_dn
## Optional args: id_attr (default 'uid')
sub new {
my ($class, %args) = @_;
for my $required (qw/host bind_dn bind_password base_dn/) {
croak "Concierge::Auth::LDAP: missing required arg '$required'"
unless defined $args{$required} && length $args{$required};
}
croak "Concierge::Auth::LDAP: Net::LDAP is not installed"
unless $HAVE_NET_LDAP;
my $ldap = Net::LDAP->new($args{host})
or croak "Concierge::Auth::LDAP: could not connect to $args{host}: $@";
my $bind = $ldap->bind($args{bind_dn}, password => $args{bind_password});
croak "Concierge::Auth::LDAP: service bind failed: " . $bind->error
if $bind->code;
return bless {
ldap => $ldap,
base_dn => $args{base_dn},
id_attr => $args{id_attr} // 'uid',
}, $class;
}
## _dn_for: internal helper, not part of the contract. Looks up the
## distinguished name for a given user_id via search, since a real
## directory's DN usually isn't just "<id_attr>=<id>,<base_dn>".
sub _dn_for ($self, $user_id) {
my $result = $self->{ldap}->search(
base => $self->{base_dn},
filter => "($self->{id_attr}=$user_id)",
attrs => ['dn'],
);
return undef unless $result->count == 1;
return ($result->entries)[0]->dn;
}
## authenticate: verify a submitted credential. Implemented as a bind
## attempt using the user's own DN and submitted password -- no
## passwords are ever read or stored locally.
sub authenticate ($self, $user_id, $credential) {
my $dn = $self->_dn_for($user_id);
return { success => 0, message => "Unknown user_id" }
unless $dn;
my $bind = $self->{ldap}->bind($dn, password => $credential);
return { success => 0, message => "Invalid credentials" }
if $bind->code;
return { success => 1 };
}
## is_id_known: existence check only -- no credential involved.
sub is_id_known ($self, $user_id) {
my $dn = $self->_dn_for($user_id);
return { success => 1, known => $dn ? 1 : 0 };
}
## enroll: directory identities are provisioned by directory admins,
## not by this backend, so enroll() confirms rather than creates.
## Concierge::Auth::Pwd's enroll() *creates* a record; an
## externally-provisioned backend like this one instead reports
## whether the ID is already known to the authority.
sub enroll ($self, $user_id, $credential, $opts = undef) {
my $dn = $self->_dn_for($user_id);
return { success => 0, message => "ID not found in directory" }
unless $dn;
return { success => 1, user_id => $user_id, status => 'already_known' };
}
## change_credentials: modify the userPassword attribute via the
## service bind. Real directories often require the *user's own* bind
## (or directory-specific password-change extended ops) rather than a
## simple attribute replace under a service account; that decision is
## directory-policy-specific and intentionally simplified here.
sub change_credentials ($self, $user_id, $new_credential) {
my $dn = $self->_dn_for($user_id);
return { success => 0, message => "ID not found in directory" }
unless $dn;
my $result = $self->{ldap}->modify(
$dn, replace => { userPassword => $new_credential },
);
return { success => 0, message => $result->error }
if $result->code;
return { success => 1, user_id => $user_id };
}
## revoke: sever the local association. This deliberately does NOT
## delete the directory entry -- that's outside this backend's
## authority, matching the Base.pm contract note that externally
## provisioned backends revoke local standing, not the external record.
sub revoke ($self, $user_id) {
my $dn = $self->_dn_for($user_id);
return { success => 0, message => "ID not found in directory" }
unless $dn;
return { success => 1, user_id => $user_id };
}
# gen_uuid / gen_random_token / gen_word_phrase / etc. are inherited
# from Concierge::Auth::Base for free -- no need to reimplement them.
}
# --- Usage, exactly like any other backend -------------------------------
say "=== Concierge::Auth::LDAP sketch ===";
say "";
say " my \$auth = Concierge::Auth->new(";
say " backend_class => 'Concierge::Auth::LDAP',";
say " host => 'ldaps://directory.example.com',";
say " bind_dn => 'cn=service,dc=example,dc=com',";
say " bind_password => \$service_password,";
say " base_dn => 'ou=people,dc=example,dc=com',";
say " id_attr => 'uid', # optional, defaults to 'uid'";
say " );";
say "";
say " my \$result = \$auth->authenticate('alice', \$submitted_password);";
say " say \$result->{success} ? 'ok' : \"failed: \$result->{message}\";";
say "";
say "No caller-visible difference from Concierge::Auth::Pwd -- same";
say "5-method contract, same hashref return shapes, same Concierge::Auth";
say "facade. The only thing that changed is what happens *inside* those";
say "five methods.";
__END__
=head1 WHY NOT OAUTH HERE
OAuth doesn't map onto this same C<authenticate($user_id, $credential)>
shape as directly as LDAP does, because Concierge::Auth's contract assumes
the application itself receives a submitted credential to check. In a
typical OAuth flow the application never sees the user's password at all --
it receives a token from the provider after a redirect-based exchange the
application mediates but doesn't perform inline.
A Concierge::Auth::OAuth backend would still satisfy the same five methods,
but with different inputs:
authenticate($user_id, $token) # verify an access/ID token instead of a password
is_id_known($user_id) # check a local cache of provider subjects
enroll($user_id, $token, \%opts) # record a new provider subject locally
change_credentials(...) # often a no-op or "revoke + re-link"; OAuth
# providers manage credentials themselves
revoke($user_id) # sever the local association only
The token exchange itself (redirect, authorization code, provider callback)
happens *before* any Concierge::Auth method is called at all -- it's
outside this contract's scope, the same way this LDAP sketch's directory
bind happens outside of any web framework's routing layer. Concierge::Auth
only needs to know how to verify what the application hands it.
=head1 SEE ALSO
=over 4
=item * L<Concierge::Auth::Base> - the five-method backend contract
=item * L<Concierge::Auth::Pwd> - the built-in reference implementation
=back
=cut
( run in 0.605 second using v1.01-cache-2.11-cpan-aadc1410aed )