package OAuth::Lite::Server::mod_perl2; use strict; use warnings; use Apache2::Connection (); use Apache2::RequestIO (); use Apache2::RequestRec (); use Apache2::RequestUtil (); use Apache2::Response (); use Apache2::URI (); use Apache2::ServerRec (); use URI::Escape (); use Apache2::Const -compile => qw(OK); use List::MoreUtils qw(none any); use bytes (); use OAuth::Lite::Util qw(:all); use OAuth::Lite::ServerUtil; use OAuth::Lite::AuthMethod qw(:all); use OAuth::Lite::Problems qw(:all); use base qw( Class::Accessor::Fast Class::ErrorHandler ); use constant PROTECTED_RESOURCE => 'PROTECTED_RESOURCE'; use constant REQUEST_TOKEN => 'REQUEST_TOKEN'; use constant ACCESS_TOKEN => 'ACCESS_TOKEN'; __PACKAGE__->mk_accessors(qw/request realm oauth xrds_location/); =head1 NAME OAuth::Lite::Server::mod_perl2 - mod_perl2 OAuth server =head1 SYNOPSIS Inherit this class, build your service with mod_perl2. For example, write MyServiceWithOAuth.pm And the source-code of L is nice example. See it. package MyServiceWithOAuth; use base 'OAuth::Lite::Server::mod_perl2'; sub init { my $self = shift; $self->oauth->allow_extra_params(qw/file size/); $self->oauth->support_signature_methods(qw/HMAC-SHA1 PLAINTEXT/); } sub get_request_token_secret { my ($self, $token_string) = @_; my $token = MyDB::Scheme->resultset('RequestToken')->find($token_string); unless ($token && $token->is_authorized_by_user && !$token->is_exchanged_to_access_token && !$token->is_expired) { return $self->error(q{Invalid token}); } return $token->secret; } sub get_access_token_secret { my ($self, $token_string) = @_; my $token = MyDB::Scheme->resultset('AccessToken')->find($token_string); unless ($token && !$token->is_expired) { return $self->error(q{Invalid token}); } return $token->secret; } sub get_consumer_secret { my ($self, $consumer_key) = @_; my $consumer = MyDB::Shceme->resultset('Consumer')->find($consumer_key); unless ($consumer && $consumer->is_valid) { return $self->error(q{Inalid consumer_key}); } return $consumer->secret; } sub publish_request_token { my ($self, $consumer_key, $callback_url) = @_; my $token = OAuth::Lite::Token->new_random; MyDB::Scheme->resultset('RequestToken')->create({ token => $token->token, secret => $token->secret, realm => $self->realm, consumer_key => $consumer_key, expired_on => '', callback => $callback_url, }); return $token; } sub publish_access_token { my ($self, $consumer_key, $request_token_string, $verifier) = @_; my $request_token = MyDB::Scheme->resultset('RequestToken')->find($request_token_string); unless ($request_token && $request_token->is_authorized_by_user && !$request_token->is_exchanged_to_access_token && !$request_token->is_expired && $request_token->has_verifier && $request_token->verifier eq $verifier) { return $self->error(q{Invalid token}); } my $access_token = OAuth::Lite::Token->new_random; MyDB::Scheme->resultset('AccessToken')->create({ token => $request_token->token, realm => $self->realm, secret => $request_token->secret, consumer_key => $consumer_key, author => $request_token->author, expired_on => '', }); $request_token->is_exchanged_to_access_token(1); $request_token->update(); return $access_token; } sub check_nonce_and_timestamp { my ($self, $consumer_key, $nonce, $timestamp) = @_; my $request_log = MyDB::Scheme->resultset('RequestLog'); # check against replay-attack my $count = $request_log->count({ consumer_key => $consumer_key, -nest => [ nonce => $nonce, timestamp => { '>' => $timestamp }, ], }); if ($count > 0) { return $self->error(q{Invalid timestamp or consumer}); } # save new request log. $request_log->create({ consumer_key => $consumer_key, nonce => $nonce, timestamp => $timestamp, }); return 1; } sub service { my $self = shift; } in httpd.conf PerlSwitches -I/var/www/MyApp/lib PerlModule MyServiceWithOAuth ServerName api.example.com DocumentRoot /var/www/MyApp/root PerlSetVar Realm "http://api.example.com/picture" SetHandler perl-script PerlSetVar Mode REQUEST_TOKEN PerlResponseHandler MyServiceWithOAuth SetHandler perl-script PerlSetVar Mode ACCESS_TOKEN PerlResponseHandler MyServiceWithOAuth SetHandler perl-script PerlSetVar Mode PROTECTED_RESOURCE PerlResponseHandler MyServiceWithOAuth =head1 DESCRIPTION This module is for mod_perl2 PerlResponseHandler, and allows you to build services with OAuth easily. =head1 TUTORIAL All you have to do is to make a package inheritting this module, and override some methods, and in httpd.conf file, write three configuration, each configuration needs to be set Mode value. The each value must be REQUEST_TOKEN, ACCESS_TOKEN, or PROTECTED_RESOURCE. And the Realm value is needed for each resource. The methods you have to override is bellow. =head1 METHODS YOU HAVE TO OVERRIDE =head2 init In this method, you can do some initialization. For example, set what signature method your service supports, and what extra-param is allowed. sub init { my $self = shift; $self->oauth->support_signature_method(qw/HMAC-SHA1 PLAINTEXT/); $self->oauth->allow_extra_params(qw/file size/); } =head2 get_request_token_secret($token_string) In this method, you should check if the request-token-string is valid, and returns token-secret value corresponds to the token value passed as argument. If the token is invalid, you should call 'error' method. =head2 get_access_token_secret($token_string) In this method, you should check if the access-token-string is valid, and returns token-secret value corresponds to the token value passed as argument. If the token is invalid, you should call 'error' method. =head2 get_consumer_secret($consumer_key) In this method, you should check if the consumer_key is valid, and returns consumer_secret value corresponds to the consumer_key passed as argument. If the consumer is invalid, you should call 'error' method. =head2 check_nonce_and_timestamp($consumer_key, $nonce, $timestamp) Check passed nonce and timestamp. Among requests the consumer send service-provider, there shouldn't be same nonce, and new timestamp should be greater than old ones. If they are valid, returns 1, or returns 0. =head2 publish_request_token($consumer_key, $callback_url) Create new request-token, and save it, and returns it as L object. =head2 publish_access_token($consumer_key, $request_token_string, $verifier) If the passed request-token is valid, create new access-token, and save it, and returns it as L object. And disables the exchanged request-token. =head2 verify_requestor_approval($consumer_key, $requestor_id) When the request is for OpenSocial Reverse Phone Home, Check if the requestor has already given approval to consumer to access the requestor's data. =head2 service Handle protected resource. This method should returns Apache2::Const::OK. sub service { my $self = shift; my $params = $self->{params}; my $token_string = $params->{oauth_token}; my $access_token = MyDB::Scheme->resultset('RequestToken')->find($token_string); my $user = $access_token->author; my $resource = $user->get_my_some_resource(); $self->request->status(200); $self->request->content_type(q{text/html; charset=utf-8}); $self->print($resource); return Apache2::Const::OK; } =head1 API =head2 handler Trigger method as response handler. =head2 new Constructor =head2 request Returns Apache request object. See L, L, and etc... $self->request; =head2 realm The realm value you set in httpd.conf by PerlSetVar. =head2 oauth Returns l object. =head2 allow_extra_param =head2 allow_extra_params =head2 support_signature_method =head2 support_signature_methods These methods are just only delegate methods. For example, $self->allow_extra_param('foo'); is same as $self->oauth->allow_extra_param('foo'); =head2 request_method Request method (Upper Case). When the raw request method is POST and X-HTTP-Method-Override is define in header, return the value of X-HTTP-Method-Override. =head2 request_uri Returns request uri =head2 request_body Requets body data when the request's http-method is POST or PUT =head2 set_authenticate_header Set proper 'WWW-Authentication' response header =head2 is_required_request_token Check if current request requires request-token. =head2 is_required_access_token Check if current request requires access-token. =head2 is_required_protected_resource Check if current request requires protected-resource. =head2 is_consumer_request Chekc if the server accepts consumer-request and the request is for protected resource without token. =head2 is_reverse_phone_home Check if the server accepts open-social reverse-phone-home and the requests is for protected resource without token. =head2 xrds_location If you want to support OAuth Discovery, you need to prepare XRDS document, and set the location as XRDSLocation value. See below. PerlSetVar Mode PROTECTED_RESOURCE PerlSetVar XRDSLocation "http://myservice/discovery/xrdsdocument" PerlResponseHandler MyServiceWithOAuth Then you can get this url in your script. sub service { my $self = shift; my $xrds_location = $self->xrds_location; } But normalry all you have to do is write location on httpd.conf. And "errout" method automatically put it into response header properly. =head2 build_xrds In case client send request which includes application/xrds+xml in Accept header, if The server is set XRDSLocation as above, return resuponse with it in header. But you can also directly return XRDS-Document. Override build_xrds document. sub build_xrds { my $self = shift; my $xrds = q{ ... }; return $xrds; } If the server doesn't support both XRDSLocation and build_xrds overriding, The server doesn't support OAuth Discovery. =head2 accepts_consumer_request You can adopt OAuth Consumer Request 1.0. See http://oauth.googlecode.com/svn/spec/ext/consumer_request/1.0/drafts/1/spec.html To adopt this spec, you have to set var 'AcceptConsumerRequest' on httpd.conf PerlSetVar Mode PROTECTED_RESOURCE PerlSetVar AcceptConsumerRequest 1 PerlResponseHandler MyServiceWithOAuth Then override service method for protected resource. sub service { my $self = shift; my $params = $self->{params}; my $resource_owner_id; if (exists $params->{oauth_token}) { my $access_token_value = $params->{oauth_token}; $resource_owner_id = $self->get_user_id_of_access_token($access_token_value); } else { my $consumer_key = $params->{oauth_consumer_key}; $resource_owner_id = $self->get_user_id_of_consumer_developer($consumer_key); } my @resources = MyDB::Scheme->resultset('SomeResource')->search({ user_id => $resource_owner_id, }); # output resource data in the manner your api defines. ... return Apache2::Const::OK; } =head2 accepts_reverse_phone_home You can adopt OpenSocial Reverse Phone Home. PerlSetVar Mode PROTECTED_RESOURCE PerlSetVar AcceptReversePhoneHome 1 PerlResponseHandler MyServiceWithOAuth =head2 error L method. In some check-method, when you find invalid request value, call this method with error message and return it. sub check_nonce_and_timestamp { my ($self, $consumer_key, $nonce, $timestamp) = @_; if ($timestamp ...) { return $self->error(q{Invalid timestamp}); } return 1; } =head2 errstr L method. You can get error message that you set with error method. my $valid = $self->check_nonce_and_timestamp($consumer_key, $nonce, $timestamp); if (!$valid) { return $self->errout(401, $self->errstr); } =head2 output(%params) Simply output response. You can set 3 params, code, type and content. return $self->output( code => 200, type => 'text/plain; charset=utf-8' content => 'success', ); =head2 errout($code, $message) Output error message. This returns Apache2::Const::OK, so, don't forget 'return'; return $self->errout(400, q{Bad request}); And you can override this and put some function into this process. For example, logging. sub errout { my ($self, $code, $message) = @_; $self->my_log_process($code, $message); return $self->SUPER::errout($code, $message); } sub my_log_process { my ($self, $code, $message) = @_; warn ... } =cut sub handler : method { my $class = shift; my $server = $class->new(@_); return $server->__service(); } sub new { my $class = shift; my $r = shift; my $self = bless { request => $r, oauth => OAuth::Lite::ServerUtil->new( strict => 0 ), realm => undef, secure => 0, mode => PROTECTED_RESOURCE, accepts_consumer_request => 0, accepts_reverse_phone_home => 0, params => {}, completed_validation => 0, }, $class; my $realm = $self->request->dir_config('Realm'); $self->{realm} = $realm if $realm; my $accept_cr = $self->request->dir_config('AcceptConsumerRequest'); $self->{accepts_consumer_request} = 1 if $accept_cr; my $accept_rp = $self->request->dir_config('AcceptReversePhoneHome'); if ($accept_rp) { $self->{accepts_reverse_phone_home} = 1; $self->allow_extra_param('xoauth_requestor_id'); } my $mode = $self->request->dir_config('Mode'); my @valid_modes = (PROTECTED_RESOURCE, REQUEST_TOKEN, ACCESS_TOKEN); if ($mode) { if (none { $mode eq $_ } @valid_modes) { die "Invalid mode."; } else { $self->{mode} = $mode; } } my $xrds_location = $self->request->dir_config('XRDSLocation'); $self->{xrds_location} = $xrds_location if $xrds_location; if ( ($INC{'Apache2/ModSSL.pm'} && $r->connection->is_https) || ($r->subprocess_env('HTTPS') && $r->subprocess_env('HTTPS') eq 'ON') ) { $self->{secure} = 1; } $self->init(@_); $self; } sub init { my ($self, %args) = @_; } sub request_body { my $self = shift; unless (defined $self->{_request_body}) { my $length = $self->request->headers_in->{'Content-Length'} || 0; my $body = ""; while ($length) { $self->request->read( my $buffer, ($length < 8192) ? $length : 8192 ); $length -= bytes::length($buffer); $body .= $buffer; } $self->{_request_body} = $body; } $self->{_request_body}; } sub request_uri { my $self = shift; return $self->request->uri; } sub request_method { my $self = shift; unless (defined $self->{_request_method}) { my $method = uc($self->request->method); my $x_method = uc($self->request->headers_in->{'X-HTTP-Method-Override'} || ''); if ($method eq 'POST' && ($x_method eq 'PUT' || $x_method eq 'DELETE')) { $self->{_request_method} = $x_method; } else { $self->{_request_method} = $method; } } return $self->{_request_method}; } sub __service { my $self = shift; my $realm; my $params = {}; my $is_authorized = 0; my $needs_to_check_token = ( $self->is_required_request_token || ( $self->is_required_protected_resource && ($self->accepts_consumer_request || $self->accepts_reverse_phone_home) ) ) ? 0 : 1; my $authorization = $self->request->headers_in->{Authorization}; if ($authorization && $authorization =~ /^\s*OAuth/) { ($realm, $params) = parse_auth_header($authorization); } if ( $self->request_method() eq 'POST' && $self->request->headers_in->{'Content-Type'} =~ m!application/x-www-form-urlencoded!) { for my $pair (split /&/, $self->request_body) { my ($key, $value) = split /=/, $pair; $params->{$key} = decode_param($value); } } for my $pair (split /&/, $self->request->args) { my ($key, $value) = split /=/, $pair; $params->{$key} = decode_param($value); } unless ($self->oauth->validate_params($params, $needs_to_check_token)) { return $self->errout(400, $self->oauth->errstr); } $self->{params} = $params; my $consumer_key = $params->{oauth_consumer_key}; my $timestamp = $params->{oauth_timestamp}; my $nonce = $params->{oauth_nonce}; my $consumer_secret = $self->get_consumer_secret($consumer_key); unless (defined $consumer_secret) { return $self->errout(401, $self->errstr||CONSUMER_KEY_UNKNOWN); } $self->check_nonce_and_timestamp($consumer_key, $nonce, $timestamp) or return $self->errout(400, $self->errstr||TIMESTAMP_REFUSED); my $request_uri = $self->__build_request_uri(); if ($self->is_required_request_token) { $self->oauth->verify_signature( method => $self->request_method(), params => $params, url => $request_uri, consumer_secret => $consumer_secret, ) or return $self->errout(401, $self->oauth->errstr||SIGNATURE_INVALID); my $callback_url = $params->{oauth_callback}; my $request_token = $self->publish_request_token($consumer_key, $callback_url) or return $self->errout(401, $self->errstr); return $self->__output_token($request_token); } elsif ($self->is_required_access_token) { my $token_value = $params->{oauth_token}; my $token_secret = $self->get_request_token_secret($token_value); unless (defined $token_secret) { return $self->errout(401, $self->errstr||TOKEN_REJECTED); } $self->oauth->verify_signature( method => $self->request_method(), params => $params, url => $request_uri, consumer_secret => $consumer_secret || '', token_secret => $token_secret || '', ) or return $self->errout(401, $self->oauth->errstr||SIGNATURE_INVALID); my $verifier = $params->{oauth_verifier} || ''; my $access_token = $self->publish_access_token($consumer_key, $token_value, $verifier) or return $self->errout(401, $self->errstr); return $self->__output_token($access_token); } else { my $token_secret = ''; if (exists $params->{oauth_token}) { my $token_value = $params->{oauth_token}; $token_secret = $self->get_access_token_secret($token_value); unless (defined $token_secret) { return $self->errout(401, $self->errstr||TOKEN_REJECTED); } } $self->oauth->verify_signature( method => $self->request_method(), params => $params, url => $request_uri, consumer_secret => $consumer_secret || '', token_secret => $token_secret, ) or return $self->errout(401, $self->oauth->errstr||SIGNATURE_INVALID); if ($self->is_reverse_phone_home) { $self->verify_requestor_approval($consumer_key, $params->{xoauth_requestor_id}) or return $self->errout(401, q{No approval}); } $self->{completed_validation} = 1; return $self->service($params); } } sub __build_request_uri { # borrowed from Catalyst::Engine::Apache my $self = shift; my $scheme = $self->{secure} ? 'https' : 'http'; my $host = $self->request->hostname || 'localhost'; my $port = $self->request->get_server_port; PROXY_CHECK: { last PROXY_CHECK unless $self->request->headers_in->{'X-Forwarded-Host'}; $host = $self->request->headers_in->{'X-Forwarded-Host'}; if ( $host =~ /^(.+):(\d+)$/ ) { $host = $1; $port = $2; } else { $port = $self->{secure} ? 443 : 80; } } my $base_path = ''; my $location = $self->request->location; if ( $location && $location ne '/' ) { $base_path = $location; } if ($host !~ /:/ ) { $host .= ":$port"; } my ($path) = split /\?/, $self->request->unparsed_uri; if ( $base_path =~ m/[$URI::uric]/o ) { my $match = qr/($base_path)/; my ($base_match) = $path =~ $match; $base_path = $base_match; } $path =~ s{^/+}{}; $base_path = '/' unless length $base_path; if ( $base_path ne '/' && $base_path ne $path && $base_path =~ m{/$path} ) { $path = $base_path; $path =~ s{^/+}{}; } my $request_uri = $scheme . '://' . $host . '/' . $path; return $request_uri; } sub __output_token { my ($self, $token) = @_; my $token_string = $token->as_encoded; return $self->output( code => 200, type => q{text/plain; charset=utf-8}, content => $token_string, ); } sub is_consumer_request { my $self = shift; return ($self->is_required_protected_resource && $self->accepts_consumer_request && !exists $self->{params}{oauth_token}) ? 1 : 0; } sub is_required_request_token { my $self = shift; return ($self->{mode} eq REQUEST_TOKEN) ? 1 : 0; } sub is_required_access_token { my $self = shift; return ($self->{mode} eq ACCESS_TOKEN) ? 1 : 0; } sub is_required_protected_resource { my $self = shift; return ($self->{mode} eq PROTECTED_RESOURCE) ? 1 : 0; } sub accepts_consumer_request { my $self = shift; return $self->{accepts_consumer_request}; } sub accepts_reverse_phone_home { my $self = shift; return $self->{accepts_reverse_phone_home}; } sub service { my ($self, $params) = @_; } sub get_request_token_secret { my ($self, $token) = @_; my $secret; return $secret; } sub get_access_token_secret { my ($self, $token) = @_; my $secret; return $secret; } sub get_consumer_secret { my ($self, $consumer_key); my $consumer_secret; return $consumer_secret; } sub publish_request_token { my ($self, $consumer_key, $callback_url) = @_; my $token = OAuth::Lite::Token->new; return $token; } sub publish_access_token { my ($self, $request_token_string, $verifier) = @_; # validate request token # and publish access token # return $token; my $token = OAuth::Lite::Token->new; return $token; } sub check_nonce_and_timestamp { my ($self, $consumer_key, $timestamp, $nonce) = @_; return $self->error(q{Invalid Consumer}); return $self->error(q{Invalid Timestamp}); return $self->error(q{Invalid Nonce}); return 1; } sub set_authenticate_header { my $self = shift; my $problem = shift; my %params = (); $params{realm} = $self->realm if $self->realm; $params{oauth_problem} = $problem if $problem; my $header = "OAuth " . join(", ", map sprintf(q{%s="%s"}, $_, $params{$_}), keys %params); $self->request->err_headers_out->add('WWW-Authenticate', $header); } sub _check_if_request_accepts_xrds { my $self = shift; unless (defined $self->{__request_accepts_xrds}) { my $accept = $self->request->headers_in->{Accept} || ''; my @types = map { (split ";", $_)[0] } split /\*s,\*s/, $accept; if (any { $_ eq q{application/xrds+xml} } @types) { $self->{__request_accepts_xrds} = 1; } else { $self->{__request_accepts_xrds} = 0; } } return $self->{__request_accepts_xrds}; } sub build_xrds { my $self = shift; return; } sub errout { my ($self, $code, $message, $description) = @_; if ( ( $self->request_method() eq 'GET' || $self->request_method() eq 'HEAD') && $self->is_required_protected_resource && $self->_check_if_request_accepts_xrds ) { if ($self->xrds_locaton) { $self->request->status(200); $self->request->err_headers_out->add('X-XRDS-Location' => $self->xrds_location); return Apache2::Const::OK; } elsif ($self->request_method() eq 'GET' && (my $xrds = $self->build_xrds())) { return $self->output( code => 200, type => q{application/xrds+xml}, content => $xrds, ); } } my $problem; if (OAuth::Lite::Problems->match($message)) { $problem = $message; $message = sprintf(q{oauth_problem=%s}, $message); } $self->set_authenticate_header($problem); return $self->output( code => $code, type => q{text/plain; charset=utf-8}, content => $message, ); } sub output { my $self = shift; my %args = @_; my $code = $args{code} || 200; my $type = $args{type} || q{text/plain; charset=utf-8}; my $content = $args{content} || ''; $self->request->status($code); if ($content) { $self->request->content_type($type); $self->request->set_content_length(bytes::length($content)); $self->request->print($content); } return Apache2::Const::OK; } sub allow_extra_param { my $self = shift; $self->oauth->allow_extra_param(@_); } sub allow_extra_params { my $self = shift; $self->oauth->allow_extra_params(@_); } sub support_signature_method { my $self = shift; $self->oauth->support_signature_method(@_); } sub support_signature_methods { my $self = shift; $self->oauth->support_signature_methods(@_); } sub is_reverse_phone_home { my $self = shift; return ( $self->is_required_protected_resource && $self->accepts_reverse_phone_home && !exists $self->{params}{oauth_token} && exists $self->{params}{xoauth_requestor_id}) ? 1 : 0 } sub verify_requestor_approval { my $self = shift; my ($consumer_key, $user_id) = @_; return 1; } =head1 SEE ALSO L L =head1 AUTHOR Lyo Kato, C =head1 COPYRIGHT AND LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.6 or, at your option, any later version of Perl 5 you may have available. =cut 1;