1/* Part of SWI-Prolog 2 3 Author: Jan Wielemaker 4 E-mail: J.Wielemaker@vu.nl 5 WWW: http://www.swi-prolog.org 6 Copyright (c) 2007-2015, University of Amsterdam 7 VU University Amsterdam 8 All rights reserved. 9 10 Redistribution and use in source and binary forms, with or without 11 modification, are permitted provided that the following conditions 12 are met: 13 14 1. Redistributions of source code must retain the above copyright 15 notice, this list of conditions and the following disclaimer. 16 17 2. Redistributions in binary form must reproduce the above copyright 18 notice, this list of conditions and the following disclaimer in 19 the documentation and/or other materials provided with the 20 distribution. 21 22 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 POSSIBILITY OF SUCH DAMAGE. 34*/ 35 36:- module(authenticate, 37 [ http_authenticate/3, % +Check, +Header, -User 38 http_authorization_data/2, % +AuthorizationText, -Data 39 http_current_user/3, % +File, ?User, ?Fields 40 41 http_read_passwd_file/2, % +File, -Data 42 http_write_passwd_file/2 % +File, +Data 43 ]). 44:- use_module(library(base64)). 45:- use_module(library(dcg/basics)). 46:- use_module(library(readutil)). 47:- use_module(library(lists)). 48:- use_module(library(crypt)). 49:- use_module(library(debug)). 50:- use_module(library(error)). 51:- use_module(library(apply)). 52 53/** <module> Authenticate HTTP connections using 401 headers 54 55This module provides the basics to validate an HTTP =Authorization= 56header. User and password information are read from a Unix/Apache 57compatible password file. 58 59This library provides, in addition to the HTTP authentication, 60predicates to read and write password files. 61*/ 62 63%! http_authenticate(+Type, +Request, -Fields) 64% 65% True if Request contains the information to continue according 66% to Type. Type identifies the required authentication technique: 67% 68% * basic(+PasswordFile) 69% Use HTTP =Basic= authetication and verify the password 70% from PasswordFile. PasswordFile is a file holding 71% usernames and passwords in a format compatible to 72% Unix and Apache. Each line is record with =|:|= 73% separated fields. The first field is the username and 74% the second the password _hash_. Password hashes are 75% validated using crypt/2. 76% 77% Successful authorization is cached for 60 seconds to avoid 78% overhead of decoding and lookup of the user and password data. 79% 80% http_authenticate/3 just validates the header. If authorization 81% is not provided the browser must be challenged, in response to 82% which it normally opens a user-password dialogue. Example code 83% realising this is below. The exception causes the HTTP wrapper 84% code to generate an HTTP 401 reply. 85% 86% == 87% ( http_authenticate(basic(passwd), Request, Fields) 88% -> true 89% ; throw(http_reply(authorise(basic, Realm))) 90% ). 91% == 92% 93% @param Fields is a list of fields from the password-file entry. 94% The first element is the user. The hash is skipped. 95% @tbd Should we also cache failures to reduce the risc of 96% DoS attacks? 97 98http_authenticate(basic(File), Request, [User|Fields]) :- 99 memberchk(authorization(Text), Request), 100 debug(http_authenticate, 'Authorization: ~w', [Text]), 101 ( cached_authenticated(Text, File, User, Fields) 102 -> true 103 ; http_authorization_data(Text, basic(User, Password)), 104 debug(http_authenticate, 105 'User: ~w, Password: ~s', [User, Password]), 106 validate(File, User, Password, Fields), 107 get_time(Now), 108 assert(authenticated(Text, File, User, Now, Fields)), 109 debug(http_authenticate, 'Authenticated ~w~n', [User]) 110 ). 111 112%! http_authorization_data(+AuthorizeText, ?Data) is semidet. 113% 114% Decode the HTTP =Authorization= header. Data is a term 115% 116% Method(User, Password) 117% 118% where Method is the (downcased) authorization method (typically 119% =basic=), User is an atom holding the user name and Password is 120% a list of codes holding the password 121 Text, Data) (:- 123 ( nonvar(Data) 124 -> functor(Data, Method, 2) % make authorization//2 fail early 125 ; true 126 ), 127 atom_codes(Text, Codes), 128 phrase(authorization(Method, Cookie), Codes), 129 phrase(base64(UserPwd), Cookie), 130 phrase(ident(UserCodes, Password), UserPwd), 131 !, 132 atom_codes(User, UserCodes), 133 Data =.. [Method, User, Password]. 134 Method, Cookie) (--> 136 nonblanks(MethodChars), 137 { atom_codes(Method0, MethodChars), 138 downcase_atom(Method0, Method) 139 }, 140 blanks, 141 nonblanks(Cookie), 142 blanks. 143 144ident(User, Password) --> 145 string(User), 146 ":", 147 string(Password). 148 149%! cached_authenticated(+Authorization, +File, -User, -RestFields) 150% 151% Validate using the cache. If the entry is not in the cache, we 152% also remove all outdated entries from the cache. 153 154:- dynamic 155 authenticated/5. % Authorization, File, User, Time, RestFields 156 157cached_authenticated(Authorization, File, User, Fields) :- 158 authenticated(Authorization, File, User, Time, Fields), 159 get_time(Now), 160 Now-Time =< 60, 161 !. % 60-second timeout 162cached_authenticated(_, _, _, _) :- 163 get_time(Now), 164 ( clause(authenticated(_, _, _, Time, _), true, Ref), 165 Now-Time > 60, 166 erase(Ref), 167 fail 168 ). 169 170 171%! validate(+File, +User, +Passwd, -Fields) 172% 173% True if User and Passwd combination appears in File. File uses 174% the same format as .htaccess files from Apache or Unix password 175% files. I.e. it consists of one line per entry with fields 176% separated by =|:|=. The first field is the User field, The 177% second contains the Passwd in DES or MD5 encrypted format. See 178% crypt/2 for details. 179 180validate(File, User, Password, Fields) :- 181 update_passwd(File, Path), 182 passwd(User, Path, Hash, Fields), 183 crypt(Password, Hash). 184 185%! http_current_user(+File, ?User, ?Fields) is nondet. 186% 187% True when User is present in the htpasswd file File and Fields 188% provides the additional fields. 189% 190% @arg Fields are the fields from the password file File, 191% converted using name/2, which means that numeric values 192% are passed as numbers and other fields as atoms. The 193% password hash is the first element of Fields and is 194% a string. 195 196http_current_user(File, User, Fields) :- 197 update_passwd(File, Path), 198 passwd(User, Path, Hash, Fields0), 199 Fields = [Hash|Fields0]. 200 201%! update_passwd(+File, -Path) is det. 202% 203% Update passwd/3 to reflect the correct passwords for File. Path 204% is the absolute path for File. 205 206:- dynamic 207 passwd/4, % User, File, Encrypted, Fields 208 last_modified/2. % File, Stamp 209 210update_passwd(File, Path) :- 211 absolute_file_name(File, Path, [access(read)]), 212 time_file(Path, Stamp), 213 ( last_modified(Path, Stamp) 214 -> true 215 ; with_mutex(http_passwd, reload_passwd_file(Path, Stamp)) 216 ). 217 218reload_passwd_file(Path, Stamp) :- 219 last_modified(Path, Stamp), 220 !. % another thread did the work 221reload_passwd_file(Path, Stamp) :- 222 http_read_passwd_file(Path, Data), 223 retractall(last_modified(Path, _)), 224 retractall(passwd(_, Path, _, _)), 225 forall(member(passwd(User, Hash, Fields), Data), 226 assertz(passwd(User, Path, Hash, Fields))), 227 assert(last_modified(Path, Stamp)). 228 229%! http_read_passwd_file(+Path, -Data) is det. 230% 231% Read a password file. Data is a list of terms of the format 232% below, where User is an atom identifying the user, Hash is a 233% string containing the salted password hash and Fields contain 234% additional fields. The string value of each field is converted 235% using name/2 to either a number or an atom. 236% 237% == 238% passwd(User, Hash, Fields) 239% == 240 241http_read_passwd_file(Path, Data) :- 242 setup_call_cleanup( 243 open(Path, read, Fd), 244 ( read_line_to_codes(Fd, Line), 245 read_passwd_file(Line, Fd, Path, Data) 246 ), 247 close(Fd)). 248 249read_passwd_file(end_of_file, _, _, []) :- !. 250read_passwd_file(Line, Fd, Path, Data) :- 251 ( phrase(password_line(User, Hash, Fields), Line, _) 252 -> Data = [passwd(User, Hash, Fields)|Tail] 253 ; Tail = Data % TBD: warning 254 ), 255 read_line_to_codes(Fd, Line2), 256 read_passwd_file(Line2, Fd, Path, Tail). 257 258 259password_line(User, Hash, Fields) --> 260 string(UserCodes), 261 ":", 262 string(HashCodes), 263 peek_eof, 264 !, 265 fields(Fields), 266 { atom_codes(User, UserCodes), 267 string_codes(Hash, HashCodes) 268 }. 269 270fields([Field|Fields]) --> 271 field(Field), 272 !, 273 fields(Fields). 274fields([]) --> []. 275 276field(Value) --> 277 ":", 278 !, 279 string(Codes), 280 peek_eof, 281 !, 282 { name(Value, Codes) 283 }. 284 285peek_eof, ":" --> ":". 286peek_eof --> eos. 287 288 289%! http_write_passwd_file(+File, +Data:list) is det. 290% 291% Write password data Data to File. Data is a list of entries as 292% below. See http_read_passwd_file/2 for details. 293% 294% == 295% passwd(User, Hash, Fields) 296% == 297% 298% @tbd Write to a new file and atomically replace the old one. 299 300http_write_passwd_file(File, Data) :- 301 must_be(list, Data), 302 maplist(valid_data, Data), 303 setup_call_cleanup( 304 open(File, write, Out, [encoding(utf8)]), 305 maplist(write_data(Out), Data), 306 close(Out)), 307 retractall(last_modified(File, _)). % flush cache 308 309valid_data(passwd(User, Hash, Fields)) :- 310 !, 311 valid_field(User), 312 valid_field(Hash), 313 must_be(list, Fields), 314 maplist(valid_field, Fields). 315valid_data(Data) :- 316 type_error(passwd_entry, Data). 317 318valid_field(Field) :- 319 must_be(atomic, Field), 320 ( number(Field) 321 -> true 322 ; sub_string(Field, _, _, _, ":") 323 -> representation_error(passwd_field) 324 ; true 325 ). 326 327write_data(Out, passwd(User, Hash, Fields)) :- 328 atomics_to_string([User, Hash|Fields], ":", String), 329 format(Out, '~s~n', [String]). 330 331 332 /******************************* 333 * PLUGIN FOR HTTP_DISPATCH * 334 *******************************/ 335 336:- multifile 337 http:authenticate/3. 338 339%! http:authenticate(+AuthData, +Request, -Fields) 340% 341% Plugin for library(http_dispatch) to perform basic HTTP 342% authentication. 343% 344% This predicate throws http_reply(authorise(basic, Realm)). 345% 346% @arg AuthData must be a term basic(File, Realm) 347% @arg Request is the HTTP request 348% @arg Fields describes the authenticated user with the option 349% user(User) and with the option user_details(Fields) if 350% the password file contains additional fields after the 351% user and password. 352 353httpauthenticate(basic(File, Realm), Request, 354 [ user(User) 355 | Details 356 ]) :- 357 ( http_authenticate(basic(File), Request, [User|Fields]) 358 -> ( Fields == [] 359 -> Details = [] 360 ; Details = [user_details(Fields)] 361 ) 362 ; throw(http_reply(authorise(basic, Realm))) 363 )