Jifty::Plugin::OAuth::Dispatcher


Jifty-Plugin-OAuth documentation Contained in the Jifty-Plugin-OAuth distribution.

Index


Code Index:

abortmsg CODE, MSG

Helper function to abort with a debug message. Maybe should be factored into the abort procedure?

request_token

The consumer wants a request token

authorize

The user is authorizing (or denying) a consumer's request token

authorize_post

The user is submitting an AuthorizeRequestToken action

access_token

The consumer is trying to trade a request token for an access token

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.

invalid_method

This aborts the request with an "invalid HTTP method" response code.

get_consumer CONSUMER KEY

Helper function to load a consumer by consumer key. Will abort if the key is unknown.

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 undef if the signature key is consumer independent, as is the case for PLAINTEXT and HMAC-SHA1. RSA-SHA1 depends on the consumer having the 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.

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 oauth_ prefix.

The precedence of parameters, from highest priority to lowest priority, is:

Authorization header
WWW-Authenticate header
POST parameters
GET parameters (aka URL's query string)

Jifty-Plugin-OAuth documentation Contained in the Jifty-Plugin-OAuth distribution.
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';

# Let the app do auth, etc, before we let these rules run
after app,
  before      '/oauth/authorized'  => redirect '/oauth/authorize';
after app, 
  before GET  '/oauth/authorize'   => \&authorize;
on       POST '/oauth/authorize'   => \&authorize_post;

on     GET  '/oauth/access_token'  => \&invalid_method;
on     POST '/oauth/access_token'  => \&access_token;
on     GET  '/oauth/request_token' => \&invalid_method;
on     POST '/oauth/request_token' => \&request_token;

before '*' => \&try_oauth;

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");
    }
}

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';
}

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};
        }
    }
}

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';
}

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';
}

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);
}

sub invalid_method {
    Jifty->web->response->add_header(Allow => 'POST');
    abort(405);
}

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;
}

{
    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;
    }
}

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;