├── README.markdown └── mod_perl ├── GodAuth.pm ├── GodAuthConfig.pm ├── GodAuthInit.pl └── god_auth.conf /README.markdown: -------------------------------------------------------------------------------- 1 | GodAuth 2 | ======= 3 | 4 | A system for handling single-signon authentication across multiple web apps under apache. 5 | 6 | 7 | Design 8 | ------ 9 | 10 | You create a simple signin system that take your user's authentication credentials (username, password, whatever) 11 | and compares it to your user database. It then mints a signed cookie containing the username and a list of 'roles'. 12 | A mod_perl layer then checks this cookie for every request, allowing or denying it based on a set of rules where 13 | different URL regexps require different users or roles. It then exposes the username and roles of the authenticated 14 | user to the underlying applications via environment variables and request headers. 15 | 16 | Because it sits in the Apache layer, you can use it to control access to multiple applications - svn browsers, wikis, 17 | bug trackers, database admin tools, deploy tools, monitoring, pastebins, logs, etc. 18 | 19 | 20 | Installation 21 | ------------ 22 | 23 | 1. Copy all the files in the mod_perl folder to somewhere on your server that Apache can read from. 24 | 2. Adjust values in GodAuthConfig.pm to match your setup. 25 | 3. Modify the path in GodAuthInit.pl. 26 | 4. Modify the config path at the bottom of GodAuth.pm. 27 | 5. Modify the path in god_auth.conf. 28 | 6. Symlink god_auth.conf into /etc/httpd/conf.d (or your local equivalent). 29 | 30 | Patches to make this less path-edity are welcome. Setting an environment variable in GodAuthInit.pl 31 | is probably a good approach. 32 | 33 | 1. Setup the login webapp. 34 | 2. But it's not done yet... 35 | -------------------------------------------------------------------------------- /mod_perl/GodAuth.pm: -------------------------------------------------------------------------------- 1 | package GodAuth; 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use GodAuthConfig; 7 | use Apache2::RequestRec (); 8 | use Apache2::Connection; 9 | use Apache2::Const -compile => qw(OK REDIRECT REMOTE_NOLOOKUP FORBIDDEN); 10 | use APR::Table; 11 | use Digest::SHA1 qw(sha1_hex); 12 | use MIME::Base64; 13 | use Data::Dumper; 14 | 15 | use Sys::Hostname; 16 | 17 | our $last_reload_time = time(); 18 | our $reload_timeout = 60; # per apache process! 19 | 20 | $| = 1; 21 | 22 | ############################################################################################################## 23 | 24 | sub handler { 25 | 26 | # 27 | # get URL 28 | # 29 | 30 | my $r = shift; 31 | 32 | my $domain = $r->headers_in->{'Host'} || 'UNKNOWN-HOST'; 33 | my $path = $r->unparsed_uri; 34 | 35 | my $host = hostname; 36 | 37 | $ENV{GodAuth_User} = ''; 38 | 39 | my $url = $domain . $path; 40 | my $log = "$$ URL : $url"; 41 | 42 | 43 | ######################################################### 44 | # 45 | # reload the config? 46 | # 47 | 48 | if (time() - $GodAuth::last_reload_time > $GodAuth::reload_timeout){ 49 | 50 | &GodAuth::reload_config(); 51 | $GodAuth::last_reload_time = time(); 52 | } 53 | 54 | 55 | ######################################################### 56 | # 57 | # 1) check we have a cookie secret 58 | # 59 | if (!$GodAuthConfig::CookieSecret){ 60 | $GodAuthConfig::CookieSecret = 'nottherightsecret'; 61 | } 62 | 63 | 64 | ######################################################### 65 | # 66 | # 1) determine if we need to perform access control for this url 67 | # 68 | 69 | my $allow = 'none'; 70 | 71 | for my $obj (@{$GodAuthConfig::PermMap}){ 72 | 73 | if ($url =~ $obj->{url}){ 74 | 75 | $allow = $obj->{who}; 76 | last; 77 | } 78 | } 79 | 80 | $log .= " $allow"; 81 | 82 | 83 | ######################################################### 84 | # 85 | # 2) we might need auth - see if we have a valid cookie 86 | # 87 | 88 | my $cookie_is_valid = 0; 89 | my $cookie_user = '?'; 90 | my $cookie_roles = '_'; 91 | 92 | my $cookie_is_old = 0; 93 | my $cookie_age = 0; 94 | my $cookie_is_future = 0; 95 | 96 | my $cookies = &parse_cookie_jar($r->headers_in->{'Cookie'}); 97 | 98 | my $cookie = $cookies->{$GodAuthConfig::CookieName}; 99 | 100 | if ($cookie){ 101 | 102 | my ($user, $roles, $ts, $hmac) = split '-', $cookie, 4; 103 | 104 | my $ua = $r->headers_in->{'User-Agent'}; 105 | 106 | if ($ua =~ /AppleWebKit/) { 107 | $ua = "StupidAppleWebkitHacksGRRR"; 108 | } 109 | $ua =~ s/ FirePHP\/\d+\.\d+//; 110 | 111 | my $raw = "$user-$roles-$ts-$ua"; 112 | 113 | #&xlog("COOKIE: $cookie $raw\n"); 114 | 115 | my $hmac2 = sha1_hex( $GodAuthConfig::CookieSecret . $raw ); 116 | 117 | if ($hmac eq $hmac2){ 118 | 119 | # 120 | # check that our cookie isn't too old 121 | # 122 | 123 | $cookie_age = time() - $ts; 124 | $ENV{GodAuth_Cookie_Age} = $cookie_age; 125 | 126 | if ($ts < time() - 8 * 60 * 60 && $user !~ /\:/){ 127 | 128 | # 129 | # cookie is old (only for non-alpha users 130 | # 131 | 132 | $cookie_is_old = 1; 133 | $cookie_age = time() - $ts; 134 | 135 | $log .= " (bad cookie ts $ts - it's too old - $cookie_age seconds)"; 136 | 137 | }elsif ($ts > time() + 5 * 60){ 138 | 139 | # 140 | # cookie starts in the future - wtf 141 | # 142 | 143 | $cookie_is_future = 1; 144 | 145 | $log .= " (bad cookie ts $ts - it starts in the future)"; 146 | 147 | }else{ 148 | 149 | $cookie_is_valid = 1; 150 | $cookie_user = $user; 151 | $cookie_roles = $roles; 152 | 153 | $r->headers_in->set('GodAuth-User', $cookie_user); 154 | $r->headers_in->set('GodAuth-Roles', $cookie_roles); 155 | 156 | $ENV{GodAuth_User} = $cookie_user; 157 | $ENV{GodAuth_Roles} = $cookie_roles; 158 | 159 | $r->notes->add("GodAuth_User" => $cookie_user); 160 | $r->notes->add("GodAuth_Roles" => $cookie_roles); 161 | 162 | $log .= " (cookie: $cookie_user $cookie_roles)"; 163 | } 164 | }else{ 165 | $log .= " (bad cookie hmac [$GodAuthConfig::CookieSecret$user-$ts-$ua] -> $hmac2 vs $hmac)"; 166 | } 167 | }else{ 168 | $log .= " (no cookie)"; 169 | } 170 | 171 | &xlog($log."\n"); 172 | 173 | 174 | ######################################################### 175 | # 176 | # 3) exit now if we got an 'all' 177 | # 178 | 179 | if (ref $allow ne 'ARRAY'){ 180 | if ($allow eq 'all'){ 181 | 182 | return Apache2::Const::OK; 183 | } 184 | } 185 | 186 | 187 | ######################################################### 188 | # 189 | # 4) if we don't have a valid cookie, redirect to the auther 190 | # 191 | 192 | if (!$cookie){ 193 | return &redir($r, $url, $GodAuthConfig::FailNeedsAuth); 194 | } 195 | 196 | if ($cookie_is_old){ 197 | return &redir($r, $url, $GodAuthConfig::FailCookieOld); 198 | } 199 | 200 | if ($cookie_is_future){ 201 | return &redir($r, $url, $GodAuthConfig::FailCookieFuture); 202 | } 203 | 204 | if (!$cookie_is_valid){ 205 | return &redir($r, $url, $GodAuthConfig::FailCookieInvalid); 206 | } 207 | 208 | 209 | ######################################################### 210 | # 211 | # 5) exit now for authed 212 | # 213 | 214 | if (ref $allow ne 'ARRAY'){ 215 | if ($allow eq 'authed'){ 216 | 217 | return Apache2::Const::OK; 218 | } 219 | } 220 | 221 | 222 | ######################################################### 223 | # 224 | # 5) now we need to match usernames and/or roles 225 | # 226 | 227 | # get arrayref of allowed roles 228 | unless (ref $allow eq 'ARRAY'){ 229 | $allow = [$allow]; 230 | } 231 | 232 | # get arrayref of our roles 233 | my $matches = [$cookie_user]; 234 | for my $role(split /,/, $cookie_roles){ 235 | if ($role ne '_'){ 236 | push @{$matches}, 'role:'.$role; 237 | } 238 | } 239 | 240 | 241 | for my $a (@{$allow}){ 242 | for my $b (@{$matches}){ 243 | 244 | if ($a eq $b){ 245 | return Apache2::Const::OK; 246 | } 247 | } 248 | } 249 | 250 | 251 | # 252 | # send the user to the not-on-list page 253 | # 254 | 255 | return &redir($r, $url, $GodAuthConfig::FailNotOnList); 256 | } 257 | 258 | ############################################################################################################## 259 | 260 | sub redir { 261 | my ($r, $ref, $url) = @_; 262 | 263 | $ref = &urlencode('http://'.$ref); 264 | $url .= ($url =~ /\?/) ? "&ref=$ref" : "?ref=$ref"; 265 | 266 | $r->headers_out->set('Location', $url); 267 | return Apache2::Const::REDIRECT; 268 | } 269 | 270 | ############################################################################################################## 271 | 272 | sub xlog { 273 | return unless $GodAuthConfig::LogFile; 274 | open F, '>>'.$GodAuthConfig::LogFile; 275 | print F $_[0]; 276 | close F; 277 | } 278 | 279 | ############################################################################################################## 280 | 281 | sub parse_cookie_jar { 282 | my ($jar) = @_; 283 | 284 | return {} unless defined $jar; 285 | 286 | my @bits = split /;\s*/, $jar; 287 | my $out = {}; 288 | for my $bit (@bits){ 289 | my ($k, $v) = split '=', $bit, 2; 290 | $k = &urldecode($k); 291 | $v = &urldecode($v); 292 | $out->{$k} = $v; 293 | } 294 | return $out; 295 | } 296 | 297 | ############################################################################################################## 298 | 299 | sub urldecode { 300 | $_[0] =~ s!\+! !g; 301 | $_[0] =~ s/%([a-fA-F0-9]{2,2})/chr(hex($1))/eg; 302 | return $_[0]; 303 | } 304 | 305 | sub urlencode { 306 | $_[0] =~ s!([^a-zA-Z0-9-_ ])! sprintf('%%%02x', ord $1) !gex; 307 | $_[0] =~ s! !+!g; 308 | return $_[0]; 309 | } 310 | 311 | ############################################################################################################## 312 | 313 | sub reload_config { 314 | open F, "/usr/local/wwwGodAuth/GodAuthConfig.pm"; 315 | my $data = ''; 316 | while (){ 317 | $data .= $_; 318 | } 319 | close F; 320 | eval $data; 321 | } 322 | 323 | ############################################################################################################## 324 | 325 | 1; 326 | 327 | -------------------------------------------------------------------------------- /mod_perl/GodAuthConfig.pm: -------------------------------------------------------------------------------- 1 | package GodAuthConfig; 2 | 3 | our $LogFile = '/usr/local/wwwGodAuth/auth_log.txt'; 4 | our $CookieName = 'ga'; 5 | our $CookieSecret = '07a8789e03e21147e09b69f21cd38a8b'; 6 | our $FailCookieOld = 'http://auth.myapp.com/login/?fail=old'; 7 | our $FailCookieFuture = 'http://auth.myapp.com/login/?fail=future'; 8 | our $FailCookieInvalid = 'http://auth.myapp.com/login/?fail=invalid'; 9 | our $FailNotOnList = 'http://auth.myapp.com/status/?fail=notonlist'; 10 | our $FailNeedsAuth = 'http://auth.myapp.com/login/'; 11 | our $FailConfig = 'http://auth.myapp.com/?fail=unknownconfig'; 12 | 13 | 14 | # 15 | # the first matching rule is used, so put sub-folders before 16 | # the root! 17 | # 18 | 19 | our $PermMap = [ 20 | 21 | 22 | # 23 | # URLs with no auth 24 | # 25 | 26 | { 27 | url => qr!^www\.myapp\.com/!, 28 | who => 'all', 29 | }, 30 | 31 | 32 | # 33 | # URLs that require a role 34 | # 35 | 36 | { 37 | url => qr!^dev\.myapp\.com/!, 38 | who => 'role:staff', 39 | }, 40 | 41 | 42 | # 43 | # URLs only for certain users 44 | # 45 | 46 | { 47 | url => qr!^debug\.myapp\.com/!, 48 | who => 'cal', 49 | }, 50 | 51 | 52 | # 53 | # combinations are fine too 54 | # 55 | 56 | { 57 | url => qr!^debug2\.myapp\.com/!, 58 | who => ['role:devel', 'cal', 'myles'], 59 | }, 60 | 61 | 62 | # 63 | # anyone with a valid auth token 64 | # 65 | 66 | { 67 | url => qr!^debug2\.myapp\.com/!, 68 | who => 'authed', 69 | }, 70 | 71 | ]; 72 | -------------------------------------------------------------------------------- /mod_perl/GodAuthInit.pl: -------------------------------------------------------------------------------- 1 | use lib qw(/usr/local/wwwGodAuth); 2 | 1; 3 | -------------------------------------------------------------------------------- /mod_perl/god_auth.conf: -------------------------------------------------------------------------------- 1 | # change this path to wherever you installed it 2 | PerlRequire /usr/local/wwwGodAuth/GodAuthInit.pl 3 | PerlPostReadRequestHandler GodAuth 4 | --------------------------------------------------------------------------------