Skip to content

Commit

Permalink
add Implicit Grant support
Browse files Browse the repository at this point in the history
the bulk of the work is done in the Net::OAuth2::AuthorizationServer
module[1] so all the work here is adding tests and updating docs
along with the few tweaks in the code to refactor and support the
"token" response_type in the call to /oauth/authorize

[1] payprop/net-oauth2-authorizationserver@8d7727f
  • Loading branch information
leejo committed Aug 31, 2016
1 parent 9dc01b6 commit d3dd41d
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 14 deletions.
3 changes: 3 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
Revision history for Mojolicious-Plugin-OAuth2-Server

0.27 2016-08-31
- add "Implicit Grant" flow (response_type = "token" in call to authorize)

0.26 2016-05-12
- Transfer repo from G3S to Humanstate

Expand Down
2 changes: 2 additions & 0 deletions MANIFEST
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ t/060_jwt_with_revoke.t
t/070_overrides_hash_args.t
t/080_password_grant_basic.t
t/090_password_grant_overrides.t
t/100_implicit_grant_basic.t
t/110_implicit_grant_overrides.t
t/AllTests.pm
2 changes: 1 addition & 1 deletion Makefile.PL
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ WriteMakefile(
LICENSE => 'perl',
PREREQ_PM => {
'Mojolicious' => '5.37',
'Net::OAuth2::AuthorizationServer' => '0.06',
'Net::OAuth2::AuthorizationServer' => '0.08',
'Carp' => 0,
},
BUILD_REQUIRES => {
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Authorization Server / Resource Server with Mojolicious

# VERSION

0.26
0.27

# SYNOPSIS

Expand Down Expand Up @@ -86,6 +86,9 @@ The "Resource Owner Password Credentials Grant" is also implmented, for which
you must pass a hash of users and a jwt\_secret. I would advice against using
this grant flow however, it has merely been added for completion.

The "Implicit Grant" flow is also implemented by passing the response type of
"token" to the autorization route.

The bulk of the functionality is implemented in the [Net::OAuth2::AuthorizationServer](https://metacpan.org/pod/Net::OAuth2::AuthorizationServer)
distribution, you should see that for more comprehensive documentation and
examples of usage.
Expand Down
48 changes: 41 additions & 7 deletions lib/Mojolicious/Plugin/OAuth2/Server.pm
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Authorization Server / Resource Server with Mojolicious
=head1 VERSION
0.26
0.27
=head1 SYNOPSIS
Expand Down Expand Up @@ -86,6 +86,9 @@ The "Resource Owner Password Credentials Grant" is also implmented, for which
you must pass a hash of users and a jwt_secret. I would advice against using
this grant flow however, it has merely been added for completion.
The "Implicit Grant" flow is also implemented by passing the response type of
"token" to the autorization route.
The bulk of the functionality is implemented in the L<Net::OAuth2::AuthorizationServer>
distribution, you should see that for more comprehensive documentation and
examples of usage.
Expand All @@ -100,10 +103,10 @@ use Mojo::URL;
use Net::OAuth2::AuthorizationServer;
use Carp qw/ croak /;

our $VERSION = '0.26';
our $VERSION = '0.27';

my $args_as_hash;
my ( $AuthCodeGrant,$PasswordGrant,$Grant );
my ( $AuthCodeGrant,$PasswordGrant,$ImplicitGrant,$Grant );

=head1 METHODS
Expand Down Expand Up @@ -154,7 +157,7 @@ sub register {
my $Server = Net::OAuth2::AuthorizationServer->new;

# note that access_tokens and refresh_tokens will not be shared between
# the AuthCodeGrant and PasswordGrant objects, so if you need to support
# the various grant type objects, so if you need to support
# both then you *must* either supply a jwt_secret or supply callbacks
$AuthCodeGrant = $Server->auth_code_grant(
( map { +"${_}_cb" => ( $config->{$_} // undef ) } qw/
Expand All @@ -174,6 +177,13 @@ sub register {
%{ $config },
);

$ImplicitGrant = $Server->implicit_grant(
( map { +"${_}_cb" => ( $config->{$_} // undef ) } qw/
verify_client store_access_token verify_access_token
/ ),
%{ $config },
);

$app->routes->get(
$auth_route => sub { _authorization_request( @_ ) },
);
Expand Down Expand Up @@ -210,32 +220,34 @@ sub _authorization_request {
if (
! defined( $client_id )
or ! defined( $type )
or $type ne 'code'
or $type !~ /^(code|token)$/
) {
$self->render(
status => 400,
json => {
error => 'invalid_request',
error_description => 'the request was missing one of: client_id, '
. 'response_type;'
. 'or response_type did not equal "code"',
. 'or response_type did not equal "code" or "token"',
error_uri => '',
}
);
return;
}

$Grant = $AuthCodeGrant;
$Grant = $type eq 'token' ? $ImplicitGrant : $AuthCodeGrant;
$Grant->legacy_args( $self ) if ! $args_as_hash;

my $mojo_url = Mojo::URL->new( $uri );
my ( $res,$error ) = $Grant->verify_client(
client_id => $client_id,
redirect_uri => $uri,
scopes => [ @scopes ],
mojo_controller => $self,
);

if ( $res ) {

if ( ! $Grant->login_resource_owner( mojo_controller => $self ) ) {
$self->app->log->debug( "OAuth2::Server: Resource owner not logged in" );
# call to $resource_owner_logged_in method should have called redirect_to
Expand All @@ -261,6 +273,9 @@ sub _authorization_request {

if ( $res ) {

return _maybe_generate_access_token( $self,$mojo_url,$client_id,$scope,$state )
if $type eq 'token';

$self->app->log->debug( "OAuth2::Server: Generating auth code for $client_id" );
my $auth_code = $Grant->token(
client_id => $client_id,
Expand Down Expand Up @@ -295,6 +310,25 @@ sub _authorization_request {
$self->redirect_to( $mojo_url );
}

sub _maybe_generate_access_token {
my ( $self,$mojo_url,$client,$scope,$state ) = @_;

my $access_token = $Grant->token(
client_id => $client,
scopes => $scope,
type => 'access',
);

# http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
# &state=xyz&token_type=example&expires_in=3600
$mojo_url->query->append( access_token => $access_token );
$mojo_url->query->append( state => $state ) if defined( $state );
$mojo_url->query->append( token_type => 'bearer' );
$mojo_url->query->append( expires_in => $Grant->access_token_ttl );

$self->redirect_to( $mojo_url );
}

sub _access_token_request {
my ( $self ) = @_;

Expand Down
57 changes: 57 additions & 0 deletions t/100_implicit_grant_basic.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!perl

use strict;
use warnings;

use Mojolicious::Lite;
use Test::More;
use FindBin qw/ $Bin /;
use lib $Bin;
use AllTests;

MOJO_APP: {
# plugin configuration
plugin 'OAuth2::Server' => {
jwt_secret => 'foo',
clients => {
1 => {
redirect_uri => 'https://client/cb',
scopes => {
eat => 1,
drink => 0,
sleep => 1,
},
},
},
};

group {
# /api - must be authorized
under '/api' => sub {
my ( $c ) = @_;
return 1 if $c->oauth;
$c->render( status => 401, text => 'Unauthorized' );
return undef;
};

get '/eat' => sub { shift->render( text => "food"); };
};

# /sleep - must be authorized and have sleep scope
get '/api/sleep' => sub {
my ( $c ) = @_;
$c->oauth( 'sleep' )
|| $c->render( status => 401, text => 'You cannot sleep' );

$c->render( text => "bed" );
};
};

AllTests::run({
grant_type => 'token',
skip_revoke_tests => 1, # there is no auth code
});

done_testing();

# vim: ts=2:sw=2:et
93 changes: 93 additions & 0 deletions t/110_implicit_grant_overrides.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!perl

use strict;
use warnings;

use Mojolicious::Lite;
use Test::More;
use FindBin qw/ $Bin /;
use lib $Bin;
use AllTests;

my $VALID_ACCESS_TOKEN;

my $verify_client_sub = sub {
my ( $c,$client_id,$scopes_ref ) = @_;

# in reality we would check a config file / the database to confirm the
# client_id and client_secret match and that the scopes are valid
return ( 0,'invalid_scope' ) if grep { $_ eq 'cry' } @{ $scopes_ref // [] };
return ( 0,'access_denied' ) if grep { $_ eq 'drink' } @{ $scopes_ref // [] };
return ( 0,'unauthorized_client' ) if $client_id ne '1';

# all good
return ( 1,undef );
};


my $store_access_token_sub = sub {
my ( %args ) = @_;

$VALID_ACCESS_TOKEN = $args{access_token};

# again, store stuff in the database
return;
};

my $verify_access_token_sub = sub {
my ( %args ) = @_;

# and here we should check the access code is valid, not expired, and the
# passed scopes are allowed for the access token
return 0 if grep { $_ eq 'sleep' } @{ $args{scopes} // [] };

# this will only ever allow one access token - for the purposes of testing
# that when a refresh token is used the previous access token is revoked
return 0 if $args{access_token} ne $VALID_ACCESS_TOKEN;

my $client_id = 1;

return $client_id;
};

MOJO_APP: {
# plugin configuration
plugin 'OAuth2::Server' => {
args_as_hash => 0,
authorize_route => '/o/auth',
verify_client => $verify_client_sub,
store_access_token => $store_access_token_sub,
verify_access_token => $verify_access_token_sub,
};

group {
# /api - must be authorized
under '/api' => sub {
my ( $c ) = @_;
return 1 if $c->oauth;
$c->render( status => 401, text => 'Unauthorized' );
return undef;
};

get '/eat' => sub { shift->render( text => "food"); };
};

# /sleep - must be authorized and have sleep scope
get '/api/sleep' => sub {
my ( $c ) = @_;
$c->oauth( 'sleep' )
|| $c->render( status => 401, text => 'You cannot sleep' );

$c->render( text => "bed" );
};
};

AllTests::run({
authorize_route => '/o/auth',
grant_type => 'token',
skip_revoke_tests => 1, # there is no auth code
});

done_testing();

# vim: ts=2:sw=2:et
21 changes: 16 additions & 5 deletions t/AllTests.pm
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ sub run {
my %valid_auth_params = (
client_id => 1,
client_secret => 'boo',
response_type => 'code',
response_type => $grant_type eq 'token' ? 'token' : 'code',
redirect_uri => 'https://client/cb',
scope => 'eat',
state => 'queasy',
Expand All @@ -28,12 +28,15 @@ sub run {
my $t = Test::Mojo->new;
my $auth_code;

if ( $grant_type eq 'authorization_code' ) {
if ( $grant_type =~ /(authorization_code|token)/ ) {

my $response_type = $grant_type eq 'token' ? 'token' : 'code';

note( "authorization request" );

note( " ... not authorized (missing params)" );
foreach my $form_params (
{ response_type => 'code', },
{ response_type => $response_type, },
{ client_id => 1 },
) {
$t->get_ok( $auth_route => form => $form_params )
Expand All @@ -42,7 +45,7 @@ sub run {
error => 'invalid_request',
error_description => 'the request was missing one of: client_id, '
. 'response_type;'
. 'or response_type did not equal "code"',
. 'or response_type did not equal "code" or "token"',
error_uri => '',
} )
;
Expand Down Expand Up @@ -75,10 +78,18 @@ sub run {
note( " ... authorized" );
my $location = Mojo::URL->new( $t->tx->res->headers->location );
is( $location->path,'/cb','redirect to right place' );
ok( $auth_code = $location->query->param( 'code' ),'includes code' );

if ( $response_type eq 'token' ) {
ok( $location->query->param( 'access_token' ),'includes access_token' );
is( $location->query->param( 'token_type' ),'bearer','includes token_type' );
} else {
ok( $auth_code = $location->query->param( 'code' ),'includes code' );
}
is( $location->query->param( 'state' ),'queasy','includes state' );
}

return if $grant_type eq 'token';

note( "access token" );

my %valid_token_params = (
Expand Down

0 comments on commit d3dd41d

Please sign in to comment.