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 |