The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Dancer::Plugin::Auth::Basic;

use strict;
use warnings;

# ABSTRACT: Basic HTTP authentication for Dancer web apps

our $VERSION = '0.030'; # VERSION

use Dancer ':syntax';
use Dancer::Plugin;
use Dancer::Response;
use HTTP::Headers;
use MIME::Base64;

my $settings = plugin_setting;

# Protected paths defined in the configuration
my $paths = {};
# "Global" users
my $users = {};

if (exists $settings->{paths}) {
    $paths = $settings->{paths};
}

if (exists $settings->{users}) {
    $users = $settings->{users};
}

sub _check_password {
    my ($password, $text) = @_;
    
    my $crypt;
    
    if (($crypt = $password =~ /^\$\w+\$/) || $password =~ /^\{\w+\}/) {
        # Crypt or RFC 2307 format
        eval {
            require Authen::Passphrase;
            1;
        }
        or do {
            error "Can't use Authen::Passphrase: " . $@;
            return 0;
        };
        
        my $ppr;
        
        eval {
            $ppr = $crypt ? Authen::Passphrase->from_crypt($password) :
                Authen::Passphrase->from_rfc2307($password);
        }
        or do {
            error "Can't construct an Authen::Passphrase recognizer object: " .
                $@;
            return 0;
        };
            
        return $ppr->match($text);
    }
    else {
        # Password in cleartext
        return $password eq $text;
    }
}

sub _auth_basic {
    my (%options) = @_;

    # Get authentication data from request
    my $auth = request->header('Authorization');
    
    my $authorized = undef;
    
    if (defined $auth && $auth =~ /^Basic (.*)$/) {
        my ($user, $password) = split(/:/, (MIME::Base64::decode($1) || ":"));
        
        if (exists $options{user}) {
            # A single user is defined
            $authorized = $user eq $options{user} &&
                _check_password($options{password}, $password);
        }
        
        if (!defined($authorized) && exists($options{users})) {
            # Multiple users are defined
            $authorized = exists($options{users}->{$user}) &&
                _check_password($options{users}->{$user}, $password);
        }
        
        if (!$authorized && defined($users)) {
            # Use the "global" users list
            $authorized = exists $users->{$user} &&
                _check_password($users->{$user}, $password);
        }
        
        if ($authorized) {
            # Authorization successful
            request->env->{REMOTE_USER} = $user;
            return 1;
        }
        
        if (!defined($authorized)) {
            # No users defined? NONE SHALL PASS!
            warning __PACKAGE__ . ": No user/password defined";
        }
    }
    
    my $content = "Authorization required";
    
    return halt(Dancer::Response->new(
        status => 401,
        content => $content,
        headers => [
            'Content-Type' => 'text/plain',
            'Content-Length' => length($content),
            'WWW-Authenticate' => 'Basic realm="' . ($options{realm} ||
                "Restricted area") . '"'
        ]
    ));
}

my $check = sub {
    # Check if the request matches one of the protected paths (reverse sort the
    # paths to find the longest matching path first)
    foreach my $path (reverse sort keys %$paths) {
        my $path_re = '^' . quotemeta($path);
        if (request->path_info =~ qr{$path_re}) {
            _auth_basic %{$paths->{$path}};
            last;
        }
    }
};

# Dynamic paths
hook before => $check;
# Static paths
hook before_file_render => $check;

register auth_basic => \&_auth_basic;
register_plugin;

1; # End of Dancer::Plugin::Auth::Basic

__END__

=pod

=encoding UTF-8

=head1 NAME

Dancer::Plugin::Auth::Basic - Basic HTTP authentication for Dancer web apps

=head1 VERSION

version 0.030

=head1 SYNOPSIS

Dancer::Plugin::Auth::Basic provides basic HTTP authentication for Dancer web
applications.

Add the plugin to your application:

    use Dancer::Plugin::Auth::Basic;

Configure the protected paths and users/passwords in the YAML configuration
file:

    plugins:
      "Auth::Basic":
        paths:
          "/restricted":
            realm: Restricted zone
            user: alice
            password: AlicesPassword
          "/secret/data":
            users:
              alice: AlicesPassword
              bob: BobsPassword

