———package
Apache2::Authen::OdinAuth;
use
5.006;
use
strict;
use
warnings;
=head1 NAME
Apache2::Authen::OdinAuth - A cookie-based single sign-on module for Apache.
=head1 VERSION
Version 0.5
=cut
our
$VERSION
= 0.5;
use
Crypt::OdinAuth;
use
Apache2::Log;
use
Apache2::RequestRec ();
use
Apache2::RequestUtil;
use
Apache2::ServerUtil ();
use
Apache2::Connection;
use
APR::Table;
use
YAML::XS;
use
Sys::Hostname;
=head1 SYNOPSIS
This module defines an Apache handler for the Odin Authenticator
single sign-on system. The system is based on the GodAuth script,
available at L<http://github.com/exflickr/GodAuth/>.
=head1 USAGE
To make Apache use the handler for authentication, enable mod_perl and
add following directives in apache2.conf:
PerlSetVar odinauth_config /path/to/odin_auth.yml
PerlFixupHandler Apache2::Authen::OdinAuth
The C<PerlSetVar> statement needs to be global; the
C<PerlFixupHandler> statement can be global or occur in a
C<VirtualHost>, C<Directory>, or C<Location> section.
=head2 YAML CONFIG
The handler reads (and automatically reloads if it's older than
C<reload_timeout> seconds) an additional YAML config file. It sets
configures the shared secret, cookie name, authorizer app URL, and
permissions (which are unfortunately regexp-based).
A sample configuration file looks like this:
# Sample config for Apache2::Authen::OdinAuth
permissions:
# URLs no auth
- url: !!perl/regexp ^localhost
who: all
# Require a role
- url: !!perl/regexp ^dev\.myapp\.com
who: role:admin
# Require username
- url: !!perl/regexp ^debug\.myapp\.com/
who: cal
# A list is fine too
- url: !!perl/regexp ^debug2\.myapp\.com/
who:
- role:devel
- cal
- myles
# Allow any authenticated user
- url: !!perl/regexp ^debug3\.myapp\.com/
who: authed
# log_file: /tmp/odin.log
secret: ****************
reload_timeout: 600
need_auth_url: http://example.com/?NA
invalid_cookie_url: http://example.com/?CIU
not_on_list_url: http://example.com/?NOL
cookie: oa
NOTE: The config is better than original GodAuth configuration, but
will probably need to be refactored; it would be best to make it live
inside Apache's configuration. I'm still not sure how to make it
happen in mod_perl.
=cut
{
my
$last_reload_time
= -1;
my
$config_file
=
undef
;
my
$config
=
undef
;
sub
config {
if
(
time
() -
$last_reload_time
> RELOAD_TIMEOUT ) {
$config
= YAML::XS::LoadFile(
$config_file
);
}
$config
;
}
sub
init_config {
my
$r
=
shift
;
$config_file
||=
$r
->server->dir_config(
'odinauth_config'
);
config;
}
}
$| = 1;
sub
handler {
#
# get URL
#
my
$r
=
shift
;
my
$domain
=
$r
->headers_in->{
'Host'
} ||
'UNKNOWN-HOST'
;
my
$path
=
$r
->unparsed_uri;
my
$host
= hostname;
$ENV
{OdinAuth_User} =
''
;
my
$url
=
$domain
.
$path
;
my
$log
=
"OdinAuth :: $url"
;
init_config(
$r
);
#########################################################
#
# 1) check we have a cookie secret
#
config->{
'secret'
} ||=
'nottherightsecret'
;
#########################################################
#
# 1) determine if we need to perform access control for this url
#
my
$allow
=
'none'
;
for
my
$obj
(@{config->{
'permissions'
}}) {
if
(
$url
=~
$obj
->{url}) {
$allow
=
$obj
->{who};
last
;
}
}
$log
.=
" allow:$allow"
;
#########################################################
#
# 2) we might need auth - see if we have a valid cookie
#
my
$cookie_is_invalid
=
'by default'
;
my
$cookie_user
=
'?'
;
my
$cookie_roles
=
'_'
;
my
$cookies
=
&parse_cookie_jar
(
$r
->headers_in->{
'Cookie'
});
my
$cookie
=
$cookies
->{config->{cookie}};
if
(
$cookie
) {
my
(
$user
,
$roles
);
eval
{
(
$user
,
$roles
) =
Crypt::OdinAuth::check_cookie(
config->{secret},
$cookie
,
$r
->headers_in->{
'User-Agent'
});
};
if
( $@ ) {
$cookie_user
=
$user
;
$cookie_roles
=
$roles
;
chomp
(
$cookie_is_invalid
= $@ );
$log
.=
"(invalid cookie: $cookie_is_invalid)"
;
}
else
{
$cookie_is_invalid
=
undef
;
$cookie_user
=
$user
;
$cookie_roles
=
$roles
;
$r
->headers_in->set(
'OdinAuth-User'
,
$cookie_user
);
$r
->headers_in->set(
'OdinAuth-Roles'
,
$cookie_roles
);
$ENV
{OdinAuth_User} =
$cookie_user
;
$ENV
{OdinAuth_Roles} =
$cookie_roles
;
$r
->notes->add(
"OdinAuth_User"
=>
$cookie_user
);
$r
->notes->add(
"OdinAuth_Roles"
=>
$cookie_roles
);
$log
.=
" (valid cookie: $cookie_user $cookie_roles)"
;
}
}
else
{
$log
.=
" (no cookie)"
;
}
$r
->
log
->debug(
$log
);
if
(
$cookie_is_invalid
) {
$r
->
log
->
warn
(
"Invalid cookie for $cookie_user($cookie_roles): $cookie_is_invalid"
);
}
#########################################################
#
# 3) exit now if we got an 'all'
#
if
(
ref
$allow
ne
'ARRAY'
) {
if
(
$allow
eq
'all'
) {
return
Apache2::Const::OK;
}
}
#########################################################
#
# 4) if we don't have a valid cookie, redirect to the auther
#
if
(!
$cookie
) {
return
&redir
(
$r
,
$url
, config->{need_auth_url});
}
if
(
$cookie_is_invalid
) {
return
&redir
(
$r
,
$url
, config->{invalid_cookie_url},
$cookie_is_invalid
);
}
#########################################################
#
# 5) set user; exit now for authed
#
$r
->user(
$cookie_user
);
$r
->subprocess_env(
'REMOTE_USER'
=>
$cookie_user
);
$r
->set_basic_credentials(
$cookie_user
,
'*****'
);
if
(
ref
$allow
ne
'ARRAY'
) {
if
(
$allow
eq
'authed'
) {
return
Apache2::Const::OK;
}
}
#########################################################
#
# 5) now we need to match usernames and/or roles
#
# get arrayref of allowed roles
unless
(
ref
$allow
eq
'ARRAY'
){
$allow
= [
$allow
];
}
# get arrayref of our roles
my
$matches
= [
$cookie_user
];
for
my
$role
(
split
/,/,
$cookie_roles
) {
if
(
$role
ne
'_'
) {
push
@{
$matches
},
'role:'
.
$role
;
}
}
for
my
$a
(@{
$allow
}) {
for
my
$b
(@{
$matches
}) {
if
(
$a
eq
$b
) {
return
Apache2::Const::OK;
}
}
}
#
# send the user to the not-on-list page
#
return
&redir
(
$r
,
$url
, config->{not_on_list_url});
}
sub
redir {
my
(
$r
,
$ref
,
$url
,
$reason
) =
@_
;
$url
.= (
$url
=~ /\?/) ?
"&ref=$ref"
:
"?ref=$ref"
;
$url
.=
'&reason='
.urlencode(
$reason
)
if
$reason
;
$r
->headers_out->set(
'Location'
,
$url
);
return
Apache2::Const::REDIRECT;
}
sub
parse_cookie_jar {
my
(
$jar
) =
@_
;
return
{}
unless
defined
$jar
;
my
@bits
=
split
/;\s*/,
$jar
;
my
$out
= {};
for
my
$bit
(
@bits
) {
my
(
$k
,
$v
) =
split
'='
,
$bit
, 2;
$k
=
&urldecode
(
$k
);
$v
=
&urldecode
(
$v
);
$out
->{
$k
} =
$v
;
}
return
$out
;
}
sub
urldecode {
$_
[0] =~ s!\+! !g;
$_
[0] =~ s/%([a-fA-F0-9]{2,2})/
chr
(
hex
($1))/eg;
return
$_
[0];
}
sub
urlencode {
$_
[0] =~ s!([^a-zA-Z0-9-_ ])!
sprintf
(
'%%%02x'
,
ord
$1) !gex;
$_
[0] =~ s! !+!g;
return
$_
[0];
}
=head1 AUTHOR
Maciej Pasternacki, C<< <maciej at pasternacki.net> >>
=head1 BUGS
Please report any bugs or feature requests to C<bug-apache2-authen-odinauth at rt.cpan.org>, or through
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Apache2-Authen-OdinAuth>. I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.
=head1 SUPPORT
You can find documentation for this module with the perldoc command.
perldoc Apache2::Authen::OdinAuth
You can also look for information at:
=over 4
=item * RT: CPAN's request tracker (report bugs here)
=item * AnnoCPAN: Annotated CPAN documentation
=item * CPAN Ratings
=item * Search CPAN
=back
=head1 ACKNOWLEDGEMENTS
=head1 LICENSE AND COPYRIGHT
Copyright 2012 Maciej Pasternacki.
This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.
See http://dev.perl.org/licenses/ for more information.
=cut
1;
# End of Apache2::Authen::OdinAuth