Concierge-Auth

 view release on metacpan or  search on metacpan

lib/Concierge/Auth.pm  view on Meta::CPAN

use Carp			qw/carp croak/;
use Fcntl			qw/:flock/;
use Crypt::Passphrase;
use parent			qw/Concierge::Auth::Generators/;

## Constants for validation
use constant {
    MIN_ID_LENGTH     	=> 2,
    MAX_ID_LENGTH     	=> 32,
    MIN_PASSWORD_LENGTH	=> 8,
    MAX_PASSWORD_LENGTH	=> 72,    # bcrypt limit
};

## Pre-compiled regex for ID validation - accepts email addresses
my $ID_ALLOWED_CHARS	= qr/^[a-zA-Z0-9._@-]+$/;
## Password file field separator
my $FIELD_SEPARATOR		= "\t";

## new: instantiate the auth object with a passwd file
## Complains if no file is provided unless the argument
## no_file => 1 is provided, but still instantiates
## the auth object; without a passwd file, the auth object
## can only provide the utility methods:
## encryptPwd(), gen_random_token(), gen_random_string(),
## gen_word_phrase(), gen_uuid()
## A file may be designated after instantiation with
## the method setFile().
## Dies if it can't open/create a designated file.
## Complains if it can't set permissions on the file.
sub new {
	my ($class, $args)	= @_;

	my $self	= bless {
		auth => Crypt::Passphrase->new(
			encoder		=> 'Argon2',
			validators	=> [ 'Bcrypt' ],
		)
	}, $class;

	if ($args->{no_file}) {
		carp "Utilities only; no ID and password checks";
		# Still functional:
		return $self;
	}
	unless ($args->{file}) {
		carp "No auth file provided for ID and password checks";
		# Still functional:
		return $self;
	}

	if (-e $args->{file}) {
		open my $afh, "<", $args->{file} or
			croak ("Can't read auth file ($args->{file}). $! ");
		close $afh;
	} else {
		open my $afh, ">", $args->{file} or
			croak ("Can't open/create auth file ($args->{file}). $! ");
		close $afh;
	}

 	chmod 0600, $args->{file} or carp $!;
	$self->{auth}->{file}	= $args->{file};
	$self;
}

## Class Methods for Responses
## confirm, reject, reply
## NOT called with object arrow notation:
##    $self->reject # !WRONG
## Once instantiated, the auth object will not die/croak;
## Instead, all methods that check or validate respond with
##	`confirm ($msg)` # wantarray ? (1, $msg) : 1;
##  	or
##  `reject ($msg)`  # wantarray ? (0, $msg) : 0;
##  	or the more general
##  `reply ($bool, $msg)` # wantarray ? ($bool, $msg) : $bool
## Use explicit `return` to assure correct contrl flow:
## `return confirm($msg);`
## `return reply( $result, $msg);`
sub confirm {
	my $message = shift || "Auth confirmation";
	wantarray ? (1, $message) : 1;
}
sub reject {
	my $message = shift || "Auth rejection";
	wantarray ? (0, $message) : 0;
}
## First arg is 1|0 or other Perl true/false value
sub reply {
	my $bool	= shift // 0;
	my $message = shift || ( $bool ? "Auth confirmation" : "Auth rejection" );
	wantarray ? ($bool, $message) : $bool;
}

## Validations
sub validateID {
    my ($self, $id) = @_;

	return reject( "ID cannot be empty" )
		unless (defined $id && length($id) > 0);

    # Check length constraints
    return reject( sprintf(
    	"ID must be between %d and %d characters",
        MIN_ID_LENGTH, MAX_ID_LENGTH
    ) ) unless (
    	length($id) >= MIN_ID_LENGTH
    	 &&
    	length($id) <= MAX_ID_LENGTH
    );

    # Check pattern
    return reject( "ID contains invalid characters" )
    	unless ($id =~ $ID_ALLOWED_CHARS);

    return confirm;
}

sub validatePwd {
    my ($self, $password) = @_;

lib/Concierge/Auth.pm  view on Meta::CPAN

    my @pwd		= $self->validatePwd($passwd);
    return reply( @pwd ) unless $pwd[0];

	my $phash	= $self->encryptPwd($passwd);

	my $sep		= $FIELD_SEPARATOR;
 	my $pfile	= $self->{auth}->{file} || '';
 	my @f		= $self->validateFile($pfile);
 	return reply( @f ) unless $f[0];

  	open my $fh, "+<", $pfile or return reject( "resetPwd: Cannot open file: $!" );
    flock($fh, LOCK_EX) or do {
		close $fh;
		return reject( "resetPwd: Cannot lock file: $!" );
	};
	my @lines	= <$fh>;

 	my $success	= 0;
	my @output;
	for my $line ( @lines ) {
		if ( $line =~ /^$id$sep/) {
			push @output => join( $sep => $id, $phash, "|\n" );
			$success++;
			next;
		}
		push @output, $line;
	}
	unless (
		seek($fh, 0, 0)
		and truncate($fh, 0)
		and print $fh @output
		and close $fh
	) {
		close $fh;
		return reject( "resetPwd: File update failed: $!" );
	}

	return reply($success, ($success ? $id : "ID $id not found to reset password") );
}

## Password file handling

## setFile: sets or changes the passwd file
## creates the file if necessary
sub setFile {
	my $self	= shift;
	my $file	= shift;

	return reject( "No filename" ) unless $file =~ /\S/;

	if (-e $file) {
		open my $afh, "<", $file or
			return reject(  "Can't read auth file ($file). $!" );
		close $afh;
	} else {
		open my $afh, ">", $file or
			return reject(  "Can't open/create auth file ($file). $!" );
		close $afh;
	}

 	chmod 0600, $file or carp $!;

 	if ( $self->validateFile($file) ) {
		$self->{auth}->{file}	= $file;
		return confirm( "Valid file" );
 	}

	return reject( "Invalid file" );
}

## rmFile: deletes the passwd file
sub rmFile {
	my $self	= shift;

 	my $pfile	= $self->{auth}->{file} || '';
 	unless ( $pfile and -e $pfile ) {
 		return reject( "No valid file to remove" );
 	}

	unless (unlink $pfile) {
		return reject( "Unable to unlink file: $! " );
	}

	$self->{auth}->{file} = '';

	return reply( $pfile, "Password file removed" );
}

sub clearFile {
	my $self	= shift;

	my ($pfile,$msg)	= $self->rmFile();
	return reply( 0, "No valid file to clear: $msg" ) unless $pfile;

	my ($ok,$setmsg)		= $self->setFile($pfile);
	return reject( "File not cleared: $setmsg" ) unless $ok;
    return confirm( "File cleared" );
}

## Utilities

## encryptPwd: returns encrypted password
sub encryptPwd {
	my $self	= shift;
	my $passwd	= shift;

    my @vp	= $self->validatePwd($passwd);
    return reply( @vp ) unless $vp[0];

	return $self->{auth}->hash_password($passwd);
}

## pfile: returns the passwd file, if any
sub pfile {
	my $self	= shift;
	return defined $self->{auth}->{file}
		? reply($self->{auth}->{file}, "Auth file" )
		: reject( "No auth file" );
}

## Generator method wrappers



( run in 0.641 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )