File Coverage

blib/lib/Dancer/Auth/GoogleAuthenticator.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Dancer::Auth::GoogleAuthenticator;
2 1     1   127571 use Dancer ':syntax';
  1         368523  
  1         5  
3 1     1   1113 use Dancer::Plugin::FlashMessage;
  1         3625  
  1         54  
4 1     1   728 use Auth::GoogleAuthenticator;
  0            
  0            
5             use vars qw($VERSION);
6              
7             =head1 NAME
8              
9             Dancer::Auth::GoogleAuthenticator - Two-Factor demo app
10              
11             =cut
12              
13             $VERSION= '0.03';
14              
15             my %users = (
16             test => { name => 'test',
17             pass => 'test',
18             otp_secret => 'test@example.com',
19             },
20             test2 => { name => 'test2',
21             pass => 'test2',
22             otp_secret => '',
23             },
24             admin => { name => 'admin',
25             pass => 'admin',
26             otp_secret => 'abcde123456',
27             },
28             );
29              
30             # Map a user to its authenticator
31             sub get_otp_auth {
32             my ($user) = @_;
33            
34             return unless $user;
35            
36             my ($otp_secret) = $user->{otp_secret};
37             if( $otp_secret ) {
38             return Auth::GoogleAuthenticator->new( secret => $otp_secret );
39             };
40             return
41             };
42              
43             get '/' => sub {
44             template 'index', {
45             (user => session('user')),
46             #(twofactor_available => get_otp_auth(session('user') || '')),
47             #(twofactor_active => session('twofactor')),
48             };
49             };
50              
51             # Force authentication for all non-index pages
52             hook before => sub {
53             return
54             if request->path_info =~ m{^/(css|javascripts|400|500|favicon.ico|$)};
55             if (! session('user') && request->path_info !~ m{^/auth/login}) {
56             # We store the redirect target internally
57             # and give the user
58             # not an URL but an internal session as the redirect target
59             var requested_path => request->path_info;
60             request->path_info('/auth/login');
61             redirect '/auth/login';
62             }
63             };
64              
65             post '/auth/setup' => sub {
66             my ($user) = session->{user};
67            
68             my $action= params->{action} || '';
69             my $enable= $action eq 'regenerate';
70             my $have_otp= $users{ $user }->{otp_secret};
71            
72             if( 'deactivate' eq $action ) {
73             warning "Erasing OTP secret for $user->{name}";
74             delete $user->{otp_secret};
75             } else {
76             warning "Generating random OTP secret for $user->{name}";
77             # Make up random OTP secret
78             # XX Should be configurable/callback
79             my @letters = ('a'..'z','0'..'9');
80             $user->{otp_secret} = join '', map { $letters[rand @letters]} 1..10;
81             };
82            
83             redirect '/auth/setup';
84             };
85              
86             get '/auth/setup' => sub {
87             my ($user) = session->{user};
88            
89             my ($otp_secret) = $user->{otp_secret};
90             my $auth = get_otp_auth( $user );
91             warning "Have auth for $user->{name}"
92             if $auth;
93            
94             # Display otp_secret if we have it
95             # XX Maybe this should be over SSL only
96             template 'setup_twofactor', {
97             auth => $auth,
98             user => $user,
99             otp_secret => $otp_secret
100             };
101             };
102              
103             get '/auth/setup/qrcode.png' => sub {
104             my ($user) = session->{user};
105            
106             my ($otp_secret) = $user->{otp_secret};
107            
108             if( $otp_secret ) {
109             my $auth = get_otp_auth( $user );
110            
111             if( $auth ) {
112             warning $auth->registration_url($user->{name});
113            
114             my $qr = $auth->registration_qr_code( "$user->{name}" );
115             return send_file( \$qr, content_type => 'image/png' );
116             } else {
117             warning "No auth despite user?!";
118             };
119             } else {
120             warning "No OTP set up for '$user->{name}'";
121             };
122             };
123              
124             get '/auth/login' => sub {
125             my $return = vars->{requested_path} || '';
126            
127             # XX Should only store relative URLs here, or at least
128             # only site-local URLs
129             session->{return_url} = $return;
130             template 'login';
131             };
132              
133             # Maybe also have "requires('password')"
134             # "requires('twofactor')"
135             post '/auth/login' => sub {
136             my ($user_id,$pass,$otp) = (params->{user}, params->{pass}, params->{otp});
137             my $return = vars->{requested_path} || session->{return_url} || '';
138            
139             my $user= $users{ $user_id };
140             if( $user and $user->{pass}
141             and $pass and $pass eq $user->{pass} ) {
142             my $auth_twofactor= get_otp_auth( $user );
143             if( $auth_twofactor ) {
144             # Check OTP in addition to password
145             if( $auth_twofactor->verify( $otp )) {
146             session 'user' => $user;
147             session 'twofactor' => 1;
148             flash success => "User logged in with two-factor auth";
149             warning "User '$user_id' logged in with two-factor auth";
150             } else {
151             # Log the failure
152             # Increment a failure counter for that user
153             # Increment a failure counter for that IP
154             # Increment a failure counter overall, to detect clock skew
155             warning "Wrong OTP for user '$user_id'";
156             flash error => "User unknown or wrong password/OTP";
157             };
158             } else {
159             warning "User '$user_id' logged in with password";
160             session 'user' => $user;
161             session 'twofactor' => 0;
162             flash success => "User logged in with password auth";
163             };
164             } else {
165             warning "Wrong password for user '$user_id'";
166             flash error => "User unknown or wrong password/OTP";
167             };
168            
169             return redirect $return
170             if( session('user') and $return );
171             template 'login';
172             };
173              
174             get '/auth/logout' => sub {
175             session->destroy; # boom
176             };
177              
178             get '/' => sub {
179             template 'index';
180             };
181              
182             true;
183              
184             =head1 SEE ALSO
185              
186             L<http://stackoverflow.com/questions/549/the-definitive-guide-to-forms-based-website-authentication>
187              
188             =cut