You can also call the C<auth_basic> function in a before filter:

    before sub {
        auth_basic user => 'alice', password => 'AlicesPassword';
    };

or in a route handler:

    get '/confidential' => sub {
        auth_basic realm => 'Authorized personnel only',
            users => { 'alice' => 'AlicesPassword', 'bob' => 'BobsPassword' };
        
        # Authenticated
        ...
    };

=head1 DESCRIPTION

Dancer::Plugin::Auth::Basic adds basic HTTP authentication to Dancer web
applications.

=head1 CONFIGURATION

The available configuration options are listed below.

=head2 paths

Defines one or more paths that will be protected, including sub-paths
(so if the path is C<"/restricted">, then C<"/restricted/secret/file.html"> will
also be protected). Each path can have the following parameters:

=over 4

=item * C<password>

Password (if a single user is allowed access).

=item * C<realm>

Realm name that will be displayed in the authentication dialog. Default:
C<"Restricted area">

=item * C<user>

User name (if a single user is allowed access).

=item * C<users>

A list of user names and passwords (if multiple users are allowed access).

=back

Example:

    plugins:
      "Auth::Basic":
        paths:
          "/secret":
            realm: "Top secret documents"
            user: charlie
            password: CharliesPassword
          "/documents":
            realm: "Only for Bob and Tim"
            users:
              bob: BobsPassword
              tim: TimsPassword

=head2 users

Defines top-level users and their passwords. These users can access all paths
configured using the C<paths> option.

Example:

    plugins:
      "Auth::Basic":
        users:
          fred: FredsPassword
          jim: JimsPassword

=head1 PASSWORDS

Passwords in configuration files can be written as clear text, or in any scheme
that is recognized by L<Authen::Passphrase> (either in RFC 2307 or crypt
encoding).

Example:

     plugins:
      "Auth::Basic":
        users:
          # Clear text
          tom: TomsPassword
          # MD5 hash, RFC 2307 encoding
          ben: "{MD5}X8/UHlR6EiFbFz/0f903OQ=="
          # Blowfish, crypt encoding
          ryan: "$2a$08$4DqiF8T1kUfj.nhxTj2VhuUt1ZX8L.y4aNA3PCAjWLfLEZCw8r0ei"          

=head1 FUNCTIONS

=head2 auth_basic

This function may be called in a before filter or at the beginning of a route
handler. It checks if the client is authorized to access the requested path --
if not, it immediately returns a 401 Unauthorized response to prompt the user to
authenticate.

    auth_basic realm => 'Top secret', user => 'alice',
        password => 'AlicesPassword';

Parameters:

=over 4

=item * C<realm>

Realm name that will be displayed in the authentication dialog. Default:
C<"Restricted area">

=item * C<password>

Password (if a single user is allowed access).

=item * C<user>

User name (if a single user is allowed access).

=item * C<users>

A hash reference mapping user names to passwords (if multiple users are allowed
access).

=back

=head1 SEE ALSO

=over 4



=back

* L<Authen::Passphrase>

=head1 ACKNOWLEDGEMENTS

Inspired by Tatsuhiko Miyagawa's L<Plack::Middleware::Auth::Basic>.

Thanks to Andrew Main for the excellent L<Authen::Passphrase> module.

=for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan

=head1 SUPPORT

=head2 Bugs / Feature Requests

Please report any bugs or feature requests through the issue tracker
at L<https://github.com/odyniec/p5-Dancer-Plugin-Auth-Basic/issues>.
You will be notified automatically of any progress on your issue.

=head2 Source Code

This is open source software.  The code repository is available for
public review and contribution under the terms of the license.

L<https://github.com/odyniec/p5-Dancer-Plugin-Auth-Basic>

  git clone https://github.com/odyniec/p5-Dancer-Plugin-Auth-Basic.git

=head1 AUTHOR

Michal Wojciechowski <odyniec@cpan.org>

=head1 CONTRIBUTOR

Ovid <curtis@weborama.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2011 by Michal Wojciechowski.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut