3 # This is part of CGI::Auth::Hybrid, a perl CGI authentication module.
4 # Copyright (C) 2012 Ian Jackson.
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
26 %EXPORT_TAGS = ( ); # eg: TAG => [ qw!name1 name2! ],
28 @EXPORT_OK = qw(setup);
35 #---------- default callbacks ----------
37 sub _def_is_logout ($$) {
39 foreach my $pn (@{ $r->{S}{logout_param_names} }) {
40 return 1 if $r->_cm('get_param')($pn);
45 #---------- verifier object methods ----------
51 assocdb_path => 'cah-assocs.db';
54 assocdb_password => '',
55 assocdb_table => 'assocs',
56 random_source => '/dev/urandom',
57 associdlen => 128, # bits
58 login_timeout => 86400, # seconds
59 assoc_param_name => 'cah_associd',
60 password_param_name => 'password',
61 logout_param_names => [qw(logout)],
62 promise_check_mutate => 0,
63 get_param => sub { $_[0]->param($_[2]) },
64 get_cah_cookie => sub { $_[0]->cookie($s->{S}{cookie_name}) },
65 get_method => sub { $_[0]->request_method() },
66 is_login => sub { defined $_[1]->_rp('password_param_name') },
67 login_ok => sub { die },
68 is_logout => \&_def_is_logout,
69 is_page => sub { return 1 },
74 while (($k,$v,@_) = @_) {
75 die "unknown setting $k" unless exists $s->{S}{$k};
88 $s->{S}{assocdb_dsn} ||= "dbi:SQLite:dbname=$s->{S}{assocdb_path}";
91 $dbh = DBI->open($s->{S}{assocdb_dsn}, $s->{S}{assocdb_user},
92 $s->{S}{assocdb_password}, {
93 AutoCommit => 0, RaiseError => 1,
95 die "${assocdb_dsn} $! ?" unless $dbh;
101 $dbh->do("CREATE TABLE $s->{S}{assocdb_table} (".
102 " associdh VARCHAR PRIMARY KEY,".
103 " username VARCHAR,".
104 " last INTEGER NOT NULL"
110 #---------- request object methods ----------
113 my ($classbase, $cgi, @extra) = @_;
114 if (!ref $classbase) {
115 $classbase = $classbase->new_verifier(@extra);
120 S => $classbase->{S},
121 Dbh => $classbase->{Dbh},
124 bless $r, ref $classbase;
127 sub _ch ($$@) { # calls an application hook
128 my ($r,$methname, @args) = @_;
129 my $methfunc = $r->{S}{$methname};
130 return $methfunc->($r->{Cgi}, $r, @args);
135 my $pn = $r->{S}{$pnvb};
136 my $p = $r->_ch('get_param',$pn)
139 # pages/param-sets are
140 # n normal non-mutating page
141 # r retrieval of information for JS, non-mutating
143 # u update of information by JS, mutating
149 # a, aN anything including -
150 # t, tN temporary value (in our db, no logged in user yet)
151 # y, yN value corresponds to logged-in user
152 # n, nN value not in our db
154 # - no value supplied
155 # if N differs the case applies only when the two values differ
156 # (eg, a1 y2 does not apply when the logged-in value is supplied twice)
158 # "stale session" means request originates from a page from a login
159 # session which has been revoked (eg by logout); "cleared session"
160 # means request originates from a browser which has a different (or
163 # Case analysis, cookie mode, app promises re mutate:
164 # cook parm meth form
166 # any - POST nrmuoi bug or attack, fail
167 # any - GET rmuoi bug or attack, fail
168 # any any GET muoi bug or attack, fail
169 # any t any nrmuo bug or attack, fail
171 # a1 a2 POST o logout
172 # if a1 is valid, revoke it
173 # if a2 is valid, revoke it
175 # redirect to "just logged out" page
176 # (which contains link to login form)
178 # - t POST i complain about cookies being disabled
180 # any n POST i complain about stale login form
181 # show new login form
183 # x1 t2 POST i login (or switch user)
184 # revoke x1 if it was valid and !=t2
185 # upgrade t2 to y2 in our db (setting username)
187 # redirect to GET of remaining params
189 # t1 a2 ANY nrmu treat as - a2 ANY
191 # y - GET n cross-site link
194 # y y GET nr fine, show page or send data
195 # y y POST nrmu mutation is OK, do operation
197 # y1 y2 GET nr request from stale page
198 # do not revoke y2 as not RESTful
201 # y1 y2 POST nrmu request from stale page
205 # y n GET n intra-site link from stale page,
206 # treat as cross-site link, show data
208 # y n POST n m intra-site form submission from stale page
209 # show "session interrupted"
210 # with link to main data page
212 # y n GET r intra-site request from stale page
215 # y n POST r u intra-site request from stale page
218 # -/n y2 GET nr intra-site link from cleared session
219 # do not revoke y2 as not RESTful
222 # -/n y2 POST nrmu request from cleared session
224 # treat as -/n n POST
226 # -/n n GET n cross-site link but user not logged in
227 # show login form with redirect to orig params
229 # -/n n GET rmu user not logged in
232 # -/n n POST nrmu user not logged in
235 sub check_divert ($) {
238 my $cookv = $r->_ch('get_cah_cookie');
239 my $parmv = $r->_rp('assoc_param_name');
241 my $cookt = $r->_db_lookup($cookv);
242 my $parmt = $r->_db_lookup($parmv);
244 if ($r->_ch('is_logout')) {
247 $r->_db_perhaps_revoke($cookv);
248 $r->_db_perhaps_revoke($parmv);
249 $r->_queue_set_cookie('');
250 return 'REDIRECT-LOGGEDOUT';
252 if ($r->_ch('is_login')) {
253 return 'NOCOOKIE' if !$cookt && $parmt eq 't';
254 return 'LOGIN-STALE' if $parmt eq 'n';
255 $r->_db_perhaps_revoke($cookv)
256 if defined $cookv && !(defined $parmv && $cookv eq $parmv);
257 $r->_queue_set_cookie($parmv);
258 my $username = $r->_ch('login_ok');
259 return 'LOGIN-BAD' unless defined $username && length $username;
260 $r->_db_record_login_ok($parmv,$username);
261 return 'REDIRECT-LOGGEDIN';
263 if (!$r->{S}{promise_check_mutate}) {
264 something with method get, check parameter, etc.
271 sub _check_core ($) {
273 my $qassoc = $r->_ch('get_param');
274 my ($nassoc,$nmutate);
275 if (!defined $r->{S}{cookie_name}) {
276 # authentication is by hidden form parameter only
277 return undef unless defined $qassoc;
281 # authentication is by cookie
282 # the cookie suffices for read-only GET requests
283 # for mutating and non-GET requests we require hidden param too
284 my $cassoc = $r->_ch('get_cookie');
285 return undef unless defined $cassoc;
287 if (defined $qassoc && $qassoc eq $cassoc) {
290 return undef unless $r->{S}{promise_check_mutate};
291 return undef unless $r->_ch('get_method') eq 'GET';
297 my ($nusername, $nlast) =
298 $dbh->selectrow_array("SELECT username, last".
299 " FROM $r->{S}{assocdb_table}".
300 " WHERE associd = ?", {}, $nassoc);
301 return undef unless defined $nusername;
302 my $timeout = $r->{S}{login_timeout};
303 return undef unless !defined $timeout || time <= $nlast + $timeout;
306 return ($nusername, $nassoc, $nmutate);
309 sub record_login ($$) {
310 my ($r,$nusername) = @_;
311 my $rsp = $r->{S}{random_source};
312 my $rsf = new IO::File $rsp, '<' or die "$rsp $!";
313 my $bytes = ($r->{S}{associdlen} + 7) >> 3;
316 read($rsf,$nassocbin,$bytes) == $bytes or die "$rsp $!";
318 my $nassoc = unpack "H*", $nassocbin;
320 $dbh->do("INSERT INTO $r->{S}{assocdb_table}".
321 " (associd, username, last) VALUES (?,?,?)", {},
322 $nassoc, $nusername, time);
324 $r->{U} = $nusername;
331 return if exists $r->{Username};
332 ($r->{Username}, $r->{Assoc}, $r->{Mutate}) = $r->_check();
334 if (defined $r->{Assoc}) {
335 $dbh->do("UPDATE $r->{S}{assocdb_table}".
337 " WHERE associd = ?", {}, time, $nassoc);
345 my ($nusername, $nassoc, $nmutate) = $r->_check();
346 return undef unless $nmutate;
347 $dbh->do("DELETE FROM $r->{S}{assocdb_table}".
348 " WHERE associd = ?", {}, $nassoc);
356 return !!defined $r->{Username};
359 sub check_mutate ($) {
368 return $r->{Username};
373 return defined $r->{Assoc} ? $r->{Assoc} : '';
376 #---------- simple wrappers ----------
378 sub hidden_hargs ($) {
380 return (-name => $r->{S}{param_name},
381 -default => $r->hidden_val());
384 sub hidden_html ($) {
386 return hidden($r->hidden_hargs());
389 sub cookiea_cargs ($) {
391 return (-name => $r->{S}{cookie_name},
392 -value => hidden_val());
399 CGI::Auth::Hybrid - web authentication optionally using cookies
403 my $verifier = CGI::Auth::Hybrid->new_verifier(setting => value,...);
404 my $authreq = $verifier->new_request($cgi_request_object);
406 my $authreq = CGI::Auth::Hybrid->new_request($cgi_request_object,
407 setting => value,...);
409 =head1 USAGE PATTERN FOR SIMPLE APPLICATIONS
411 $authreq->check_ok() or return;
414 $authreq->mutating();
417 =head1 USAGE PATTERN FOR FANCY APPLICATIONS
419 my $divert_kind = $authreq->check_divert();
421 if ($divert_kind eq 'LOGGEDOUT') {
422 print "goodbye you are now logged out" and quit
423 } elsif ($divert_kind eq 'NOCOOKIES') {
424 print "you need cookies" and quit