Skip to content
Permalink
Browse files

proof of concept for OAuth2 support in PAUSE

this is using the pause_2017 code as that runs on Mojolicious so makes
it easy to use the Plugin for OAuth2 server support - note that some of
things are hardcoded and should be moved into the private config, namely
the JWT secrets and the client whitelist (with secrets)

note that this needs to be moved into its own standalone server, but we
can just take the existing app_2017.psgi as a base and strip out all the
stuff we don't need, moving the Web controller here into an API
app/controller

the currently functionality is just to allow users to login using PAUSE
from whitelisted third parties, an exampe flow is as follows using the
current aforementioned hardcoded secrets:

1) third party redirects user to PAUSE to get an auth code (at which point
the user may or may not need to log in):

    https://192.168.56.1/pause/authenquery/oauth/authorize?client_id=ACT&response_type=code&redirect_uri=https://www.thirdparty.com

2) PAUSE redirects back to the thirdparty with an authorisation code:

    https://www.thirdparty.com/?code=eyJwMmMiOjUwMDAsImFsZyI6IlBCRVMyLUhTNTEyK0EyNTZLVyIsInAycyI6IkJ3YU9FLTE1QkUzT2VGcFlCMHlxOVEiLCJlbmMiOiJBMTI4R0NNIn0.cSG8otV75tinTQh_kHmhkGq1IWT450a9.WKznzE66QEJlBk5x.k82I_PjjtjLQ4PD_Lj8cl0QQhZbwsyYjxFW-yaRPvHwrgCbQIwVASSWSNBxsyCPRqzSP_hVHvdCT6K9UCC9nyrPJSjWKSZzgYynBribIhgHfP8S10xEyBm3h-pVoBh6yxyCngF6esoocc4JyaYM1tb-N9nF8FHdcOXjOaYgzRl3m7vpTEycHOaTRzas5euHAZL_chmHQLFachX3UlY9knvYk3drB5G224V4.d8U_FzMe3QkgP_GsQwHv8A

3) thirdparty does a *BACKGROUND POST* to get an access token using the
authorisation code and their client secret:

     curl -X POST -k -v 'https://192.168.56.1/pause/oauth/access_token' -d 'code=eyJwMmMiOjUwMDAsImFsZyI6IlBCRVMyLUhTNTEyK0EyNTZLVyIsInAycyI6IkJ3YU9FLTE1QkUzT2VGcFlCMHlxOVEiLCJlbmMiOiJBMTI4R0NNIn0.cSG8otV75tinTQh_kHmhkGq1IWT450a9.WKznzE66QEJlBk5x.k82I_PjjtjLQ4PD_Lj8cl0QQhZbwsyYjxFW-yaRPvHwrgCbQIwVASSWSNBxsyCPRqzSP_hVHvdCT6K9UCC9nyrPJSjWKSZzgYynBribIhgHfP8S10xEyBm3h-pVoBh6yxyCngF6esoocc4JyaYM1tb-N9nF8FHdcOXjOaYgzRl3m7vpTEycHOaTRzas5euHAZL_chmHQLFachX3UlY9knvYk3drB5G224V4.d8U_FzMe3QkgP_GsQwHv8A&client_id=ACT&client_secret=some_strong_client_secret&grant_type=authorization_code&redirect_uri=https://www.thirdparty.com'

