package Jifty::Plugin::OAuth::Dispatcher; use warnings; use strict; use Jifty::Dispatcher -base; use Net::OAuth::RequestTokenRequest; use Net::OAuth::AccessTokenRequest; use Net::OAuth::ProtectedResourceRequest; use URI::Escape 'uri_unescape'; on POST '/oauth/request_token' => \&request_token; before GET '/oauth/authorize' => \&authorize; on POST '/oauth/authorize' => \&authorize_post; on POST '/oauth/access_token' => \&access_token; on '/oauth/authorized' => run { redirect '/oauth/authorize' }; on GET '/oauth/request_token' => \&invalid_method; on GET '/oauth/access_token' => \&invalid_method; before '*' => \&try_oauth; =head2 abortmsg CODE, MSG Helper function to abort with a debug message. Maybe should be factored into the C procedure? =cut sub abortmsg { my ($code, $msg) = @_; if ($code) { Jifty->log->debug("$code for ".Jifty->web->request->path.": $msg") if defined($msg); abort($code); } elsif (defined $msg) { Jifty->log->debug("OAuth denied for ".Jifty->web->request->path.": $msg"); } } =head2 request_token The consumer wants a request token =cut sub request_token { my @params = qw/consumer_key signature_method signature timestamp nonce version/; set no_abort => 0; my %oauth_params = get_parameters(@params); my $consumer = get_consumer($oauth_params{consumer_key}); my $signature_key = get_signature_key($oauth_params{signature_method}, $consumer); my ($ok, $msg) = $consumer->is_valid_request(@oauth_params{qw/timestamp nonce/}); abortmsg(401, $msg) if !$ok; # Net::OAuth::Request will die hard if it doesn't get everything it wants my $request = eval { Net::OAuth::RequestTokenRequest->new( request_url => Jifty->web->url(path => '/oauth/request_token'), request_method => Jifty->handler->apache->method(), consumer_secret => $consumer->secret, signature_key => $signature_key, map { $_ => $oauth_params{$_} } @params ) }; abortmsg(400, "Unable to create RequestTokenRequest: $@") if $@ || !defined($request); # make sure the signature matches the rest of what the consumer gave us abortmsg(401, "Invalid signature (type: $oauth_params{signature_method}).") unless $request->verify; # ok, everything checks out. send them back a request token # at this point, the only things that could go wrong are: # 1) we've already seen this nonce and timestamp. possibly a replay attack, # so we abort # 2) we tried a bunch of times to create a unique token but failed. abort # because we don't have any other option my $token = Jifty::Plugin::OAuth::Model::RequestToken->new(current_user => Jifty::CurrentUser->superuser); ($ok, $msg) = eval { $token->create(consumer => $consumer); }; abortmsg(401, "Unable to create a Request Token: " . $@ || $msg) if $@ || !$ok; $consumer->made_request(@oauth_params{qw/timestamp nonce/}); set oauth_response => { oauth_token => $token->token, oauth_token_secret => $token->secret }; show 'oauth/response'; } =head2 authorize The user is authorizing (or denying) a consumer's request token =cut sub authorize { my @params = qw/token callback/; abortmsg(403, "Cannot authorize tokens as an OAuthed user") if Jifty->web->current_user->is_oauthed; abortmsg(401, "Must be authenticated to authorize tokens") if !Jifty->web->current_user->id; set no_abort => 1; my %oauth_params = get_parameters(@params); set callback => $oauth_params{callback}; set consumer => 'Some application'; del 'token'; if ($oauth_params{token}) { my $request_token = Jifty::Plugin::OAuth::Model::RequestToken->new(current_user => Jifty::CurrentUser->superuser); $request_token->load_by_cols(token => $oauth_params{token}, authorized => 0); if ($request_token->id) { set consumer => $request_token->consumer; set token => $oauth_params{token}; } } } =head2 authorize_post The user is submitting an AuthorizeRequestToken action =cut sub authorize_post { abortmsg(403, "Cannot authorize tokens as an OAuthed user") if Jifty->web->current_user->is_oauthed; my $result = Jifty->web->response->result("authorize_request_token"); unless ($result && $result->success) { redirect '/oauth/authorize'; } set result => $result; show '/oauth/authorized'; } =head2 access_token The consumer is trying to trade a request token for an access token =cut sub access_token { my @params = qw/consumer_key signature_method signature timestamp nonce token version/; set no_abort => 0; my %oauth_params = get_parameters(@params); my $consumer = get_consumer($oauth_params{consumer_key}); my $signature_key = get_signature_key($oauth_params{signature_method}, $consumer); my ($ok, $msg) = $consumer->is_valid_request(@oauth_params{qw/timestamp nonce/}); abortmsg(401, $msg) if !$ok; # is the request token they're using still valid? my $request_token = Jifty::Plugin::OAuth::Model::RequestToken->new(current_user => Jifty::CurrentUser->superuser); $request_token->load_by_cols(consumer => $consumer, token => $oauth_params{token}); abortmsg(401, "No token found for consumer ".$consumer->name." with key $oauth_params{token}") unless $request_token->id; ($ok, $msg) = $request_token->can_trade_for_access_token; abortmsg(401, "Cannot trade request token for access token: $msg") if !$ok; # Net::OAuth::Request will die hard if it doesn't get everything it wants my $request = eval { Net::OAuth::AccessTokenRequest->new( request_url => Jifty->web->url(path => '/oauth/access_token'), request_method => Jifty->handler->apache->method(), consumer_secret => $consumer->secret, token_secret => $request_token->secret, signature_key => $signature_key, map { $_ => $oauth_params{$_} } @params ) }; abortmsg(400, "Unable to create AccessTokenRequest: $@") if $@ || !defined($request); # make sure the signature matches the rest of what the consumer gave us abortmsg(401, "Invalid signature (type: $oauth_params{signature_method}).") unless $request->verify; my $token = Jifty::Plugin::OAuth::Model::AccessToken->create_from_request_token($request_token); abortmsg(401, "Unable to create an Access Token: " . $@ || $msg) if $@ || !defined($token) || !$ok; $consumer->made_request(@oauth_params{qw/timestamp nonce/}); $request_token->set_used(1); set oauth_response => { oauth_token => $token->token, oauth_token_secret => $token->secret }; show 'oauth/response'; } =head2 try_oauth If this is a protected resource request, see if we can authorize the request with an access token. This is dissimilar to the other OAuth requests because if anything fails, you just don't set a current_user, and then the rest of the dispatcher rules will take care of it. Thus, failure is handled quite differently in this rule. We try to abort as early as possible to make OAuth less of a hit on all requests. =cut sub try_oauth { my @params = qw/consumer_key signature_method signature timestamp nonce token version/; set no_abort => 1; my %oauth_params = get_parameters(@params); for (@params) { abortmsg(undef, "Undefined required parameter: $_"), return if !defined($oauth_params{$_}); } my $consumer = get_consumer($oauth_params{consumer_key}); return if !$consumer->id; abortmsg(undef, "No known consumer with key $oauth_params{consumer_key}"), return unless $consumer->id; my $signature_key = get_signature_key($oauth_params{signature_method}, $consumer); if ($signature_key && ref($signature_key) && !defined($$signature_key)) { abortmsg(undef, "Failed to get signature key."); return; } my ($ok, $msg) = $consumer->is_valid_request(@oauth_params{qw/timestamp nonce/}); abortmsg(undef, $msg), return if !$ok; my $access_token = Jifty::Plugin::OAuth::Model::AccessToken->new(current_user => Jifty::CurrentUser->superuser); $access_token->load_by_cols(consumer => $consumer, token => $oauth_params{token}); abortmsg(undef, "No token found for consumer ".$consumer->name." with key $oauth_params{token}"), return unless $access_token->id; ($ok, $msg) = $access_token->is_valid; abortmsg(undef, "Cannot access protected resources with this access token: $msg"), return if !$ok; # Net::OAuth::Request will die hard if it doesn't get everything it wants my $request = eval { Net::OAuth::ProtectedResourceRequest->new( request_url => Jifty->web->url(path => Jifty->web->request->path), request_method => Jifty->handler->apache->method(), consumer_secret => $consumer->secret, token_secret => $access_token->secret, signature_key => $signature_key, map { $_ => $oauth_params{$_} } @params ) }; abortmsg(undef, "Unable to create ProtectedResourceRequest: $@"), return if $@ || !defined($request); abortmsg(undef, "Invalid signature (type: $oauth_params{signature_method})."), return unless $request->verify; $consumer->made_request(@oauth_params{qw/timestamp nonce/}); my $new_current_user = Jifty->app_class('CurrentUser')->new( id => $access_token->auth_as, ); $new_current_user->is_oauthed(1); $new_current_user->oauth_token($access_token); Jifty->web->temporary_current_user($new_current_user); Jifty->log->info("Consumer " . $consumer->name . " successfully OAuthed as user ". $access_token->auth_as); } =head2 invalid_method This aborts the request with an "invalid HTTP method" response code. =cut sub invalid_method { Jifty->web->response->add_header(Allow => 'POST'); abort(405); } =head2 get_consumer CONSUMER KEY Helper function to load a consumer by consumer key. Will abort if the key is unknown. =cut sub get_consumer { my $key = shift; my $consumer = Jifty::Plugin::OAuth::Model::Consumer->new(current_user => Jifty::CurrentUser->superuser); $consumer->load_by_cols(consumer_key => $key); abortmsg(401, "No known consumer with key $key") unless $consumer->id || get 'no_abort'; return $consumer; } =head2 get_signature_key SIGNATURE METHOD, CONSUMER Figures out the signature key for this consumer. Will abort if the signature method is unsupported, or if the consumer lacks the prerequisites for this signature method. Will return C if the signature key is consumer independent, as is the case for C and C<HMAC-SHA1>. C<RSA-SHA1> depends on the consumer having the C<rsa_key> field. If the signature method is invalid and no_abort is set, it will return a special value of a reference to undef. Yes this sucks but undef already has an important meaning. =cut { my %valid_signature_methods = map { $_ => 1 } qw/PLAINTEXT HMAC-SHA1 /; if (eval { require Crypt::OpenSSL::RSA; 1 }) { $valid_signature_methods{"RSA-SHA1"} = 1; } else { Jifty->log->debug("RSA-SHA1 support for OAuth unavailable: Crypt::OpenSSL::RSA is not installed."); } my %key_field = ('RSA-SHA1' => 'rsa_key'); sub get_signature_key { my ($method, $consumer) = @_; if (!$valid_signature_methods{$method}) { abortmsg(400, "Unsupported signature method requested: $method") unless get 'no_abort'; return \undef; } my $field = $key_field{$method}; # this MUST return undef if the signature method requires no prior key return undef if !defined($field); my $key = $consumer->$field; if (!defined $key) { abortmsg(400, "Consumer does not have necessary field $field required for signature method $method") unless get 'no_abort'; return; } if ($method eq 'RSA-SHA1') { $key = Crypt::OpenSSL::RSA->new_public_key($key); } return $key; } } =head2 get_parameters REQUIRED PARAMETERS This will retrieve all the request paremeters. This gets parameters besides the ones in the OAuth spec, because the signature is based on all such request parameters. Pass in by name all the OAuth-required parameters. Do not include the C<oauth_> prefix. The precedence of parameters, from highest priority to lowest priority, is: =over 4 =item Authorization header =item WWW-Authenticate header =item POST parameters =item GET parameters (aka URL's query string) =back =cut sub get_parameters { my %p; my %params = Jifty->handler->apache->params(); # Check Authorization header my $authz = Jifty->handler->apache->header_in("Authorization"); if ($authz && $authz =~ s/^\s*OAuth\s*//i) { while ($authz =~ m{\s*([%a-zA-Z0-9._~-]+)="([%a-zA-Z0-9._~-]*)"\s*}g) { $params{uri_unescape($1)} = uri_unescape($2); } } for (@_) { $p{$_} = delete $params{"oauth_$_"} if !defined $p{$_}; } $p{version} ||= '1.0'; unless (get 'no_abort') { # check to see if there are any unsupported parameters while (my ($key, undef) = each %params) { abortmsg(400, "Unsupported parameter: $key") if $key =~ /^oauth_/; } # check to see if we're missing anything for (@_) { abortmsg(400, "Undefined required parameter: $_") if !defined($p{$_}); } if ($p{timestamp} && $p{timestamp} !~ /^\d+$/) { abortmsg(400, "Malformed timestamp. Expected positive integer, got $p{timestamp}"); } } return %p; } 1;