├── 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 |
--------------------------------------------------------------------------------