4) a JSON struct is returned with access and refresh tokens:

	{"access_token":"eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJwMmMiOjUwMDAsInAycyI6Ikd5eHlnUlBYODM3VVRuMS1lTkNYV3ciLCJlbmMiOiJBMTI4R0NNIn0.u_LE8MSwRuwj4y0I8JZf_rh8wmC81AW-.C82o8GEXtT-SqQxI.PfpIh_HpxWH0I6BhWrUSMiSFmrwCJkEtcZiGeHTiYkw7XOEX7Wk_oEEGbDVoRYIZkYmTHyPG3KLmf-PvNK-nBk1swmanrpldNoRXwnKllG0OiiLymxrLYQweoI-1reJ0dw92PdBTOI7AVYbuSSBMwwOYJnuDiHoIfcgsvEUzwEKihDtRIqchuZF3820gvqr9KW40mQ.cMrkblxtZdMaGTU3qQLzVw","expires_in":86400,"refresh_token":"eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJwMmMiOjUwMDAsInAycyI6Ii1hdTgyZmUxc2RDczdVcmdjanQ5TXciLCJlbmMiOiJBMTI4R0NNIn0.W0GJXGWHj3-XPLcbBCH_Ewrk5EaIIC5N._O5W1P77p07YPIS3.YX9Bb16KU3fwqNb--FYgmCugbzacr4dRIZD__qKVMwgUsmAtkvQmLt6qfsyH9J7yMpQp4pO7G1BUcOLg6eLLS8MEBF3TFwtP4xssSNBxSr8eC1amtW4C-x2zz-noiRXDHTCiG9X0oN7oBwLb2-S22ljjqez9rTd_xWuBz34DE5eqvgtE.hsN_rM-K2AbLqkHEcDIy9Q","token_type":"Bearer"}

5) the access token can be used to query PAUSE to get the user details:

    curl -k -v -H"Authorization: Bearer eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJwMmMiOjUwMDAsInAycyI6Ikd5eHlnUlBYODM3VVRuMS1lTkNYV3ciLCJlbmMiOiJBMTI4R0NNIn0.u_LE8MSwRuwj4y0I8JZf_rh8wmC81AW-.C82o8GEXtT-SqQxI.PfpIh_HpxWH0I6BhWrUSMiSFmrwCJkEtcZiGeHTiYkw7XOEX7Wk_oEEGbDVoRYIZkYmTHyPG3KLmf-PvNK-nBk1swmanrpldNoRXwnKllG0OiiLymxrLYQweoI-1reJ0dw92PdBTOI7AVYbuSSBMwwOYJnuDiHoIfcgsvEUzwEKihDtRIqchuZF3820gvqr9KW40mQ.cMrkblxtZdMaGTU3qQLzVw" https://192.168.56.1/pause/api/me

    {
      "email": "lee....@.......com",
      "homepage": "leejo.github.io",
      "pause_id": "LEEJO"
    }
  • Loading branch information...
leejo committed Apr 26, 2019
1 parent 75750ee commit 0fdb10a2d911f8815ef02a76b0e10f50c6e82ce9
@@ -33,6 +33,7 @@ requires 'Module::Signature';
requires 'MojoX::Log::Dispatch::Simple';
requires 'Mojolicious';
requires 'Mojolicious::Plugin::WithCSRFProtection';
requires 'Mojolicious::Plugin::OAuth2::Server';
requires 'Net::SSLeay', '1.49';
requires 'Parse::CPAN::Packages';
requires 'Parse::CPAN::Perms';
@@ -3,6 +3,7 @@ package PAUSE::Web;
use Mojo::Base "Mojolicious";
use MojoX::Log::Dispatch::Simple;
use Digest::SHA1 qw/sha1_hex/;
use HTTP::Status qw/:constants status_message/;

has pause => sub { Carp::confess "requires PAUSE::Web::Context" };

@@ -85,6 +86,27 @@ sub startup {
}
}
}

# note that we define the /oauth/authorize route before we install
# the plugin to avoid it defining it first (FIFO) - we use the same
# route in the plugin setup to avoid it defining an alternate route
$private->get("/oauth/authorize")->to("api-user#oauth_authorize");
$app->plugin("PAUSE::Web::Plugin::OAuth2Server");

# API/OAuth2
my $api = $app->routes->under("/api")->to(
cb => sub {
my ( $c ) = @_;
return 1 if $c->oauth;
$c->render(
status => HTTP_UNAUTHORIZED,
json => { error => 'Bad credentials' },
);
return;
}
);

$api->get("/me")->to("api-user#me");
}

