mod_perl2を使ってシングルサインオン認証を書いてみた - end0tknr's kipple - web写経開発
上記entryの 2021年版です。 前回は、ブラウザでのアクセスを想定し、ログイン画面を持っていましたが、 今回は、スマホアプリ等からのREST APIアクセスを想定し、画面を持っていません。
1. 構成と、書く処理の流れ
1.1 login
┌───────────────────┐ │user │ └┬──────────────────┘ ↓①login request ↑ ④auth ticket ┌─────────┴─────────┐ │今回の ApacheSSO │ └┬──────────────────┘ ↓②ip/pw check ↑ ③check result ┌─────────┴─────────┐ │app server │ └───────────────────┘
1.2 認証中の処理
┌───────────────────┐ │user │ └┬──────────────────┘ ↓①login request ┌───────────────────┐ │今回の ApacheSSO │ └┬──────────────────┘ ↓②reverse proxy ┌───────────────────┐ │app server │ └───────────────────┘
1.3 logout
認証ticket cookieを削除するのみ
┌───────────────────┐ │user │ └┬──────────────────┘ ↓①logout ┌───────────────────┐ │今回の ApacheSSO │ └───────────────────┘
2. 各種src
2.1 httpd.conf
DocumentRoot "/home/end0tknr/html_pub" LoadModule perl_module modules/mod_perl.so PerlRequire "/home/end0tknr/apache_sso/startup.pl" #認証が必要な部分 <Directory /home/end0tknr/html_pri> AuthType ApacheSSO AuthName member PerlAuthenHandler ApacheSSO->authen PerlAuthzHandler ApacheSSO->authz require valid-user </Directory> Alias /pri /home/end0tknr/html_pri #login実行(ticket発行) <Location /login> AuthType ApacheSSO AuthName member SetHandler perl-script PerlHandler ApacheSSO->login </Location> #logout実行(ticket無効化) <Location /logout> AuthType ApacheSSO AuthName member SetHandler perl-script PerlHandler ApacheSSO->logout </Location>
2.2 startup.pl
#!/usr/local/bin/perl BEGIN { use lib qw(. /home/end0tknr/apache_sso/lib ); } use ApacheSSO; my $AUTH_CONF = {crypt_key_file => '/home/end0tknr/apache_sso/key_aes.txt', public_key_file => '/home/end0tknr/apache_sso/public-key.txt', private_key_file =>'/home/end0tknr/apache_sso/private-key.txt', cookie =>{domain =>'cent7.a5.jp', path =>'/', expires => undef, # expires => '+1d', #refer to CGI.pm document secure =>0 }, remote_login_api=> 'http://localhost/pub' }; ApacheSSO::set_common_auth_conf('member',$AUTH_CONF); 1;
2.3 ApacheSSO.pm
package ApacheSSO; #Single Sign On use strict; use warnings; use utf8; use APR::Table; use Apache2::Access (); use Apache2::Const qw(FORBIDDEN OK REDIRECT SERVER_ERROR); use Apache2::URI (); use CGI; use CGI::Cookie; use CGI::Util; use Crypt::CBC; use Crypt::OpenSSL::RSA; #ticketへの署名やverifyに使用 use Crypt::OpenSSL::AES; #ticketの可逆暗号化に使用 use LWP::UserAgent; use MIME::Base64; use Data::Dumper; #for debug my $AUTH_CONF = {}; my $AUTH_COOKIE_HEADER = __PACKAGE__; sub set_common_auth_conf { my ($auth_name,$conf) = @_; $AUTH_CONF->{$auth_name}->{cookie} = $conf->{cookie}; local $/ = undef; #record読込のdelimiter my $fh; # 以降は、各鍵のload. open($fh, '<', $conf->{crypt_key_file}) or die "can't open $conf->{crypt_key_file} $!"; my $aes_key = substr(<$fh>,0,32); #blocksize=16bytesという制限を回避する為、Crypt::CBCを経由 $AUTH_CONF->{$auth_name}->{crypt_key} = Crypt::CBC->new(-key=>$aes_key, -cipher=>"Crypt::OpenSSL::AES", -pbkdf=>'pbkdf2', -nodeprecate=>1); close($fh) or die "can't close crypt_key_file $!"; open($fh, '<', $conf->{public_key_file}) or die "can't open $conf->{public_key_file} $!"; $AUTH_CONF->{$auth_name}->{public_key} = Crypt::OpenSSL::RSA->new_public_key(<$fh>); close($fh) or die "can't close public_key_file $!"; open($fh, '<', $conf->{private_key_file}) or die "can't open $conf->{private_key_file} $!"; $AUTH_CONF->{$auth_name}->{private_key} = Crypt::OpenSSL::RSA->new_private_key(<$fh>); close($fh) or die "can't close private_key_file $!"; } #ticket発行と署名 sub encode_ticket { my ($self,$auth_name,$user_id,$remote_ip) = @_; my $data = join(":", $user_id, $remote_ip, time()); my $rsa_pri = $AUTH_CONF->{$auth_name}->{private_key}; my $signature = encode_base64($rsa_pri->sign($data),''); #署名 my $data_sign = join("\t",$data,$signature); my $cipher = $AUTH_CONF->{$auth_name}->{crypt_key}; $data_sign = encode_base64( $cipher->encrypt($data_sign),'' ); return $data_sign; } #ticketの署名を検証 sub decode_ticket { my ($self,$auth_name,$ticket) = @_; my $cipher = $AUTH_CONF->{$auth_name}->{crypt_key}; my $data_sign = $cipher->decrypt(decode_base64($ticket)); my ($data,$signature) = split(/\t/,$data_sign); $signature = decode_base64($signature); my $rsa_pub = $AUTH_CONF->{$auth_name}->{public_key}; unless ( $rsa_pub->verify($data, $signature) ){ print STDERR "invalid data (can't verify)"; return undef; } return split(/:/,$data); } # 暗号化ticketから、user情報を抽出することで、authen(認証)とします sub authen { my ($self,$req) = @_; my $auth_name = $req->auth_name; #Apache2::Access my $cookie_name = join('_',$AUTH_COOKIE_HEADER,$auth_name); my $cookies_str = $req->headers_in->get("Cookie") || ""; #APR::Table my %cookies = CGI::Cookie->parse($cookies_str); my $ticket; if ($cookies{$cookie_name}) { $ticket = $cookies{$cookie_name}->value; } else { return FORBIDDEN; } my ($user,$ip) = $self->decode_ticket($auth_name, $ticket); if ($user) { $req->user($user); #これで $ENV{REMOTE_USER}によるuser_idが参照可 return OK; } return FORBIDDEN; } # authen() で、暗号化ticketから、user情報抽出済の為 # authz(認可)では特に何を行いません。 sub authz { my ($self,$req) = @_; return OK; } #user_id,passwordを検証し、認証cookie ticketを発行 sub login { my ($self, $req) = @_; my $auth_name = $req->auth_name; my $cookie_name = join('_',$AUTH_COOKIE_HEADER,$auth_name); my $cookie_conf = $AUTH_CONF->{$auth_name}->{cookie}; my $args = CGI->new($req)->Vars(); for my $arg_key (keys %$args){ $args->{$arg_key} =~ s/^\s+//o; #左trim $args->{$arg_key} =~ s/\s+$//o; #右trim } my $uid = $self->check_passwd($auth_name,$args->{uid},$args->{passwd}); if( not $uid ){ my $cookie = new CGI::Cookie(-name => $cookie_name, -value=> '', -expires=>0, -domain =>$cookie_conf->{domain}, -path =>$cookie_conf->{path}, -secure =>$cookie_conf->{secure}); return FORBIDDEN; } # remote_ip を使用できなくなっていましたので、useragent_ip へ置換. my $ticket = $self->encode_ticket($auth_name,$uid,$req->useragent_ip); # $self->encode_ticket($auth_name,$uid,$req->connection->remote_ip); my $cookie = new CGI::Cookie(-name => $cookie_name, -value=> $ticket, -expires=>$cookie_conf->{expires}, -domain =>$cookie_conf->{domain}, -path =>$cookie_conf->{path}, -secure =>$cookie_conf->{secure}); $req->err_headers_out->add("Set-Cookie" => $cookie); # print STDERR $ticket; return OK; } sub check_passwd { my ($self,$auth_name,$uid,$passwd) = @_; my $ua = LWP::UserAgent->new; $ua->timeout(10); my $payload = { uid => $uid, passwd => $passwd }; my $res = $ua->post($AUTH_CONF->{remote_login_api}, $payload ); if( ! $res->is_success ){ #print $res->as_string; return $uid; } return undef; } # 暗号化ticketであるcookieを削除することで、logoutとします. sub logout { my ($self,$req) = @_; my $auth_name = $req->auth_name; my $cookie_name = join('_',$AUTH_COOKIE_HEADER,$auth_name); my $cookie_conf = $AUTH_CONF->{$auth_name}->{cookie}; my $cookie = new CGI::Cookie(-name => $cookie_name, -value=> '', -expires=>0, -domain =>$cookie_conf->{domain}, -path =>$cookie_conf->{path}, -secure =>$cookie_conf->{secure}); $req->err_headers_out->add("Set-Cookie" => $cookie); return OK; } 1; __END__
2.4 各鍵
key_aes.txt
1234567890ABCDEF1234567890ABCDEF
private-key.txt
-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgQC+dlvKyOzo2XFoRD9u2CKIV/vXrDP2NbBoTp9fr9kNCkakWnZs x19nXXl11F+p1Uc9ayO2O+ylTsJSiSoo3OGM+mK2X905lRXiX1ZlY8OgCDpBR0qf PNFGKzd9RisssUIyCoPogF1+lR68Ghr1odLr6X2ISxdFm13POz0SSAO5GQIBAwKB gH75kocwnfCQ9kWC1PSQFwWP/TpyzU7OdZrfFOp1O14G2cLm+Z3aP5o+UPk4P8aO L35HbSQn8xjfLDcGHBs967IpzIiPMGdBlgZAlPaXvx2QU+GYHRS0Uta/31q2+veC fO3ftZMVplXWdw12mNzbF1N1EEvEFYGNex2iEKmE6KKrAkEA8zPoWgQqOwt0G3oP 2rqbkS7OMrQOnhiHjTF1ekn7m/1qEjkyEi+Sc6rzxIonBOD/bHN3QE5dSyPSo9eZ k6CngQJBAMh8AS8QdHepZOL846VqWB6NoLBq8ZA8fH42/7lovdGJYyi6Wc20a1/B NCOrg1RO701xn2SopfwBUCRMem0GHZkCQQCiIprmrXF8sk1nprU8fGe2HzQhzV8U EFpeIPj8MVJn/ka20MwMH7b3x00tsW9Ylf+dok+AND4yF+HCj7u3wG+rAkEAhagA ygr4T8ZDQf3tGPGQFF5rIEdLtX2oVCSqe5spNluXcHw73nhHlSt4F8es4t9KM6EU 7cXD/VY1bYhRngQTuwJACwzEez5Hi0jDK5NY+czEyVlc08rMhkvloyU4vpHFmjiK gxqVsgoWZYPTsDlXtRbiEIkjXr2fFsO7hHi/kNMZfw== -----END RSA PRIVATE KEY-----
public-key.txt
-----BEGIN PUBLIC KEY----- MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC+dlvKyOzo2XFoRD9u2CKIV/vX rDP2NbBoTp9fr9kNCkakWnZsx19nXXl11F+p1Uc9ayO2O+ylTsJSiSoo3OGM+mK2 X905lRXiX1ZlY8OgCDpBR0qfPNFGKzd9RisssUIyCoPogF1+lR68Ghr1odLr6X2I SxdFm13POz0SSAO5GQIBAw== -----END PUBLIC KEY-----