sub _log {
@@ -0,0 +1,52 @@
package PAUSE::Web::Controller::Api::User;

use Mojo::Base "Mojolicious::Controller";
use HTTP::Status qw/:constants status_message/;

sub me {
my ( $c ) = @_;

my $oauth_details = $c->oauth;
my $user_id = $oauth_details->{user_id};

my $mgr = $c->app->pause;
my $dbh = $mgr->connect;
my $query = qq{
SELECT userid, email, homepage
FROM users
WHERE userid = ?
};

my $sth = $dbh->prepare($query);
$sth->execute( $user_id );

my ( $u_id,$email,$web ) = $sth->fetchrow_array;

return $c->render( json => {
pause_id => $u_id,
email => $email,
homepage => $web || undef,
} );
}

sub oauth_authorize {
my ( $c ) = @_;

my $u = $c->active_user_record;

my $redirect_uri = $c->oauth2_auth_request({
user_id => $u->{userid},
});

if ( $redirect_uri ) {
return $c->redirect_to( $redirect_uri );
}

# something didn't work, e.g. bad client, scopes, etc
my $error = "Failed to generate a redirect_uri for oauth_authorize";
$c->app->pause->log({level => 'error', message => $error });
$c->res->code(HTTP_INTERNAL_SERVER_ERROR);
return $c->reply->exception($error);
}

1;
@@ -0,0 +1,38 @@
package PAUSE::Web::Plugin::OAuth2Server;

use Mojo::Base "Mojolicious::Plugin";
use YAML::Syck;
use Encode;

sub register {
my ($self, $app, $conf) = @_;

$app->plugin(
'OAuth2::Server' => {
# authorize route falls under /authenquery to make sure user
# is logged in or is asked to log in
authorize_route => '/authenquery/oauth/authorize',

# access token route doesn't fall under /authenquery as it will
# use the oauth2 auth code and client secret for authentication
access_token_route => '/oauth/access_token',
args_as_hash => 1,

# FIXME - the following values need to be set in the private
# config (or eventually - database?)
access_token_ttl => 60 * 60 * 24, # 1 day
jwt_secret => 'some_strong_secret_key',
jwt_algorithm => 'PBES2-HS512+A256KW',

clients => {
ACT => {
client_secret => "some_strong_client_secret",
}
}
},
);


}

1;
@@ -0,0 +1,98 @@
use Mojo::Base -strict;
use FindBin;
use lib "$FindBin::Bin/lib";
use Test::PAUSE::Web;
use HTTP::Status qw/:constants/;
use JSON::PP;
use Test::Deep;
use utf8;

Test::PAUSE::Web->setup;

my $common_qparams = "response_type=code&redirect_uri=https://foo.com";
my $test = Test::PAUSE::Web->tests_for('user');
my ($path, $user) = @$test;
my $t = Test::PAUSE::Web->new(user => $user);

# we're testing the redirect content, so disable the user agent's
# automatic handling of them so we can inspect the redirect
$t->{mech}->requests_redirectable( [] );

subtest 'unknown client' => sub {

my $res = $t->get(
"/pause/authenquery/oauth/authorize"
. "?client_id=bad_client_id"
. "&$common_qparams"
);

is(
$res->header( 'location' ),
'https://foo.com?error=unauthorized_client',
'redirect location has error'
);
};

my $access_token;

subtest 'known client' => sub {

my $res = $t->get(
"/pause/authenquery/oauth/authorize"
. "?client_id=ACT"
. "&$common_qparams"
);

like(
$res->header( 'location' ),
qr!https://foo\.com\?code=(.*?)!,
'redirect location has code'
);

my $auth_code = ( split( 'code=',$res->header( 'location' ) ) )[1];

$res = $t->post_ok(
"/pause/oauth/access_token",
{
code => $auth_code,
client_id => 'ACT',
client_secret => 'some_strong_client_secret',
grant_type => 'authorization_code',
redirect_uri => 'https://foo.com',
}
);

cmp_deeply(
my $json = decode_json( $res->content ),
{
access_token => re( '.+' ),
refresh_token => re( '.+' ),
expires_in => 86400,
token_type => 'Bearer',
},
'request for access token'
);

$access_token = $json->{access_token};
};

subtest 'get user info' => sub {

$t->{mech}->requests_redirectable( [] );
my $res = $t->get(
"/pause/api/me",
Authorization => "Bearer $access_token",
);

cmp_deeply(
my $json = decode_json( $res->content ),
{
pause_id => 'TESTUSER',
email => 'pause_admin@localhost.localdomain',
homepage => undef,
},
'JSON struct',
);
};

done_testing;

0 comments on commit 0fdb10a

Please sign in to comment.
You can’t perform that action at this time.