Net-OAuth2-AuthorizationServer-0.28/000755 000770 000120 00000000000 13750017447 020613 5ustar00leejohnsonadmin000000 000000 Net-OAuth2-AuthorizationServer-0.28/Changes000644 000770 000120 00000010422 13750017120 022071 0ustar00leejohnsonadmin000000 000000 Revision history for Net-OAuth2-AuthorizationServer 0.28 2020-11-02 - Handle lack of token in Authorization header (GH #27) 0.27 2020-09-02 - Update example w/r/t recent(ish) changes in callback return expectations 0.26 2020-07-20 - Allow access_token_ttl to be passed as callback 0.25 2020-05-06 - Add "FURTHER READING" section to Manual - Audit code from "OAuth 2.0 Security Best Current Practice" draft - The above states "clients SHOULD NOT use the implicit grant" - The above states "The resource owner password credentials grant MUST NOT be used" - Add some documentation to note the above, with links - The above draft also reveals: - PKCE will be required (https://tools.ietf.org/html/rfc7636) - "authorization codes MUST be invalidated by the AS after their first use at the token endpoint" - "configured to return an AS identitifier [sic] ("iss") as a non-standard parameter" - "Authorization server MUST utilize ... methods to detect refresh token replay" 0.24 2019-12-09 - Remove hard dependency on Mojo::JWT (GH #26, with thanks to ap) 0.23 2019-06-04 - Fix examples to work with recent version of deps (GH #23, GH #25) 0.22 2019-04-27 - Add support for JWEs as well as JWTs (GH #24) - Fix make sure user_id is returned in AuthorizationCodeGrant defaults 0.20 2019-03-01 - Fix example oauth2_client.pl (GH #23) 0.19 2018-12-01 - Avoid returning from the try/catch block as this never works (GH #20, GH #21, thanks to Dylan William Hardison) 0.18 2018-05-17 - Fix a couple of typos and path issues revealed by Debian package built linter (GH #18, GH #17, with thanks to Mirko Tietge) 0.17 2018-04-16 - Handle inconsistencies between various grant types and the return data from ->verify_token_and_scope sometimes returning a hash ref and sometimes returning a string - now they always return a hash ref in the case of a successful authentication (GH #16) - Note that this may be a BREAKING CHANGE if you are using password grant in your app - Thanks to sillitoe for the above find + suggestions on a fix 0.16 2017-09-01 - Correct return type from verification of refresh token when the refresh token is a JWT (GH #12, thanks to pierre-vigier) 0.15 2017-05-12 - Add support for jwt_claims_cb in call to ->token to allow the override or addition of claims to the JWT 0.14 2017-03-03 - Additions and changes for handling modification of scopes, many thanks to Martin Renvoize for patches and assistance with this - Add scopes to returned information from from verify_client (GH #5) this will allow modification of requested scopes, which can be then passed back through other callbacks - Add document response_type in verify_client (GH #5) - Fix catch missing client_id in _verify_client callback 0.13 2016-10-15 - Remove undocumented legacy_args flag 0.12 2016-10-15 - Deprecate undocumented legacy_args flag 0.11 2016-09-16 - Add more documentation to Net::OAuth2::AuthorizationServer::Manual 0.10 2016-09-15 - Add Net::OAuth2::AuthorizationServer::ClientCredentialsGrant - Add more documentation to Net::OAuth2::AuthorizationServer::Manual 0.09 2016-08-31 - Fix clients with a client_secret must use Authorization Code flow and not Implicit Grant flow - Fix pass redirect_uri and response_type to verify_client cb so correct validation can be done for above fix 0.08 2016-08-31 - Add Net::OAuth2::AuthorizationServer::ImplicitGrant 0.07 2016-05-12 - Transfer repo from G3S to Humanstate 0.06 2016-04-17 - Add Net::OAuth2::AuthorizationServer::PasswordGrant - Add Net::OAuth2::AuthorizationServer::Manual 0.03 2016-04-11 - First release, broken out of Mojolicious::Plugin::OAuth2::Server for better abstraction and decoupling from the Mojolicious framework. Should also allow tidying up of method args and easier additions of other OAtuth2 grant types Net-OAuth2-AuthorizationServer-0.28/MANIFEST000644 000770 000120 00000003142 13750017447 021744 0ustar00leejohnsonadmin000000 000000 Changes MANIFEST Makefile.PL README.md examples/OAuth2Functions.pm examples/README examples/oauth2_client.pl examples/oauth2_db.json examples/oauth2_schema.sql examples/oauth2_server_db.pl examples/oauth2_server_realistic.pl examples/oauth2_server_simple.pl examples/oauth2_server_simple_jwt.pl lib/Net/OAuth2/AuthorizationServer.pm lib/Net/OAuth2/AuthorizationServer/AuthorizationCodeGrant.pm lib/Net/OAuth2/AuthorizationServer/ClientCredentialsGrant.pm lib/Net/OAuth2/AuthorizationServer/Defaults.pm lib/Net/OAuth2/AuthorizationServer/ImplicitGrant.pm lib/Net/OAuth2/AuthorizationServer/Manual.pod lib/Net/OAuth2/AuthorizationServer/PasswordGrant.pm t/001_compiles_pod.t t/003_changes.t t/net/oauth2/authorizationserver.t t/net/oauth2/authorizationserver/authorizationcodegrant.t t/net/oauth2/authorizationserver/authorizationcodegrant_no_jwt.t t/net/oauth2/authorizationserver/authorizationcodegrant_tests.pm t/net/oauth2/authorizationserver/clientcredentialsgrant.t t/net/oauth2/authorizationserver/clientcredentialsgrant_no_jwt.t t/net/oauth2/authorizationserver/clientcredentialsgrant_tests.pm t/net/oauth2/authorizationserver/defaults.t t/net/oauth2/authorizationserver/implicitgrant.t t/net/oauth2/authorizationserver/implicitgrant_no_jwt.t t/net/oauth2/authorizationserver/implicitgrant_tests.pm t/net/oauth2/authorizationserver/passwordgrant.t t/net/oauth2/authorizationserver/passwordgrant_no_jwt.t t/net/oauth2/authorizationserver/passwordgrant_tests.pm META.yml Module YAML meta-data (added by MakeMaker) META.json Module JSON meta-data (added by MakeMaker) Net-OAuth2-AuthorizationServer-0.28/t/000755 000770 000120 00000000000 13750017447 021056 5ustar00leejohnsonadmin000000 000000 Net-OAuth2-AuthorizationServer-0.28/META.yml000644 000770 000120 00000002040 13750017447 022060 0ustar00leejohnsonadmin000000 000000 --- abstract: 'Easier implementation of an OAuth2 Authorization Server' author: - 'Lee Johnson ' build_requires: Test::Exception: '0.32' Test::Most: '0' configure_requires: ExtUtils::MakeMaker: '0' dynamic_config: 1 generated_by: 'ExtUtils::MakeMaker version 7.36, CPAN::Meta::Converter version 2.150010' license: perl meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: Net-OAuth2-AuthorizationServer no_index: directory: - t - inc requires: Carp: '0' Crypt::JWT: '0.023' CryptX: '0.021' MIME::Base64: '0' Moo: '2.000002' Time::HiRes: '0' Try::Tiny: '0.22' Types::Standard: '1.000005' perl: '5.010001' resources: bugtracker: https://github.com/Humanstate/net-oauth2-authorizationserver/issues homepage: https://metacpan.org/module/Net::OAuth2::AuthorizationServer license: http://dev.perl.org/licenses/ repository: https://github.com/Humanstate/net-oauth2-authorizationserver version: '0.28' x_serialization_backend: 'CPAN::Meta::YAML version 0.018' Net-OAuth2-AuthorizationServer-0.28/META.json000644 000770 000120 00000003222 13750017447 022233 0ustar00leejohnsonadmin000000 000000 { "abstract" : "Easier implementation of an OAuth2 Authorization Server", "author" : [ "Lee Johnson " ], "dynamic_config" : 1, "generated_by" : "ExtUtils::MakeMaker version 7.36, CPAN::Meta::Converter version 2.150010", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Net-OAuth2-AuthorizationServer", "no_index" : { "directory" : [ "t", "inc" ] }, "prereqs" : { "build" : { "requires" : { "Test::Exception" : "0.32", "Test::Most" : "0" } }, "configure" : { "requires" : { "ExtUtils::MakeMaker" : "0" } }, "runtime" : { "requires" : { "Carp" : "0", "Crypt::JWT" : "0.023", "CryptX" : "0.021", "MIME::Base64" : "0", "Moo" : "2.000002", "Time::HiRes" : "0", "Try::Tiny" : "0.22", "Types::Standard" : "1.000005", "perl" : "5.010001" } } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/Humanstate/net-oauth2-authorizationserver/issues" }, "homepage" : "https://metacpan.org/module/Net::OAuth2::AuthorizationServer", "license" : [ "http://dev.perl.org/licenses/" ], "repository" : { "url" : "https://github.com/Humanstate/net-oauth2-authorizationserver" } }, "version" : "0.28", "x_serialization_backend" : "JSON::PP version 4.02" } Net-OAuth2-AuthorizationServer-0.28/README.md000644 000770 000120 00000006756 13750017172 022103 0ustar00leejohnsonadmin000000 000000 # NAME Net::OAuth2::AuthorizationServer - Easier implementation of an OAuth2 Authorization Server
Build Status Coverage Status
# VERSION 0.28 # SYNOPSIS my $Server = Net::OAuth2::AuthorizationServer->new; my $Grant = $Server->$grant_type( ... ); # DESCRIPTION This module is the gateway to the various OAuth2 grant flows, as documented at [https://tools.ietf.org/html/rfc6749](https://tools.ietf.org/html/rfc6749). Each module implements a specific grant flow and is designed to "just work" with minimal detail and effort. Please see [Net::OAuth2::AuthorizationServer::Manual](https://metacpan.org/pod/Net::OAuth2::AuthorizationServer::Manual) for more information on how to use this module and the various grant types. You should use the manual in conjunction with the grant type module you are using to understand how to override the defaults if the "just work" mode isn't good enough for you. # GRANT TYPES ## auth\_code\_grant OAuth Authorisation Code Grant as document at [http://tools.ietf.org/html/rfc6749#section-4.1](http://tools.ietf.org/html/rfc6749#section-4.1). See [Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant](https://metacpan.org/pod/Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant). ## implicit\_grant OAuth Implicit Grant as document at [https://tools.ietf.org/html/rfc6749#section-4.2](https://tools.ietf.org/html/rfc6749#section-4.2). See [Net::OAuth2::AuthorizationServer::ImplicitGrant](https://metacpan.org/pod/Net::OAuth2::AuthorizationServer::ImplicitGrant). ## password\_grant OAuth Resource Owner Password Grant as document at [http://tools.ietf.org/html/rfc6749#section-4.3](http://tools.ietf.org/html/rfc6749#section-4.3). See [Net::OAuth2::AuthorizationServer::PasswordGrant](https://metacpan.org/pod/Net::OAuth2::AuthorizationServer::PasswordGrant). ## client\_credentials\_grant OAuth Client Credentials Grant as document at [http://tools.ietf.org/html/rfc6749#section-4.4](http://tools.ietf.org/html/rfc6749#section-4.4). See [Net::OAuth2::AuthorizationServer::ClientCredentialsGrant](https://metacpan.org/pod/Net::OAuth2::AuthorizationServer::ClientCredentialsGrant). # SEE ALSO [Mojolicious::Plugin::OAuth2::Server](https://metacpan.org/pod/Mojolicious::Plugin::OAuth2::Server) - A Mojolicious plugin using this module [Crypt::JWT](https://metacpan.org/pod/Crypt::JWT) - encode/decode JWTs # AUTHOR & CONTRIBUTORS Lee Johnson - `leejo@cpan.org` With contributions from: Martin Renvoize - `martin.renvoize@ptfs-europe.com` Pierre VIGIER - `pierre.vigier@gmail.com` Ian Sillitoe - [https://github.com/sillitoe](https://github.com/sillitoe) Mirko Tietgen - [mirko@abunchofthings.net](https://metacpan.org/pod/mirko@abunchofthings.net) Dylan William Hardison - [dylan@hardison.net](https://metacpan.org/pod/dylan@hardison.net) # LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. If you would like to contribute documentation or file a bug report then please raise an issue / pull request: https://github.com/Humanstate/net-oauth2-authorizationserver Net-OAuth2-AuthorizationServer-0.28/examples/000755 000770 000120 00000000000 13750017447 022431 5ustar00leejohnsonadmin000000 000000 Net-OAuth2-AuthorizationServer-0.28/lib/000755 000770 000120 00000000000 13750017447 021361 5ustar00leejohnsonadmin000000 000000 Net-OAuth2-AuthorizationServer-0.28/Makefile.PL000644 000770 000120 00000002234 13573372741 022572 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; use ExtUtils::MakeMaker; WriteMakefile( NAME => 'Net::OAuth2::AuthorizationServer', ABSTRACT_FROM => 'lib/Net/OAuth2/AuthorizationServer.pm', VERSION_FROM => 'lib//Net/OAuth2/AuthorizationServer.pm', AUTHOR => 'Lee Johnson ', LICENSE => 'perl', PREREQ_PM => { 'Moo' => '2.000002', 'Types::Standard' => '1.000005', 'MIME::Base64' => 0, 'Time::HiRes' => 0, 'Carp' => 0, 'CryptX' => '0.021', 'Try::Tiny' => '0.22', 'Crypt::JWT' => '0.023', }, BUILD_REQUIRES => { 'Test::Most' => 0, 'Test::Exception' => 0.32, }, META_MERGE => { requires => { perl => '5.010001' }, resources => { license => 'http://dev.perl.org/licenses/', homepage => 'https://metacpan.org/module/Net::OAuth2::AuthorizationServer', bugtracker => 'https://github.com/Humanstate/net-oauth2-authorizationserver/issues', repository => 'https://github.com/Humanstate/net-oauth2-authorizationserver' }, }, test => { RECURSIVE_TEST_FILES => 1, }, ); # vim: ts=4:sw=4:et Net-OAuth2-AuthorizationServer-0.28/lib/Net/000755 000770 000120 00000000000 13750017447 022107 5ustar00leejohnsonadmin000000 000000 Net-OAuth2-AuthorizationServer-0.28/lib/Net/OAuth2/000755 000770 000120 00000000000 13750017447 023211 5ustar00leejohnsonadmin000000 000000 Net-OAuth2-AuthorizationServer-0.28/lib/Net/OAuth2/AuthorizationServer/000755 000770 000120 00000000000 13750017447 027240 5ustar00leejohnsonadmin000000 000000 Net-OAuth2-AuthorizationServer-0.28/lib/Net/OAuth2/AuthorizationServer.pm000644 000770 000120 00000007267 13750017136 027605 0ustar00leejohnsonadmin000000 000000 package Net::OAuth2::AuthorizationServer; =head1 NAME Net::OAuth2::AuthorizationServer - Easier implementation of an OAuth2 Authorization Server =for html Build Status Coverage Status =head1 VERSION 0.28 =head1 SYNOPSIS my $Server = Net::OAuth2::AuthorizationServer->new; my $Grant = $Server->$grant_type( ... ); =head1 DESCRIPTION This module is the gateway to the various OAuth2 grant flows, as documented at L. Each module implements a specific grant flow and is designed to "just work" with minimal detail and effort. Please see L for more information on how to use this module and the various grant types. You should use the manual in conjunction with the grant type module you are using to understand how to override the defaults if the "just work" mode isn't good enough for you. =cut use strict; use warnings; use Moo; use Types::Standard qw/ :all /; use Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant; use Net::OAuth2::AuthorizationServer::ImplicitGrant; use Net::OAuth2::AuthorizationServer::PasswordGrant; use Net::OAuth2::AuthorizationServer::ClientCredentialsGrant; our $VERSION = '0.28'; =head1 GRANT TYPES =head2 auth_code_grant OAuth Authorisation Code Grant as document at L. See L. =cut sub auth_code_grant { my ( $self, @args ) = @_; return Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant->new( @args ); } =head2 implicit_grant OAuth Implicit Grant as document at L. See L. =cut sub implicit_grant { my ( $self, @args ) = @_; return Net::OAuth2::AuthorizationServer::ImplicitGrant->new( @args ); } =head2 password_grant OAuth Resource Owner Password Grant as document at L. See L. =cut sub password_grant { my ( $self, @args ) = @_; return Net::OAuth2::AuthorizationServer::PasswordGrant->new( @args ); } =head2 client_credentials_grant OAuth Client Credentials Grant as document at L. See L. =cut sub client_credentials_grant { my ( $self, @args ) = @_; return Net::OAuth2::AuthorizationServer::ClientCredentialsGrant->new( @args ); } =head1 SEE ALSO L - A Mojolicious plugin using this module L - encode/decode JWTs =head1 AUTHOR & CONTRIBUTORS Lee Johnson - C With contributions from: Martin Renvoize - C Pierre VIGIER - C Ian Sillitoe - L Mirko Tietgen - L Dylan William Hardison - L =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. If you would like to contribute documentation or file a bug report then please raise an issue / pull request: https://github.com/Humanstate/net-oauth2-authorizationserver =cut __PACKAGE__->meta->make_immutable; Net-OAuth2-AuthorizationServer-0.28/lib/Net/OAuth2/AuthorizationServer/ClientCredentialsGrant.pm000644 000770 000120 00000007273 13432550612 034170 0ustar00leejohnsonadmin000000 000000 package Net::OAuth2::AuthorizationServer::ClientCredentialsGrant; =head1 NAME Net::OAuth2::AuthorizationServer::ClientCredentialsGrant - OAuth2 Client Credentials Grant =head1 SYNOPSIS my $Grant = Net::OAuth2::AuthorizationServer::ClientCredentialsGrant->new( clients => { TrendyNewService => { client_secret => 'TopSecretClientSecret', # optional scopes => { post_images => 1, annoy_friends => 1, }, }, } ); # verify a client against known clients my ( $is_valid,$error,$scopes ) = $Grant->verify_client( client_id => $client_id, client_secret => $client_secret, scopes => [ qw/ list of scopes / ], # optional ); # generate a token my $token = $Grant->token( client_id => $client_id, scopes => [ qw/ list of scopes / ], user_id => $user_id, # optional jwt_claims_cb => sub { ... }, # optional, see jwt_claims_cb in Manual ); # store access token $Grant->store_access_token( client_id => $client, access_token => $access_token, scopes => [ qw/ list of scopes / ], ); # verify an access token my ( $is_valid,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ list of scopes / ], ); =head1 DESCRIPTION This module implements the OAuth2 "Client Credentials Grant" flow as described at L. =head1 CONSTRUCTOR ARGUMENTS Along with those detailed at L the following are supported by this grant type: =head1 CALLBACK FUNCTIONS The following callbacks are supported by this grant type: verify_client_cb store_access_token_cb verify_access_token_cb Please see L for documentation on each callback function. =cut use strict; use warnings; use Moo; # prety much the same as implicit grant but even simpler extends 'Net::OAuth2::AuthorizationServer::ImplicitGrant'; use Carp qw/ croak /; use Types::Standard qw/ :all /; sub _uses_auth_codes { 0 }; sub _uses_user_passwords { 0 }; sub _verify_client { my ( $self, %args ) = @_; my ( $client_id, $scopes_ref, $client_secret ) = @args{ qw/ client_id scopes client_secret / }; if ( my $client = $self->clients->{ $client_id } ) { my $client_scopes = []; foreach my $scope ( @{ $scopes_ref // [] } ) { if ( ! exists($self->clients->{ $client_id }{ scopes }{ $scope }) ) { return ( 0, 'invalid_scope' ); } elsif ( $self->clients->{ $client_id }{ scopes }{ $scope } ) { push @{$client_scopes}, $scope; } } return ( 0, 'invalid_grant' ) if ! defined $client_secret; if ( $client_secret ne $self->clients->{ $client_id }{ client_secret } ) { return ( 0, 'invalid_grant' ); } return ( 1, undef, $client_scopes ); } return ( 0, 'unauthorized_client' ); } sub _verify_access_token { my ( $self, %args ) = @_; return $self->SUPER::_verify_access_token( %args ); } sub _store_access_token { my ( $self, %args ) = @_; return $self->SUPER::_store_access_token( %args ); } =head1 AUTHOR Lee Johnson - C =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. If you would like to contribute documentation or file a bug report then please raise an issue / pull request: https://github.com/Humanstate/net-oauth2-authorizationserver =cut __PACKAGE__->meta->make_immutable; Net-OAuth2-AuthorizationServer-0.28/lib/Net/OAuth2/AuthorizationServer/ImplicitGrant.pm000644 000770 000120 00000012561 13654543633 032355 0ustar00leejohnsonadmin000000 000000 package Net::OAuth2::AuthorizationServer::ImplicitGrant; =head1 NAME Net::OAuth2::AuthorizationServer::ImplicitGrant - OAuth2 Resource Owner Implicit Grant You "SHOULD NOT" use this grant type (see L) =head1 SYNOPSIS my $Grant = Net::OAuth2::AuthorizationServer::ImplicitGrant->new( clients => { TrendyNewService => { # optional redirect_uri => 'https://...', # optional scopes => { post_images => 1, annoy_friends => 1, }, }, } ); # verify a client against known clients my ( $is_valid,$error,$scopes ) = $Grant->verify_client( client_id => $client_id, redirect_uri => $uri, # optional scopes => [ qw/ list of scopes / ], # optional ); if ( ! $Grant->login_resource_owner ) { # resource owner needs to login ... } # have resource owner confirm (and perhaps modify) scopes my ( $confirmed,$error,$scopes_ref ) = $Grant->confirm_by_resource_owner( client_id => $client_id, scopes => [ qw/ list of scopes / ], ); # generate a token my $token = $Grant->token( client_id => $client_id, scopes => $scopes_ref, redirect_uri => $redirect_uri, user_id => $user_id, # optional jwt_claims_cb => sub { ... }, # optional, see jwt_claims_cb in Manual ); # store access token $Grant->store_access_token( client_id => $client, access_token => $access_token, scopes => $scopes_ref, ); # verify an access token my ( $is_valid,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => $scopes_ref, ); =head1 DESCRIPTION This module implements the OAuth2 "Resource Owner Implicit Grant" flow as described at L. =head1 CONSTRUCTOR ARGUMENTS Along with those detailed at L the following are supported by this grant type: =head1 CALLBACK FUNCTIONS The following callbacks are supported by this grant type: verify_client_cb login_resource_owner_cb confirm_by_resource_owner_cb store_access_token_cb verify_access_token_cb Please see L for documentation on each callback function. =cut use strict; use warnings; use Moo; with 'Net::OAuth2::AuthorizationServer::Defaults'; use Carp qw/ croak /; use Types::Standard qw/ :all /; sub _uses_auth_codes { 0 }; sub _uses_user_passwords { 0 }; sub BUILD { my ( $self, $args ) = @_; if ( # if we don't have a list of clients !$self->_has_clients # we must know how to verify clients and tokens and ( !$args->{ verify_client_cb } and !$args->{ store_access_token_cb } and !$args->{ verify_access_token_cb } ) ) { croak __PACKAGE__ . " requires either clients or overrides"; } } sub _verify_client { my ( $self, %args ) = @_; my ( $client_id, $scopes_ref, $redirect_uri ) = @args{ qw/ client_id scopes redirect_uri / }; if ( my $client = $self->clients->{ $client_id } ) { my $client_scopes = []; foreach my $scope ( @{ $scopes_ref // [] } ) { if ( ! exists($self->clients->{ $client_id }{ scopes }{ $scope }) ) { return ( 0, 'invalid_scope' ); } elsif ( $self->clients->{ $client_id }{ scopes }{ $scope } ) { push @{$client_scopes}, $scope; } } if ( # redirect_uri is optional $self->clients->{ $client_id }{ redirect_uri } && ( ! $redirect_uri || $redirect_uri ne $self->clients->{ $client_id }{ redirect_uri } ) ) { return ( 0, 'invalid_request' ); } if ( # implies Authorization Code Grant, not Implicit Grant $self->clients->{ $client_id }{ client_secret } ) { return ( 0, 'unauthorized_client' ); } return ( 1, undef, $client_scopes ); } return ( 0, 'unauthorized_client' ); } sub _verify_access_token { my ( $self, %args ) = @_; delete( $args{is_refresh_token} ); # not supported by implicit grant return $self->_verify_access_token_jwt( %args ) if $self->jwt_secret; my ( $a_token, $scopes_ref ) = @args{ qw/ access_token scopes / }; if ( exists( $self->access_tokens->{ $a_token } ) ) { if ( $self->access_tokens->{ $a_token }{ expires } <= time ) { $self->_revoke_access_token( $a_token ); return ( 0, 'invalid_grant' ); } elsif ( $scopes_ref ) { foreach my $scope ( @{ $scopes_ref // [] } ) { return ( 0, 'invalid_grant' ) if !$self->_has_scope( $scope, $self->access_tokens->{ $a_token }{ scope } ); } } return ( $self->access_tokens->{ $a_token }{ client_id }, undef ); } return ( 0, 'invalid_grant' ); } =head1 AUTHOR Lee Johnson - C =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. If you would like to contribute documentation or file a bug report then please raise an issue / pull request: https://github.com/Humanstate/net-oauth2-authorizationserver =cut __PACKAGE__->meta->make_immutable; Net-OAuth2-AuthorizationServer-0.28/lib/Net/OAuth2/AuthorizationServer/Defaults.pm000644 000770 000120 00000025331 13750017042 031340 0ustar00leejohnsonadmin000000 000000 package Net::OAuth2::AuthorizationServer::Defaults; use strict; use warnings; use Moo::Role; use Types::Standard qw/ :all /; use Carp qw/ croak /; use Crypt::JWT qw/ encode_jwt decode_jwt /; use Crypt::PRNG qw/ random_string /; use Try::Tiny; use Time::HiRes qw/ gettimeofday /; use MIME::Base64 qw/ encode_base64 /; has 'jwt_secret' => ( is => 'ro', isa => Any, required => 0, ); has 'jwt_algorithm' => ( is => 'ro', isa => Any, required => 0, default => sub { 'HS256' }, # back compat with Mojo::JWT ); has 'jwt_encoding' => ( is => 'ro', isa => Str, required => 0, default => sub { 'A128GCM' }, ); has 'access_token_ttl' => ( is => 'ro', isa => Maybe[Int|CodeRef], required => 0, default => sub { 3600 }, ); has [ qw/ clients access_tokens refresh_tokens / ] => ( is => 'ro', isa => Maybe [HashRef], required => 0, default => sub { {} }, ); has [ qw/ verify_client_cb store_access_token_cb verify_access_token_cb login_resource_owner_cb confirm_by_resource_owner_cb / ] => ( is => 'ro', isa => Maybe [CodeRef], required => 0, ); sub _has_clients { return keys %{ shift->clients // {} } ? 1 : 0 } sub _uses_auth_codes { die "You must override _uses_auth_codes" }; sub verify_client { _delegate_to_cb_or_private( 'verify_client', @_ ); } sub store_access_token { _delegate_to_cb_or_private( 'store_access_token', @_ ); } sub verify_access_token { _delegate_to_cb_or_private( 'verify_access_token', @_ ); } sub login_resource_owner { _delegate_to_cb_or_private( 'login_resource_owner', @_ ); } sub confirm_by_resource_owner { _delegate_to_cb_or_private( 'confirm_by_resource_owner', @_ ); } sub verify_token_and_scope { my ( $self, %args ) = @_; my ( $refresh_token, $scopes_ref, $auth_header ) = @args{ qw/ refresh_token scopes auth_header / }; my $access_token; if ( !$refresh_token ) { if ( $auth_header ) { my ( $auth_type, $auth_access_token ) = split( / /, $auth_header ); if ( $auth_type ne 'Bearer' ) { return ( 0, 'invalid_request' ); } else { $access_token = $auth_access_token; } } else { return ( 0, 'invalid_request' ); } } else { $access_token = $refresh_token; } # we must have some form of token to continue return ( 0, 'invalid_request' ) if ( ! $access_token && ! $refresh_token ); return $self->verify_access_token( %args, access_token => $access_token, scopes => $scopes_ref, is_refresh_token => $refresh_token, ); } sub _delegate_to_cb_or_private { my $method = shift; my $self = shift; my %args = @_; my $cb_method = "${method}_cb"; my $p_method = "_$method"; if ( my $cb = $self->$cb_method ) { return $cb->( %args ); } else { return $self->$p_method( %args ); } } sub _login_resource_owner { 1 } sub _confirm_by_resource_owner { my ( $self,%args ) = @_; # out of the box we just pass back "yes you can" and the list of scopes # note the wantarray is here for backwards compat as this method used # to just return 1 but now passing the scopes back requires an array return wantarray ? ( 1,undef,$args{scopes} // [] ) : 1; } sub _verify_client { my ( $self, %args ) = @_; my ( $client_id, $scopes_ref ) = @args{ qw/ client_id scopes / }; if ( my $client = $self->clients->{ $client_id // '' } ) { my $client_scopes = []; foreach my $scope ( @{ $scopes_ref // [] } ) { if ( ! exists($self->clients->{ $client_id }{ scopes }{ $scope }) ) { return ( 0, 'invalid_scope' ); } elsif ( $self->clients->{ $client_id }{ scopes }{ $scope } ) { push @{$client_scopes}, $scope; } } return ( 1, undef, $client_scopes ); } return ( 0, 'unauthorized_client' ); } sub _store_access_token { my ( $self, %args ) = @_; my ( $c_id, $auth_code, $access_token, $refresh_token, $expires_in, $scope, $old_refresh_token ) = @args{ qw/ client_id auth_code access_token refresh_token expires_in scopes old_refresh_token / }; $expires_in //= $self->get_access_token_ttl( scopes => $scope, client_id => $c_id, ); return 1 if $self->jwt_secret; if ( !defined( $auth_code ) && $old_refresh_token ) { # must have generated an access token via a refresh token so revoke the old # access token and refresh token and update the auth_codes hash to store the # new one (also copy across scopes if missing) $auth_code = $self->refresh_tokens->{ $old_refresh_token }{ auth_code }; my $prev_access_token = $self->refresh_tokens->{ $old_refresh_token }{ access_token }; # access tokens can be revoked, whilst refresh tokens can remain so we # need to get the data from the refresh token as the access token may # no longer exist at the point that the refresh token is used $scope //= $self->refresh_tokens->{ $old_refresh_token }{ scope }; $self->_revoke_access_token( $prev_access_token ); } delete( $self->refresh_tokens->{ $old_refresh_token } ) if $old_refresh_token; $self->access_tokens->{ $access_token } = { scope => $scope, expires => time + $expires_in, refresh_token => $refresh_token // undef, client_id => $c_id, }; if ( $refresh_token ) { $self->refresh_tokens->{ $refresh_token } = { scope => $scope, client_id => $c_id, access_token => $access_token, ( $self->_uses_auth_codes ? ( auth_code => $auth_code ) : () ), }; } if ( $self->_uses_auth_codes ) { $self->auth_codes->{ $auth_code }{ access_token } = $access_token; } return $c_id; } sub _verify_access_token { my ( $self, %args ) = @_; return $self->_verify_access_token_jwt( %args ) if $self->jwt_secret; my ( $a_token, $scopes_ref, $is_refresh_token ) = @args{ qw/ access_token scopes is_refresh_token / }; if ( $is_refresh_token && exists( $self->refresh_tokens->{ $a_token } ) ) { if ( $scopes_ref ) { foreach my $scope ( @{ $scopes_ref // [] } ) { return ( 0, 'invalid_grant' ) if !$self->_has_scope( $scope, $self->refresh_tokens->{ $a_token }{ scope } ); } } return ( $self->refresh_tokens->{ $a_token }, undef, $self->refresh_tokens->{ $a_token }{ scope }, $self->refresh_tokens->{ $a_token }{ user_id }, ); } elsif ( exists( $self->access_tokens->{ $a_token } ) ) { if ( $self->access_tokens->{ $a_token }{ expires } <= time ) { $self->_revoke_access_token( $a_token ); return ( 0, 'invalid_grant' ); } elsif ( $scopes_ref ) { foreach my $scope ( @{ $scopes_ref // [] } ) { return ( 0, 'invalid_grant' ) if !$self->_has_scope( $scope, $self->access_tokens->{ $a_token }{ scope } ); } } return ( $self->access_tokens->{ $a_token }, undef, $self->access_tokens->{ $a_token }{ scope }, $self->access_tokens->{ $a_token }{ user_id }, ); } return ( 0, 'invalid_grant' ); } sub _has_scope { my ( $self, $scope, $available_scopes ) = @_; return scalar grep { $_ eq $scope } @{ $available_scopes // [] }; } sub _verify_access_token_jwt { my ( $self, %args ) = @_; my ( $access_token, $scopes_ref, $is_refresh_token ) = @args{ qw/ access_token scopes is_refresh_token / }; my $access_token_payload; my $invalid_jwt; try { $access_token_payload = decode_jwt( alg => $self->jwt_algorithm, key => $self->jwt_secret, token => $access_token, ); } catch { $invalid_jwt = 1; }; return ( 0, 'invalid_grant' ) if $invalid_jwt; if ( $access_token_payload && ( $access_token_payload->{ type } eq 'access' || $is_refresh_token && $access_token_payload->{ type } eq 'refresh' ) ) { if ( $scopes_ref ) { foreach my $scope ( @{ $scopes_ref // [] } ) { return ( 0, 'invalid_grant' ) if !$self->_has_scope( $scope, $access_token_payload->{ scopes } ); } } return ( $access_token_payload, undef, $access_token_payload->{scopes} ); } return ( 0, 'invalid_grant' ); } sub _revoke_access_token { my ( $self, $access_token ) = @_; delete( $self->access_tokens->{ $access_token } ); } sub get_access_token_ttl { my ( $self, %args ) = @_; return ref $self->access_token_ttl eq 'CODE' ? $self->access_token_ttl->( %args ) : $self->access_token_ttl; } sub token { my ( $self, %args ) = @_; my ( $client_id, $scopes, $type, $redirect_uri, $user_id, $claims ) = @args{ qw/ client_id scopes type redirect_uri user_id jwt_claims_cb / }; if ( ! $self->_uses_auth_codes && $type eq 'auth' ) { croak "Invalid type for ->token ($type)"; } my $ttl = $type eq 'auth' ? $self->auth_code_ttl : $self->get_access_token_ttl( scopes => $scopes, client_id => $client_id, ); undef( $ttl ) if $type eq 'refresh'; my $code; if ( !$self->jwt_secret ) { my ( $sec, $usec ) = gettimeofday; $code = encode_base64( join( '-', $sec, $usec, rand(), random_string( 30 ) ), '' ); } else { if ( $self->jwt_algorithm =~ /none/i ) { croak "A jwt_algorithm of 'none' is not supported as this is insecure"; } my $jti = random_string( 32 ); $code = encode_jwt( allow_none => 0, # do NOT allow the "none" algorithm, as this is massively insecure # we actually already check this above, but this is here a backup alg => $self->jwt_algorithm, key => $self->jwt_secret, enc => $self->jwt_encoding, ( $ttl ? ( relative_exp => $ttl ) : () ), auto_iat => 1, # https://tools.ietf.org/html/rfc7519#section-4 payload => { # Registered Claim Names aud => $redirect_uri, # the "audience" jti => $jti, # Private Claim Names user_id => $user_id, client => $client_id, type => $type, scopes => $scopes, ( $claims ? ( $claims->({ user_id => $user_id, client_id => $client_id, type => $type, scopes => $scopes, redirect_uri => $redirect_uri, jti => $jti, }) ) : () ), }, ); } return $code; } __PACKAGE__->meta->make_immutable; Net-OAuth2-AuthorizationServer-0.28/lib/Net/OAuth2/AuthorizationServer/AuthorizationCodeGrant.pm000644 000770 000120 00000017362 13460615265 034236 0ustar00leejohnsonadmin000000 000000 package Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant; =head1 NAME Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant - OAuth2 Authorization Code Grant =head1 SYNOPSIS my $Grant = Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant->new( clients => { TrendyNewService => { client_secret => 'TopSecretClientSecret', scopes => { post_images => 1, annoy_friends => 1, }, }, } ); # verify a client against known clients my ( $is_valid,$error ) = $Grant->verify_client( client_id => $client_id, scopes => [ qw/ list of scopes / ], ); if ( ! $Grant->login_resource_owner ) { # resource owner needs to login ... } # have resource owner confirm (and perhaps modify) scopes my ( $confirmed,$error,$scopes_ref ) = $Grant->confirm_by_resource_owner( client_id => $client_id, scopes => [ qw/ list of scopes / ], ); # generate a token my $token = $Grant->token( client_id => $client_id, scopes => $scopes_ref, type => 'auth', # one of: auth, access, refresh redirect_uri => $redirect_uri, user_id => $user_id, # optional jwt_claims_cb => sub { ... }, # optional, see jwt_claims_cb in Manual ); # store the auth code $Grant->store_auth_code( auth_code => $auth_code, client_id => $client_id, redirect_uri => $uri, scopes => $scopes_ref, ); # verify an auth code my ( $client,$error,$scope,$user_id ) = $Grant->verify_auth_code( client_id => $client_id, client_secret => $client_secret, auth_code => $auth_code, redirect_uri => $uri, ); # store access token $Grant->store_access_token( client_id => $client, auth_code => $auth_code, access_token => $access_token, refresh_token => $refresh_token, scopes => $scopes_ref, old_refresh_token => $old_refresh_token, ); # verify an access token my ( $is_valid,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ list of scopes / ], is_refresh_token => 0, ); # or: my ( $client,$error,$scope,$user_id ) = $Grant->verify_token_and_scope( refresh_token => $refresh_token, auth_header => $http_authorization_header, ); =head1 DESCRIPTION This module implements the OAuth2 "Authorization Code Grant" flow as described at L. =head1 CONSTRUCTOR ARGUMENTS Along with those detailed at L the following are supported by this grant type: =head2 auth_code_ttl The validity period of the generated authorization code in seconds. Defaults to 600 seconds (10 minutes) =head1 CALLBACK FUNCTIONS The following callbacks are supported by this grant type: verify_client_cb login_resource_owner_cb confirm_by_resource_owner_cb store_auth_code_cb verify_auth_code_cb store_access_token_cb verify_access_token_cb Please see L for documentation on each callback function. =cut use strict; use warnings; use Moo; with 'Net::OAuth2::AuthorizationServer::Defaults'; use Types::Standard qw/ :all /; use Carp qw/ croak /; use MIME::Base64 qw/ decode_base64 /; use Crypt::JWT qw/ decode_jwt /; use Try::Tiny; has 'auth_code_ttl' => ( is => 'ro', isa => Int, required => 0, default => sub { 600 }, ); has 'auth_codes' => ( is => 'ro', isa => Maybe [HashRef], required => 0, default => sub { {} }, ); has [ qw/ store_auth_code_cb verify_auth_code_cb / ] => ( is => 'ro', isa => Maybe [CodeRef], required => 0, ); sub _uses_auth_codes { 1 }; sub _uses_user_passwords { 0 }; sub BUILD { my ( $self, $args ) = @_; if ( # if we don't have a list of clients !$self->_has_clients # we must know how to verify clients and tokens and ( !$args->{ verify_client_cb } and !$args->{ store_auth_code_cb } and !$args->{ verify_auth_code_cb } and !$args->{ store_access_token_cb } and !$args->{ verify_access_token_cb } ) ) { croak __PACKAGE__ . " requires either clients or overrides"; } } sub store_auth_code { _delegate_to_cb_or_private( 'store_auth_code', @_ ); } sub verify_auth_code { _delegate_to_cb_or_private( 'verify_auth_code', @_ ); } sub _store_auth_code { my ( $self, %args ) = @_; my ( $auth_code, $client_id, $expires_in, $uri, $scopes_ref ) = @args{ qw/ auth_code client_id expires_in redirect_uri scopes / }; return 1 if $self->jwt_secret; $expires_in //= $self->auth_code_ttl; $self->auth_codes->{ $auth_code } = { client_id => $client_id, expires => time + $expires_in, redirect_uri => $uri, scope => $scopes_ref, }; return 1; } sub _verify_auth_code { my ( $self, %args ) = @_; my ( $client_id, $client_secret, $auth_code, $uri ) = @args{ qw/ client_id client_secret auth_code redirect_uri / }; my $client = $self->clients->{ $client_id } || return ( 0, 'unauthorized_client' ); return $self->_verify_auth_code_jwt( %args ) if $self->jwt_secret; my ( $sec, $usec, $rand ) = split( '-', decode_base64( $auth_code ) ); if ( !exists( $self->auth_codes->{ $auth_code } ) or !exists( $self->clients->{ $client_id } ) or ( $client_secret ne $self->clients->{ $client_id }{ client_secret } ) or $self->auth_codes->{ $auth_code }{ access_token } or ( $uri && $self->auth_codes->{ $auth_code }{ redirect_uri } ne $uri ) or ( $self->auth_codes->{ $auth_code }{ expires } <= time ) ) { if ( my $access_token = $self->auth_codes->{ $auth_code }{ access_token } ) { # this auth code has already been used to generate an access token # so we need to revoke the access token that was previously generated $self->_revoke_access_token( $access_token ); } return ( 0, 'invalid_grant' ); } else { return ( 1, undef, @{ $self->auth_codes->{ $auth_code } }{ qw/ scope user_id / } ); } } sub _verify_auth_code_jwt { my ( $self, %args ) = @_; my ( $client_id, $client_secret, $auth_code, $uri ) = @args{ qw/ client_id client_secret auth_code redirect_uri / }; my $client = $self->clients->{ $client_id } || return ( 0, 'unauthorized_client' ); return ( 0, 'invalid_grant' ) if ( $client_secret ne $client->{ client_secret } ); my ( $auth_code_payload,$invalid_jwt ); try { $auth_code_payload = decode_jwt( alg => $self->jwt_algorithm, key => $self->jwt_secret, token => $auth_code, ); } catch { $invalid_jwt = 1; }; if ( !$auth_code_payload or $invalid_jwt or $auth_code_payload->{ type } ne 'auth' or $auth_code_payload->{ client } ne $client_id or ( $uri && $auth_code_payload->{ aud } ne $uri ) ) { return ( 0, 'invalid_grant' ); } my $scope = $auth_code_payload->{ scopes }; my $user_id = $auth_code_payload->{ user_id }; return ( $client_id, undef, $scope, $user_id ); } =head1 AUTHOR Lee Johnson - C =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. If you would like to contribute documentation or file a bug report then please raise an issue / pull request: https://github.com/Humanstate/net-oauth2-authorizationserver =cut __PACKAGE__->meta->make_immutable; Net-OAuth2-AuthorizationServer-0.28/lib/Net/OAuth2/AuthorizationServer/Manual.pod000644 000770 000120 00000073064 13654546414 031177 0ustar00leejohnsonadmin000000 000000 =head1 NAME Net::OAuth2::AuthorizationServer::Manual - How to use Net::OAuth2::AuthorizationServer =head1 DESCRIPTION The modules within the L namespace implement the various OAuth2 grant type flows. Each module implements a specific grant flow and is designed to "just work" with minimal detail and effort. However there are some limitations in using the grant modules in a minimal way, such as inability to store tokens across restarts or multi-proc and losing tokens over a restart. You can get around these limitations by supplying a C or by supplying your own overrides. The overrides are detailed in the L section below - each grant module's docs lists the supported callback functions. If you would still like to use the grant modules in an easy way, but also have tokens persistent across restarts and shared between multi processes then you can supply a jwt_secret. What you lose when doing this is the ability for tokens to be revoked. You could implement the verify_auth_code and verify_access_token methods to handle the revoking in your app. So that would be halfway between the "simple" and the "realistic" way. L has more detail about JWTs. =head1 WHICH GRANT TYPE SHOULD I USE? You are advised to read L and figure that out depending on your needs, but to simplify - if you are implementing an app server and need to give out access tokens you should be using the "Authorization Code Grant" as this is the most secure flow. If you need to do a call from javascript then the "Implicit Grant" is probably what you need. You should B use the "Password Grant" or "Client Credentials Grant" unless your client/server apps are non-public facing or you B know what you are doing. The "Authorization Code Grant" is used to obtain both access tokens and refresh tokens and is optimized for confidential clients. Since this is a redirection-based flow, the client must be capable of interacting with the resource owner's user-agent (typically a web browser) and capable of receiving incoming requests (via redirection) from the authorization server. The "Implicit Grant" is used to obtain access tokens (it does not support the issuance of refresh tokens) and is optimized for public clients known to operate a particular redirection URI. These clients are typically implemented in a browser using a scripting language such as JavaScript. The "Password Grant" type MUST NOT be used according to recent draft RFC L The "Client Credentials Grant" can request an access token using only its client credentials (or other supported means of authentication) when the client is requesting access to the protected resources under its control, or those of another resource owner that have been previously arranged with the authorization server. =head1 CONSTRUCTOR ARGUMENTS The following are accepted by all grant types =head2 clients A hashref of client details keyed like so: clients => { $client_id => { client_secret => $client_secret, redirect_uri => $redirect_uri, # in the case of "Implicit Grant" scopes => { eat => 1, drink => 0, sleep => 1, }, }, }, Note that setting a client_secret implies either "Authorization Code Grant" or "Client Credentials Grant". if this is not set then "Implicit Grant" is implied (that has an optional redirect_uri parameter) Note the clients config is not required if you add all of the necessary callback functions detailed below, but is necessary for running the grant types in their simplest form (when there are *no* callbacks provided) =head2 users In the case of the "Password Grant" you should supply a hash of user to password mappings as well as the above client details: users => { test_user => 'reallyletmein', } Again, the users config is not required if you add all of the necessary callback functions detailed below. =head2 jwt_secret This is optional. If set JWTs will be returned for the auth codes, access, and refresh tokens. JWTs allow you to validate tokens without doing a db lookup, but there are certain considerations (see L) =head2 jwt_algorithm This is optional, and sets the algorithm used in the creation of JWTs (see L) =head2 jwt_encoding This is optional, and sets the encoding used in the creation of JWTs (see L) =head2 access_token_ttl The validity period of the generated access token in seconds. Defaults to 3600 seconds (1 hour) =head1 CALLBACK FUNCTIONS These are the callbacks necessary to use the grant modules in a more realistic way, and are required to make the auth code, access token, refresh token, etc available across several processes and persistent. They should be passed to the grant module constructor, each should be a reference to a subroutine: my $Grant = Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant->new( ... confirm_by_resource_owner_cb => \&your_subroutine, ); The examples below use monogodb (a db helper returns a MongoDB::Database object) for the code that would be bespoke to your application - such as finding access codes in the database, and so on. You can refer to the tests in t/ and examples in examples/ in this distribution for how it could be done and to actually play around with the code both in a browser and on the command line. The examples below are also using a "mojo_controller" object within the args hash passed to the callbacks - you can pass any extra keys/values you want within the args hash so you can do the necessary things (e.g. logging) along with the required args =head2 confirm_by_resource_owner_cb A callback to tell the grant module if the Resource Owner allowed or denied access to the Resource Server by the Client. The args hash should contain the client id, and an array reference of scopes requested by the client. The callback should return a list with three elements. The first element is 1 if access is allowed, 0 if access is not allowed, otherwise undef if the flow was interrupted (e.g. calling redirect in a controller). The second element should be the error message in the case of problems with the confirmation of the scopes. The third element should be an array reference of scopes as the user may choose to modify the list of requested scopes when confirming them. my $resource_owner_confirm_scopes_sub = sub { my ( %args ) = @_; my ( $obj,$client_id,$scopes_ref,$redirect_uri,$response_type ) = @args{ qw/ mojo_controller client_id scopes redirect_uri response_type / }; my $error; my $is_allowed = $obj->flash( "oauth_${client_id}" ); # if user hasn't yet allowed the client access, or if they denied # access last time, we check [again] with the user for access if ( ! $is_allowed ) { $obj->flash( client_id => $client_id ); $obj->flash( scopes => $scopes_ref ); # we need to redirect back to the /oauth/authorize route after # confirm/deny by resource owner (with the original params) my $uri = join( '?',$obj->url_for('current'),$obj->url_with->query ); $obj->flash( 'redirect_after_login' => $uri ); $obj->redirect_to( '/oauth/confirm_scopes' ); } return ( $is_allowed,$error,$scopes_ref ); }; Note that you need to pass on the current url (with query) so it can be returned to after the user has confirmed/denied access, and the confirm/deny result is stored in the flash (this could be stored in the user session if you do not want the user to confirm/deny every single time the Client requests access). Also note the caveat regarding flash and Path as documented above (L) =head2 login_resource_owner_cb A callback to tell the grant module if the Resource Owner is logged in. You can pass a hash of arguments should you need to do anything within the callback It should return 1 if the Resource Owner is logged in, otherwise it should do the required things to login the resource owner (e.g. redirect) and return 0: my $resource_owner_logged_in_sub = sub { my ( %args ) = @_; my $c = $args{mojo_controller}; if ( ! $c->session( 'logged_in' ) ) { # we need to redirect back to the /oauth/authorize route after # login (with the original params) my $uri = join( '?',$c->url_for('current'),$c->url_with->query ); $c->flash( 'redirect_after_login' => $uri ); $c->redirect_to( '/oauth/login' ); return 0; } return 1; }; Note that you need to pass on the current url (with query) so it can be returned to after the user has logged in. You can see that the flash is in use here - be aware that the default routes (if you don't pass them to grant module constructor) for authorize and access_token are under /oauth/ so it is possible that the flash may have a Path of /oauth/ - the consequence of this is that if your login route is under a different path (likely) you will not be able to access the value you set in the flash. The solution to this? Simply create another route under /oauth/ (so in this case /oauth/login) that points to the same route as the /login route =head2 verify_client_cb References: L, L, L, L, A callback to verify if the B asking for authorization is known to the B and allowed to get authorization for the passed scopes. The args hash will always contain the B and an array reference of B. Additionally for a ClientCredentials Grant the args hash will also contain the B or for an Implicit Grant B and optionally B will be present, or for a Code Grant B will be present. The callback should return a list with three elements. The first element is either 1 or 0 to say that the client is allowed or disallowed, the second element should be the error message in the case of the client being disallowed and the third should be the amended scopes_ref denoting the allowed scopes after filtering by the client allowed scopes: my $verify_client_sub = sub { my ( %args ) = @_; my ( $obj,$client_id,$scopes_ref,$client_secret,$redirect_uri,$response_type ) = @args{ qw/ mojo_controller client_id scopes client_secret redirect_uri response_type / }; if (my $client = $obj->db->get_collection( 'clients' )->find_one({ client_id => $client_id })) { my $client_scopes = []; # Check scopes foreach my $scope ( @{ $scopes_ref // [] } ) { if ( ! exists( $client->{scopes}{$scope} ) ) { return ( 0,'invalid_scope' ); } elsif ( $client->{scopes}{$scope} ) { push @{$client_scopes}, $scope; } } # Implicit Grant Checks if ( $response_type && $response_type eq 'token' ) { # If 'credentials' have been assigned Implicit Grant should be prevented, so check for secret return (0, 'unauthorized_grant') if $client->{'secret'}; # Check redirect_uri return (0, 'access_denied') if ($client->{'redirect_uri'} && (!$redirect_uri || $redirect_uri ne $client->{'redirect_uri'}); } # Credentials Grant Checks if ($client_secret && $client_secret ne $client->{'secret'}) { return (0, 'access_denied'); } return ( 1, undef, $client_scopes ); } return ( 0,'unauthorized_client' ); }; =head2 store_auth_code_cb A callback to allow you to store the generated authorization code. The args hash should contain the client id, the auth code validity period in seconds, the Client redirect URI, and a list of the scopes requested by the Client. You should save the information to your data store, it can then be retrieved by the verify_auth_code callback for verification: my $store_auth_code_sub = sub { my ( %args ) = @_; my ( $obj,$auth_code,$client_id,$expires_in,$uri,$scopes_ref ) = @args{qw/ mojo_controller auth_code client_id expires_in redirect_uri scopes / }; my $auth_codes = $obj->db->get_collection( 'auth_codes' ); my $id = $auth_codes->insert({ auth_code => $auth_code, client_id => $client_id, user_id => $obj->session( 'user_id' ), expires => time + $expires_in, redirect_uri => $uri, scope => { map { $_ => 1 } @{ $scopes_ref // [] } }, }); return; }; =head2 verify_auth_code_cb Reference: L A callback to verify the authorization code passed from the Client to the Authorization Server. The args hash should contain the client_id, the client_secret, the authorization code, and the redirect uri. The callback should verify the authorization code using the rules defined in the reference RFC above, and return a list with 4 elements. The first element should be a client identifier (a scalar, or reference) in the case of a valid authorization code or 0 in the case of an invalid authorization code. The second element should be the error message in the case of an invalid authorization code. The third element should be a hash reference of scopes as requested by the client in the original call for an authorization code. The fourth element should be a user identifier: my $verify_auth_code_sub = sub { my ( %args ) = @_; my ( $obj,$client_id,$client_secret,$auth_code,$uri ) = @args{qw/ mojo_controller client_id client_secret auth_code redirect_uri / }; my $auth_codes = $obj->db->get_collection( 'auth_codes' ); my $ac = $auth_codes->find_one({ client_id => $client_id, auth_code => $auth_code, }); my $client = $obj->db->get_collection( 'clients' ) ->find_one({ client_id => $client_id }); $client || return ( 0,'unauthorized_client' ); if ( ! $ac or $ac->{verified} or ( $uri ne $ac->{redirect_uri} ) or ( $ac->{expires} <= time ) or ( $client_secret ne $client->{client_secret} ) ) { if ( $ac->{verified} ) { # the auth code has been used before - we must revoke the auth code # and access tokens $auth_codes->remove({ auth_code => $auth_code }); $obj->db->get_collection( 'access_tokens' )->remove({ access_token => $ac->{access_token} }); } return ( 0,'invalid_grant' ); } # scopes are those that were requested in the authorization request, not # those stored in the client (i.e. what the auth request restricted scopes # to and not everything the client is capable of) my $scope = $ac->{scope}; $auth_codes->update( $ac,{ verified => 1 } ); return ( $client_id,undef,$scope,$ac->{user_id} ); }; =head2 store_access_token_cb A callback to allow you to store the generated access and refresh tokens. The args hash should contain the client identifier as returned from the verify_auth_code callback, the authorization code, the access token, the refresh_token, the validity period in seconds, the scope returned from the verify_auth_code callback, and the old refresh token, Note that the passed authorization code could be undefined, in which case the access token and refresh tokens were requested by the Client by the use of an existing refresh token, which will be passed as the old refresh token variable. In this case you should use the old refresh token to find out the previous access token and revoke the previous access and refresh tokens (this is *not* a hard requirement according to the OAuth spec, but I would recommend it). The callback does not need to return anything. You should save the information to your data store, it can then be retrieved by the verify_access_token callback for verification: my $store_access_token_sub = sub { my ( %args ) = @_; my ( $obj,$client,$auth_code,$access_token,$refresh_token, $expires_in,$scope,$old_refresh_token ) = @args{qw/ mojo_controller client_id auth_code access_token refresh_token expires_in scopes old_refresh_token / }; my $access_tokens = $obj->db->get_collection( 'access_tokens' ); my $refresh_tokens = $obj->db->get_collection( 'refresh_tokens' ); my $user_id; if ( ! defined( $auth_code ) && $old_refresh_token ) { # must have generated an access token via refresh token so revoke the old # access token and refresh token (also copy required data if missing) my $prev_rt = $obj->db->get_collection( 'refresh_tokens' )->find_one({ refresh_token => $old_refresh_token, }); my $prev_at = $obj->db->get_collection( 'access_tokens' )->find_one({ access_token => $prev_rt->{access_token}, }); # access tokens can be revoked, whilst refresh tokens can remain so we # need to get the data from the refresh token as the access token may # no longer exist at the point that the refresh token is used $scope //= $prev_rt->{scope}; $user_id = $prev_rt->{user_id}; # need to revoke the access token $obj->db->get_collection( 'access_tokens' ) ->remove({ access_token => $prev_at->{access_token} }); } else { $user_id = $obj->db->get_collection( 'auth_codes' )->find_one({ auth_code => $auth_code, })->{user_id}; } if ( ref( $client ) ) { $scope = $client->{scope}; $client = $client->{client_id}; } # if the client has an existing refresh token we need to revoke it $refresh_tokens->remove({ client_id => $client, user_id => $user_id }); $access_tokens->insert({ access_token => $access_token, scope => $scope, expires => time + $expires_in, refresh_token => $refresh_token, client_id => $client, user_id => $user_id, }); $refresh_tokens->insert({ refresh_token => $refresh_token, access_token => $access_token, scope => $scope, client_id => $client, user_id => $user_id, }); return; }; =head2 verify_access_token_cb Reference: L A callback to verify the access token. The args hash should contain the access token, an optional reference to a list of the scopes and if the access_token is actually a refresh token. Note that the access token could be the refresh token, as this method is also called when the client uses the refresh token to get a new access token (in which case the value of the $is_refresh_token variable will be true). The callback should verify the access code using the rules defined in the reference RFC above, and return false if the access token is not valid otherwise it should return something useful if the access token is valid - since this method is called by the call to $c->oauth you probably need to return a hash of details that the access token relates to (client id, user id, etc). In the event of an invalid, expired, etc, access or refresh token you should return a list where the first element is 0 and the second contains the error message (almost certainly 'invalid_grant' in this case) my $verify_access_token_sub = sub { my ( %args ) = @_; my ( $obj,$access_token,$scopes_ref,$is_refresh_token ) = @args{qw/ mojo_controller access_token scopes is_refresh_token /}; my $rt = $obj->db->get_collection( 'refresh_tokens' )->find_one({ refresh_token => $access_token }); if ( $is_refresh_token && $rt ) { if ( $scopes_ref ) { foreach my $scope ( @{ $scopes_ref // [] } ) { if ( ! exists( $rt->{scope}{$scope} ) or ! $rt->{scope}{$scope} ) { return ( 0,'invalid_grant' ) } } } # $rt contains client_id, user_id, etc return $rt; } elsif ( my $at = $obj->db->get_collection( 'access_tokens' )->find_one({ access_token => $access_token, }) ) { if ( $at->{expires} <= time ) { # need to revoke the access token $obj->db->get_collection( 'access_tokens' ) ->remove({ access_token => $access_token }); return ( 0,'invalid_grant' ) } elsif ( $scopes_ref ) { foreach my $scope ( @{ $scopes_ref // [] } ) { if ( ! exists( $at->{scope}{$scope} ) or ! $at->{scope}{$scope} ) { return ( 0,'invalid_grant' ) } } } # $at contains client_id, user_id, etc return $at; } return ( 0,'invalid_grant' ) }; =head2 verify_user_password_cb A callback to verify a username and password. The args hash should contain the client_id, client_secret, username, password, an optional reference to a list of the scopes. The callback should verify client details and username + password and return a a hash list with 2 elements. The first element should a hash containing the client id if the client details + username is valid + scopes + username. The second element should be the error message in the case of bad credentials. my $verify_user_password_sub = sub { my ( $self, %args ) = @_; my ( $obj, $client_id, $client_secret, $username, $password, $scopes ) = @args{ qw/ mojo_controller client_id client_secret username password scopes / }; my $client = $obj->db->get_collection( 'clients' ) ->find_one({ client_id => $client_id }); $client || return ( 0, 'unauthorized_client' ); my $user = $obj->db->get_collection( 'users' ) ->find_one({ username => $username }); if ( ! $user or $client_secret ne $client->{client_secret} # some routine to check the password against hashed + salted or ! $obj->passwords_match( $user->{password},$password ) ) { return ( 0, 'invalid_grant' ); } else { return ({ client_id => $client_id, scopes => $scopes, username => $username, }); } }; =head1 PUTTING IT ALL TOGETHER Having defined the above callbacks, customized to your app/data store/etc, you can configuration the grant module. This example is using the L: my $Grant = Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant->new( login_resource_owner_cb => $resource_owner_logged_in_sub, confirm_by_resource_owner_cb => $resource_owner_confirm_scopes_sub, verify_client_cb => $verify_client_sub, store_auth_code_cb => $store_auth_code_sub, verify_auth_code_cb => $verify_auth_code_sub, store_access_token_cb => $store_access_token_sub, verify_access_token_cb => $verify_access_token_sub, ); Note because we are using the verify_client_cb above we do not need to pass a hashref of clients - this will be handled in the verify_client_cb sub =head1 SCOPES Access tokens can have the concept of "scopes", which you can roughly translate to permissions/privileges on your application side. The RFC covers the details: L. You've probably seen this in action when logging into a service using a social login option, you see a list of things the service would like to be able to do on your behalf and in most cases you are allowed to uncheck certain permissions from the list. The various grant types supported by the modules in this distribution fully support the use of scopes and the important thing to note is that the various grant types (as used with no overrides) will validate any requested scopes against the configured scopes with the following logic: 1) If a scope is requested that the client is not configured to have (does not exist in the client's scope list) then "invalid_scope" will be returned 2) If a scope is requested that the client is not configured to use (exists in the client's scope list but is set to false) then "access_denied" wil be returned Note that this may change slightly as we figure out the best implementation for various use cases when no overrides are supplied. =head1 CLIENT SECRET, TOKEN SECURITY, AND JWT The auth codes and access tokens generated by the grant modules should be unique. When jwt_secret is B supplied they are generated using a combination of the generation time (to microsecond precision) + rand() + a call to Crypt::PRNG's random_string function. These are then base64 encoded to make sure there are no problems with URL encoding. If jwt_secret is set, which should be a strong secret, the tokens are created with the L module and each token should contain a jti using a call to Crypt::PRNG's random_string function. You can decode the tokens, typically with L, to get the information about the client and scopes - but you should not trust the token unless the signature matches. If you wish to encrypt JWT, that is to say generate JWE tokens, you can set the jwt_encoding and jwt_algorithm attributes, these map respectively to the L enc and alg attributes - see that module's POD for more info As the JWT contains the client information and scopes you can, in theory, use this information to validate an auth code / access token / refresh token without doing a database lookup. However, it gets somewhat more complicated when you need to revoke tokens. For more information about JWTs and revoking tokens see L and L. Ultimately you're going to have to use some shared store to revoke tokens, but using the jwt_secret config setting means you can simplify parts of the process as the JWT will contain the client, user, and scope information (JWTs are also easy to debug: L). When using JWTs expiry dates will be automatically checked (L has this built in to the decoding). The claims hash looks something like this: { 'iat' => 1435225100, # generation time 'exp' => 1435228700, # expiry time 'aud' => undef # redirect uri in case of type: auth 'jti' => 'psclb1AcC2OjAKtVJRg1JjRJumkVTkDj', # unique 'type' => 'access', # auth, access, or refresh 'scopes' => [ 'list','of','scopes' ], # as requested by client 'client' => 'some client id', # as returned from verify_auth_code 'user_id' => 'some user id', # as returned from verify_auth_code }; If you wish to override the details set above you can pass a B in the call to token. This will be passed the details that are used above, and any returned keys will override the defaults: $Grant->token( ... jwt_claims_cb => sub { my ( $args ) = @_; return ( user_id => 'foo', # override default user_id iss => ... # add extra claims ); }, ); the args hash passed to the callback looks like so: { user_id => $user_id, client_id => $client_id, type => $type, scopes => $scopes_ref, redirect_uri => $redirect_uri, jti => $jti, } Since a call for an access token requires both the authorization code and the client secret you don't need to worry too much about protecting the authorization code - however you obviously need to make sure the client secret and resultant access tokens and refresh tokens are stored securely. Since if any of these are compromised you will have your app endpoints open to use by who or whatever has access to them. You should therefore treat the client secret, access token, and refresh token as you would treat passwords - so hashed, salted, and probably encrypted. As with the various checking functions required by the grant module, the securing of this data is left to you. More information: L L L =head1 FURTHER READING L - The OAuth 2.0 Authorization Framework. L - OAuth 2.0 Threat Model and Security Considerations. L - The OAuth 2.0 Authorization Framework: Bearer Token Usage. L - Assertion Framework for OAuth 2.0. L - Security Assertion Markup Language (SAML) 2.0 Profile for OAuth 2.0 Client Authentication and Authorization Grants. L - JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants. L - Proof Key for Code Exchange by OAuth Public Clients L - OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens, L - OAuth 2.0 Security Best Current Practice (Draft). =head1 REFERENCES =over 4 =item * L =item * L =back =head1 EXAMPLES There are examples included with this distribution in the examples/ dir. See examples/README for more information about these examples. =head1 AUTHOR Lee Johnson - C =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. If you would like to contribute documentation or file a bug report then please raise an issue / pull request: https://github.com/Humanstate/net-oauth2-authorizationserver =cut Net-OAuth2-AuthorizationServer-0.28/lib/Net/OAuth2/AuthorizationServer/PasswordGrant.pm000644 000770 000120 00000012453 13654543524 032404 0ustar00leejohnsonadmin000000 000000 package Net::OAuth2::AuthorizationServer::PasswordGrant; =head1 NAME Net::OAuth2::AuthorizationServer::PasswordGrant - OAuth2 Resource Owner Password Credentials Grant You "MUST NOT" use this grant type (see L) =head1 SYNOPSIS my $Grant = Net::OAuth2::AuthorizationServer::PasswordGrant->new( clients => { TrendyNewService => { client_secret => 'TopSecretClientSecret', scopes => { post_images => 1, annoy_friends => 1, }, }, }, users => { bob => 'j$s03R#!\fs', tom => 'dE0@@s^tWg1', }, ); # verify a client and username against known clients/users my ( $client_id,$error,$scopes,$username ) = $Grant->verify_user_password( client_id => $client_id, client_secret => $client_secret, username => $username, password => $password, scopes => [ qw/ list of scopes / ], ); if ( ! $Grant->login_resource_owner ) { # resource owner needs to login ... } # have resource owner confirm (and perhaps modify) scopes my ( $confirmed,$error,$scopes_ref ) = $Grant->confirm_by_resource_owner( client_id => $client_id, scopes => [ qw/ list of scopes / ], ); # generate a token my $token = $Grant->token( client_id => $client_id, scopes => $scopes_ref, type => 'access', # one of: access, refresh redirect_uri => $redirect_uri, user_id => $user_id, # optional jwt_claims_cb => sub { ... }, # optional, see jwt_claims_cb in Manual ); # store access token $Grant->store_access_token( client_id => $client, access_token => $access_token, refresh_token => $refresh_token, scopes => $scopes_ref, old_refresh_token => $old_refresh_token, ); # verify an access token my ( $is_valid,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => $scopes_ref, is_refresh_token => 0, ); # or: my ( $oauth_details,$error ) = $Grant->verify_token_and_scope( refresh_token => $refresh_token, auth_header => $http_authorization_header, ); =head1 DESCRIPTION This module implements the OAuth2 "Resource Owner Password Credentials Grant" flow as described at L. =head1 CONSTRUCTOR ARGUMENTS Along with those detailed at L the following are supported by this grant type: =head2 users A hashref of client details keyed like so: $username => $password =head1 CALLBACK FUNCTIONS The following callbacks are supported by this grant type: login_resource_owner_cb confirm_by_resource_owner_cb verify_client_cb verify_user_password_cb store_access_token_cb verify_access_token_cb Please see L for documentation on each callback function. =cut use strict; use warnings; use Moo; with 'Net::OAuth2::AuthorizationServer::Defaults'; use Carp qw/ croak /; use Types::Standard qw/ :all /; has 'verify_user_password_cb' => ( is => 'ro', isa => Maybe [CodeRef], required => 0, ); has 'users' => ( is => 'ro', isa => Maybe [HashRef], required => 0, default => sub { {} }, ); sub _uses_auth_codes { 0 }; sub _uses_user_passwords { 1 }; sub _has_users { return keys %{ shift->users // {} } ? 1 : 0 } sub BUILD { my ( $self, $args ) = @_; if ( # if we don't have a list of clients !$self->_has_clients # and we don't have a list of users and !$self->_has_users # we must know how to verify clients and tokens and ( !$args->{ verify_client_cb } and !$args->{ verify_user_password_cb } and !$args->{ store_access_token_cb } and !$args->{ verify_access_token_cb } ) ) { croak __PACKAGE__ . " requires either clients or overrides"; } } sub verify_user_password { _delegate_to_cb_or_private( 'verify_user_password', @_ ); } sub _verify_user_password { my ( $self, %args ) = @_; my ( $client_id, $client_secret, $username, $password, $scopes ) = @args{ qw/ client_id client_secret username password scopes / }; my $client = $self->clients->{ $client_id } || return ( 0, 'unauthorized_client' ); if ( !exists( $self->clients->{ $client_id } ) or !exists( $self->users->{ $username } ) or ( $client_secret ne $self->clients->{ $client_id }{ client_secret } ) or ( $password ne $self->users->{ $username } ) ) { return ( 0, 'invalid_grant' ); } else { return ( { client_id => $client_id, scopes => $scopes, username => $username }, undef, $scopes, # here for back compat $username, # here for back compat ); } } =head1 AUTHOR Lee Johnson - C =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. If you would like to contribute documentation or file a bug report then please raise an issue / pull request: https://github.com/Humanstate/net-oauth2-authorizationserver =cut __PACKAGE__->meta->make_immutable; Net-OAuth2-AuthorizationServer-0.28/examples/README000644 000770 000120 00000013246 13475440035 023314 0ustar00leejohnsonadmin000000 000000 The various parties: -------------------- In this diagram the Resource Server also plays the part of the Authorization Server __3_[dis]allow access from client to resource server____ / \ / \ / __1_client to use resource?___ __2_auth code__ \ / / \ / \ \ / / \ / \ \ [ Resource Owner ] <-> [ User Agent ] <-> [ Client ] <-> [ Resource Server ] \ \ / / \ \_4_access token_/ / \ / \_5_refresh token__/ 1. Client wants to be able to manipulate Resource Server on behalf of the Resource Owner (the user). 2. Client gets an Authorization Code from Resource Server (this can only work if the Resource Server knows of the Client in advance - the Client must identify itself to the Resource Server). 3. Resource Server checks with Resource Owner that Client can act on behalf of Resource Owner (with the scopes requires by the Client). Resource Owner confirms/denies. In most cases the Resource Owner must be logged in on the Resource Server. 4. (If Client confirms in step 3) Client asks Resource Server for an access token - Client POSTs to Resource Server as the Client must pass the client secret in this part of the process. If client ids, secret, allowed scopes pass checks the Resource Server returns an access token back to the Client. 5. Client can use the access token to act on behalf of the Resource Owner until the access token expires or the Resource Owner revokes the access token (or scopes). If the access token expires the refresh token can be used to obtain a new access token from the Resource Server by the Client. What we have available in this examples/ directory: --------------------------------------------------- examples/oauth2_client.pl - A Client - make sure you are up to date with the Mojolicious::Plugin::OAuth2 module examples/oauth2_server_simple.pl - An Authorization Server and Resource Server (that only runs one proc and will not retain tokens across restarts) in its simplest form. it will not prompt for login or permission so is useful to "emulate" an oauth2 server examples/oauth2_server_simple_jwt.pl - As above but uses JWTs for the auth codes and access/refresh tokens. this allows the server to be persistent and multi threaded but makes revoking tokens slightly awkward examples/oauth2_server_realistic.pl - An Authorization Server and Resource Server that will prompt for login and permission so is a more realistic emulation. configured with the examples/oauth2_db.json file. also of note this can handle multiple procs and restarts without losing know tokens - although will not scale, you should use a database really. examples/oauth2_server_db.pl - As above but uses a db (you will need mongodb and the perl MongoDB module to be able to use this example) examples/oauth2_schema.sql - And example schema for storing auth codes, access tokens, etc, in a relational database examples/OAuth2Functions.pm - Functions that could be used with the above schema web browser / /usr/bin/curl - A User Agent the command line - A Resource Owner (A person, You!) To run: ------- # make sure you have up to date dependencies: - Mojolicious - Mojolicious::Plugin::OAuth2 cd examples; perl -I../lib ~/bin/morbo -l "https://*:3000" oauth2_server_realistic.pl & perl ~/bin/morbo -l "https://*:3001" oauth2_client.pl & Then: ----- Visit https://127.0.0.1:3001 and click "Connect to Overly Attached Social Network" then "login" and confirm permissions, you will be given a JSON response that should contain the details of the access token (if you don't deny access) that you can use to access the API of Overly Attached Social Network. The JSON response will also contain the refresh token, type, and the expires_in details. So on the command line (which we are substituting for calls that would be made by TrendyNewService here): # should say "Lee Annoyed Friends": curl -k -H"Authorization: Bearer $token" \ https://127.0.0.1:3000/api/annoy_friends # should say "Lee Posted Image": curl -k -H"Authorization: Bearer $token" \ https://127.0.0.1:3000/api/post_image # should say "You cannot track location" (with a 401 status): curl -k -H"Authorization: Bearer $token" \ https://127.0.0.1:3000/api/track_location # should say "Unauthorized" (with a 401 status): curl -k -H"Authorization: Bearer foo" \ https://127.0.0.1:3000/api/annoy_friends # You can even use the refresh token to generate a new access token: curl -k -XPOST https://127.0.0.1:3000/oauth/access_token \ -d "client_id=TrendyNewService&refresh_token=$refresh_token&grant_type=refresh_token" | json_pp Net-OAuth2-AuthorizationServer-0.28/examples/OAuth2Functions.pm000644 000770 000120 00000023446 13475225644 025777 0ustar00leejohnsonadmin000000 000000 package OAuth2Functions; use strict; use warnings; use DateTime; use Exporter::Easy ( OK => [ qw/ oauth2_functions / ], ); sub oauth2_functions { my ( $self ) = @_; $self->plugin( 'OAuth2::Server' => { login_resource_owner => \&_resource_owner_logged_in, confirm_by_resource_owner => \&_resource_owner_confirm_scopes, verify_client => \&_verify_client, store_auth_code => \&_store_auth_code, verify_auth_code => \&_verify_auth_code, store_access_token => \&_store_access_token, verify_access_token => \&_verify_access_token, }, ); return 1; } sub _resource_owner_logged_in { my ( %args ) = @_; my $c = $args{mojo_controller}; if ( ! $c->session( 'session_id' ) ) { # we need to redirect back to the /oauth/authorize route after # login (with the original params) my $uri = join( '?',$c->url_for('current'),$c->url_with->query ); $c->flash( 'redirect_after_login' => $uri ); $c->redirect_to( '/oauth/login' ); return 0; } return 1; } sub _resource_owner_confirm_scopes { my ( %args ) = @_; my ( $c,$client_id,$scopes_ref,$redirect_uri,$response_type ) = @args{ qw/ mojo_controller client_id scopes redirect_uri response_type / }; my $is_allowed = $c->flash( "oauth_${client_id}" ); # if user hasn't yet allowed the client access, or if they denied # access last time, we check [again] with the user for access if ( ! $is_allowed ) { $c->flash( client_id => $client_id ); $c->flash( scopes => $scopes_ref ); my $uri = join( '?',$c->url_for('current'),$c->url_with->query ); $c->flash( 'redirect_after_login' => $uri ); $c->redirect_to( '/oauth/confirm_scopes' ); } return ( $is_allowed,undef,$scopes_ref ); } sub _verify_client { my ( %args ) = @_; my ( $c,$client_id,$scopes_ref,$client_secret,$redirect_uri,$response_type ) = @args{ qw/ mojo_controller client_id scopes client_secret redirect_uri response_type / }; if ( my $client = $c->model->rs( 'Oauth2Client' )->find( $client_id ) ) { if ( ! $client->active ) { $c->app->log->debug( "Client ($client_id) is not active" ); return ( 0,'unauthorized_client' ); } foreach my $rqd_scope ( @{ $scopes_ref // [] } ) { if ( my $scope = $c->model->rs( 'Oauth2ClientScope' )->find({ 'scope.description' => $rqd_scope, 'client_id' => $client_id, },{ join => [ qw/ scope / ] } ) ) { if ( ! $scope->allowed ) { $c->app->log->debug( "Client disallowed scope ($rqd_scope)" ); return ( 0,'access_denied' ); } } else { $c->app->log->debug( "Client lacks scope ($rqd_scope)" ); return ( 0,'invalid_scope' ); } } return ( 1 ); } $c->app->log->debug( "Client ($client_id) does not exist" ); return ( 0,'unauthorized_client' ); } sub _store_auth_code { my ( %args ) = @_; my ( $c,$auth_code,$client_id,$expires_in,$uri,$scopes_ref ) = @args{qw/ mojo_controller auth_code client_id expires_in redirect_uri scopes / }; my $user_id = $c->session( 'user_id' ); $c->model->rs( 'Oauth2AuthCode' )->create({ auth_code => $auth_code, client_id => $client_id, user_id => $user_id, expires => DateTime->from_epoch( epoch => time + $expires_in ), redirect_uri => $uri, verified => 0, }); foreach my $rqd_scope ( @scopes ) { if ( my $scope = $c->model->rs( 'Oauth2Scope' )->find({ description => $rqd_scope }) ) { $scope->create_related( 'oauth2_auth_code_scopes', { auth_code => $auth_code, allowed => 1 } ); } else { $c->app->log->error( "Unknown scope ($rqd_scope) in _store_auth_code" ); } } return; } sub _verify_auth_code { my ( %args ) = @_; my ( $c,$client_id,$client_secret,$auth_code,$uri ) = @args{qw/ mojo_controller client_id client_secret auth_code redirect_uri / }; my $client = $c->model->rs( 'Oauth2Client' )->find( $client_id ) || return ( 0,'unauthorized_client' ); my $ac = $c->model->rs( 'Oauth2AuthCode' )->find({ client_id => $client_id, auth_code => $auth_code, }); if ( ! $ac or $ac->verified or ( $uri ne $ac->redirect_uri ) or ( $ac->expires->epoch <= time ) or ! _check_password( $client_secret,$client->secret ) ) { $c->app->log->debug( "Auth code does not exist" ) if ! $ac; $c->app->log->debug( "Client secret does not match" ) if ! _check_password( $client_secret,$client->secret ); if ( $ac ) { $c->app->log->debug( "Client secret does not match" ) if ( $uri && $ac->redirect_uri ne $uri ); $c->app->log->debug( "Auth code expired" ) if ( $ac->expires->epoch <= time ); if ( $ac->verified ) { # the auth code has been used before - we must revoke the auth code # and any associated access tokens (same client_id and user_id) $c->app->log->debug( "Auth code already used to get access token, " . "revoking all associated access tokens" ); $ac->delete; if ( my $rs = $c->model->rs( 'Oauth2AccessToken' )->search({ client_id => $client_id, user_id => $ac->user_id, }) ) { while ( my $row = $rs->next ) { $row->delete; } } } } return ( 0,'invalid_grant' ); } $ac->verified( 1 ); $ac->update; # scopes are those that were requested in the authorization request, not # those stored in the client (i.e. what the auth request restriced scopes # to and not everything the client is capable of) my %scope = map { $_->scope->description => 1 } $ac->oauth2_auth_code_scopes->all; return ( $client_id,undef,{ %scope },$ac->user_id ); } sub _check_password { my ( $hashed_password,$password ) = @_; die "Implement _check_password"; } sub _store_access_token { my ( %args ) = @_; my ( $c,$client,$auth_code,$access_token,$refresh_token, $expires_in,$scope,$old_refresh_token ) = @args{qw/ mojo_controller client_id auth_code access_token refresh_token expires_in scopes old_refresh_token / }; my ( $user_id ); if ( ! defined( $auth_code ) && $old_refresh_token ) { # must have generated an access token via a refresh token so revoke the # old access token and refresh token (also copy required data if missing) my $prt = $c->model->rs( 'Oauth2RefreshToken' ) ->find( $old_refresh_token ); my $pat = $c->model->rs( 'Oauth2AccessToken' ) ->find( $prt->access_token ); # access tokens can be revoked, whilst refresh tokens can remain so we # need to get the data from the refresh token as the access token may # no longer exist at the point that the refresh token is used $scope //= { map { $_->scope->description => 1 } $prt->oauth2_refresh_token_scopes->all }; $user_id = $prt->user_id; } else { my $ac = $c->model->rs( 'Oauth2AuthCode' )->find( $auth_code ); $user_id = $ac->user_id; } if ( ref( $client ) ) { $scope //= $client->{scope}; $user_id //= $client->{user_id}; $client = $client->{client_id}; } foreach my $token_type ( qw/ Access Refresh / ) { # if the client has en existing access/refresh token we need to revoke it if ( my $rs = $c->model->rs( "Oauth2${token_type}Token" )->search({ client_id => $client, user_id => $user_id, }) ) { $c->app->log->debug( "Revoking existing @{[lc $token_type]} token" ); while ( my $row = $rs->next ) { $row->delete; } } } # N.B. you should probably encrypt the access tokens and refresh tokens here $c->model->rs( 'Oauth2AccessToken' )->create({ access_token => $access_token, refresh_token => $refresh_token, client_id => $client, user_id => $user_id, expires => DateTime->from_epoch( epoch => time + $expires_in ), }); $c->model->rs( 'Oauth2RefreshToken' )->create({ refresh_token => $refresh_token, access_token => $access_token, client_id => $client, user_id => $user_id, }); foreach my $rqd_scope ( keys( %{ $scope } ) ) { if ( my $db_scope = $c->model->rs( 'Oauth2Scope' )->find({ description => $rqd_scope }) ) { foreach my $related ( qw/ access_token refresh_token / ) { # N.B. you should probably encrypt the access tokens and refresh tokens here $db_scope->create_related( "oauth2_${related}_scopes",{ allowed => $scope->{$rqd_scope}, $related => $related eq 'access_token' ? $access_token : $refresh_token, }); } } else { $c->app->log->error( "Unknown scope ($rqd_scope) in _store_access_token" ); } } return; } sub _verify_access_token { my ( %args ) = @_; my ( $c,$access_token,$scopes_ref,$is_refresh_token ) = @args{qw/ mojo_controller access_token scopes is_refresh_token /}; if ( my $rt = $c->model->rs( 'Oauth2RefreshToken' )->find( $access_token ) ) { foreach my $scope ( @{ $scopes_ref // [] } ) { my $db_scope = $c->model->rs( 'Oauth2RefreshTokenScope' )->find({ 'scope.description' => $scope, 'refresh_token' => $access_token, },{ join => [ qw/ scope / ] } ); if ( ! $db_scope || ! $db_scope->allowed ) { $c->app->log->debug( "Refresh token doesn't have scope ($scope)" ); return ( 0,'invalid_grant' ); } } return $rt->client_id; } elsif ( my $at = $c->model->rs( 'Oauth2AccessToken' )->find( $access_token ) ) { if ( $at->expires->epoch <= time ) { $c->app->log->debug( "Access token has expired" ); $at->delete; return ( 0,'invalid_grant' ); } foreach my $scope ( @{ $scopes_ref // [] } ) { my $db_scope = $c->model->rs( 'Oauth2AccessTokenScope' )->find({ 'scope.description' => $scope, 'access_token' => $access_token, },{ join => [ qw/ scope / ] } ); if ( ! $db_scope || ! $db_scope->allowed ) { $c->app->log->debug( "Access token doesn't have scope ($scope)" ); return ( 0,'invalid_grant' ); } } return { client_id => $at->client_id, user_id => $at->user_id, }; } else { $c->app->log->debug( "Access token does not exist" ); return ( 0,'invalid_grant' ); } } 1; Net-OAuth2-AuthorizationServer-0.28/examples/oauth2_schema.sql000644 000770 000120 00000011331 13432550612 025664 0ustar00leejohnsonadmin000000 000000 -- N.B some of these tables assume you have a user table with -- an id column for linking access tokens, etc, to a user -- and the use of varchar( 255 ) is unlikey to be big enough -- if you use the jwt_secret option of the plugin to make -- tokens JWTs (at which point a TEXT field would be required, -- and then that has an impact on how you defined the indexes -- and primary keys...) create table if not exists oauth2_client ( id varchar( 255 ) NOT NULL PRIMARY KEY, secret varchar( 255 ) NOT NULL, active boolean NOT NULL DEFAULT true, last_modified timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); create table if not exists oauth2_scope ( id bigint NOT NULL PRIMARY KEY, description varchar( 255 ) NOT NULL, UNIQUE KEY( description ) ); create table if not exists oauth2_client_scope ( client_id varchar( 255 ) NOT NULL, scope_id bigint NOT NULL, allowed boolean NOT NULL DEFAULT false, CONSTRAINT `oauth2_client_scope__client_id` FOREIGN KEY ( `client_id` ) REFERENCES `oauth2_client` ( `id` ) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT `oauth2_client_scope__scope_id` FOREIGN KEY ( `scope_id` ) REFERENCES `oauth2_scope` ( `id` ) ON UPDATE CASCADE ON DELETE CASCADE, PRIMARY KEY( client_id, scope_id ) ); create table if not exists oauth2_auth_code ( auth_code varchar( 255 ) NOT NULL PRIMARY KEY, client_id varchar( 255 ) NOT NULL, user_id integer( 20 ) DEFAULT NULL, expires timestamp NOT NULL, redirect_uri tinytext NOT NULL, verified boolean NOT NULL DEFAULT false, CONSTRAINT `oauth2_auth_code__client_id` FOREIGN KEY ( `client_id` ) REFERENCES `oauth2_client` ( `id` ) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT `oauth2_auth_code__user_id` FOREIGN KEY ( `user_id` ) REFERENCES `user` ( `id` ) ON UPDATE CASCADE ON DELETE CASCADE ); create table if not exists oauth2_auth_code_scope ( auth_code varchar( 255 ) NOT NULL, scope_id bigint NOT NULL, allowed boolean NOT NULL DEFAULT false, CONSTRAINT `oauth2_auth_code_scope__auth_code` FOREIGN KEY ( `auth_code` ) REFERENCES `oauth2_auth_code` ( `auth_code` ) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT `oauth2_auth_code_scope__scope_id` FOREIGN KEY ( `scope_id` ) REFERENCES `oauth2_scope` ( `id` ) ON UPDATE CASCADE ON DELETE CASCADE, PRIMARY KEY( auth_code, scope_id ) ); create table if not exists oauth2_access_token ( access_token varchar( 255 ) NOT NULL PRIMARY KEY, refresh_token varchar( 255 ) DEFAULT NULL, client_id varchar( 255 ) NOT NULL, user_id integer( 20 ) DEFAULT NULL, expires timestamp NOT NULL, CONSTRAINT `oauth2_access_token__client_id` FOREIGN KEY ( `client_id` ) REFERENCES `oauth2_client` ( `id` ) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT `oauth2_access_token__user_id` FOREIGN KEY ( `user_id` ) REFERENCES `user` ( `id` ) ON UPDATE CASCADE ON DELETE CASCADE ); create table if not exists oauth2_access_token_scope ( access_token varchar( 255 ) NOT NULL, scope_id bigint NOT NULL, allowed boolean NOT NULL DEFAULT false, CONSTRAINT `oauth2_access_token_scope__auth_code` FOREIGN KEY ( `access_token` ) REFERENCES `oauth2_access_token` ( `access_token` ) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT `oauth2_access_token_scope__scope_id` FOREIGN KEY ( `scope_id` ) REFERENCES `oauth2_scope` ( `id` ) ON UPDATE CASCADE ON DELETE CASCADE, PRIMARY KEY( access_token, scope_id ) ); create table if not exists oauth2_refresh_token ( refresh_token varchar( 255 ) NOT NULL PRIMARY KEY, access_token varchar( 255 ) NOT NULL, client_id varchar( 255 ) NOT NULL, user_id integer( 20 ) DEFAULT NULL, CONSTRAINT `oauth2_refresh_token__client_id` FOREIGN KEY ( `client_id` ) REFERENCES `oauth2_client` ( `id` ) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT `oauth2_refresh_token__user_id` FOREIGN KEY ( `user_id` ) REFERENCES `user` ( `id` ) ON UPDATE CASCADE ON DELETE CASCADE ); create table if not exists oauth2_refresh_token_scope ( refresh_token varchar( 255 ) NOT NULL, scope_id bigint NOT NULL, allowed boolean NOT NULL DEFAULT false, CONSTRAINT `oauth2_refresh_token_scope__auth_code` FOREIGN KEY ( `refresh_token` ) REFERENCES `oauth2_refresh_token` ( `refresh_token` ) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT `oauth2_refresh_token_scope__scope_id` FOREIGN KEY ( `scope_id` ) REFERENCES `oauth2_scope` ( `id` ) ON UPDATE CASCADE ON DELETE CASCADE, PRIMARY KEY( refresh_token, scope_id ) ); Net-OAuth2-AuthorizationServer-0.28/examples/oauth2_server_simple_jwt.pl000644 000770 000120 00000002023 13432550612 030001 0ustar00leejohnsonadmin000000 000000 #!/usr/bin/perl use strict; use warnings; use Mojolicious::Lite; use Mojo::JWT; plugin 'OAuth2::Server' => { jwt_secret => "Is it secret?, Is it safe?", clients => { TrendyNewService => { client_secret => 'boo', scopes => { "post_images" => 1, "annoy_friends" => 1, }, }, } }; group { # /api - must be authorized under '/api' => sub { my ( $c ) = @_; return 1 if $c->oauth; $c->render( status => 401, text => 'Unauthorized' ); return undef; }; any '/annoy_friends' => sub { shift->render( text => "Annoyed Friends" ); }; any '/post_image' => sub { shift->render( text => "Posted Image" ); }; }; any '/api/track_location' => sub { my ( $c ) = @_; $c->oauth( 'track_location' ) || return $c->render( status => 401, text => 'You cannot track location' ); $c->render( text => "Target acquired" ); }; get '/' => sub { my ( $c ) = @_; $c->render( text => "Welcome to Overly Attached Social Network" ); }; app->start; # vim: ts=2:sw=2:et Net-OAuth2-AuthorizationServer-0.28/examples/oauth2_server_db.pl000644 000770 000120 00000033316 13723636651 026235 0ustar00leejohnsonadmin000000 000000 #!/usr/bin/perl use strict; use warnings; use Mojolicious::Lite; use Mojo::JSON qw/ decode_json encode_json /; use MongoDB; use FindBin qw/ $Bin /; chdir( $Bin ); my $client = MongoDB::MongoClient->new( host => 'localhost', port => 27017, auto_reconnect => 1, ); { my $db = $client->get_database( 'oauth2' ); my $clients = $db->get_collection( 'clients' ); if ( ! $clients->find_one({ client_id => 'TrendyNewService' }) ) { $clients->insert_one({ client_id => "TrendyNewService", client_secret => "boo", scopes => { post_images => 1, track_location => 1, annoy_friends => 1, download_data => 0, } }); } } app->config( hypnotoad => { listen => [ 'https://*:3000' ] } ); helper db => sub { my $db = $client->get_database( 'oauth2' ); return $db; }; my $resource_owner_logged_in_sub = sub { my ( %args ) = @_; my $c = $args{mojo_controller}; if ( ! $c->session( 'logged_in' ) ) { # we need to redirect back to the /oauth/authorize route after # login (with the original params) my $uri = join( '?',$c->url_for('current'),$c->url_with->query ); $c->flash( 'redirect_after_login' => $uri ); $c->redirect_to( '/oauth/login' ); return 0; } return 1; }; my $resource_owner_confirm_scopes_sub = sub { my ( %args ) = @_; my ( $c,$client_id,$scopes_ref,$redirect_uri,$response_type ) = @args{ qw/ mojo_controller client_id scopes redirect_uri response_type / }; my $is_allowed = $c->flash( "oauth_${client_id}" ); # if user hasn't yet allowed the client access, or if they denied # access last time, we check [again] with the user for access if ( ! $is_allowed ) { $c->flash( client_id => $client_id ); $c->flash( scopes => $scopes_ref ); my $uri = join( '?',$c->url_for('current'),$c->url_with->query ); $c->flash( 'redirect_after_login' => $uri ); $c->redirect_to( '/oauth/confirm_scopes' ); } return ( $is_allowed,undef,$scopes_ref ); }; my $verify_client_sub = sub { my ( %args ) = @_; my ( $c,$client_id,$scopes_ref,$client_secret,$redirect_uri,$response_type ) = @args{ qw/ mojo_controller client_id scopes client_secret redirect_uri response_type / }; if ( my $client = $c->db->get_collection( 'clients' ) ->find_one({ client_id => $client_id }) ) { foreach my $scope ( @{ $scopes_ref // [] } ) { if ( ! exists( $client->{scopes}{$scope} ) ) { $c->app->log->debug( "OAuth2::Server: Client lacks scope ($scope)" ); return ( 0,'invalid_scope' ); } elsif ( ! $client->{scopes}{$scope} ) { $c->app->log->debug( "OAuth2::Server: Client cannot scope ($scope)" ); return ( 0,'access_denied' ); } } return ( 1 ); } $c->app->log->debug( "OAuth2::Server: Client ($client_id) does not exist" ); return ( 0,'unauthorized_client' ); }; my $store_auth_code_sub = sub { my ( %args ) = @_; my ( $c,$auth_code,$client_id,$expires_in,$uri,$scopes_ref ) = @args{qw/ mojo_controller auth_code client_id expires_in redirect_uri scopes / }; my $auth_codes = $c->db->get_collection( 'auth_codes' ); my $id = $auth_codes->insert_one({ auth_code => $auth_code, client_id => $client_id, user_id => $c->session( 'user_id' ), expires => time + $expires_in, redirect_uri => $uri, scope => { map { $_ => 1 } @{ $scopes_ref } }, }); return; }; my $verify_auth_code_sub = sub { my ( %args ) = @_; my ( $c,$client_id,$client_secret,$auth_code,$uri ) = @args{qw/ mojo_controller client_id client_secret auth_code redirect_uri / }; my $auth_codes = $c->db->get_collection( 'auth_codes' ); my $ac = $auth_codes->find_one({ client_id => $client_id, auth_code => $auth_code, }); my $client = $c->db->get_collection( 'clients' ) ->find_one({ client_id => $client_id }); $client || return ( 0,'unauthorized_client' ); if ( ! $ac or $ac->{verified} or ( $uri ne $ac->{redirect_uri} ) or ( $ac->{expires} <= time ) or ( $client_secret ne $client->{client_secret} ) ) { $c->app->log->debug( "OAuth2::Server: Auth code does not exist" ) if ! $ac; $c->app->log->debug( "OAuth2::Server: Client secret does not match" ) if ( $uri && $ac->{redirect_uri} ne $uri ); $c->app->log->debug( "OAuth2::Server: Auth code expired" ) if ( $ac->{expires} <= time ); $c->app->log->debug( "OAuth2::Server: Client secret does not match" ) if ( $client_secret ne $client->{client_secret} ); if ( $ac->{verified} ) { # the auth code has been used before - we must revoke the auth code # and access tokens $c->app->log->debug( "OAuth2::Server: Auth code already used to get access token" ); $auth_codes->delete_many({ auth_code => $auth_code }); $c->db->get_collection( 'access_tokens' )->delete_many({ access_token => $ac->{access_token} }); } return ( 0,'invalid_grant' ); } # scopes are those that were requested in the authorization request, not # those stored in the client (i.e. what the auth request restriced scopes # to and not everything the client is capable of) my $scope = $ac->{scope}; $auth_codes->update_one( $ac,{ '$set' => { verified => 1 } } ); return ( $client_id,undef,$scope,$ac->{user_id} ); }; my $store_access_token_sub = sub { my ( %args ) = @_; my ( $c,$client,$auth_code,$access_token,$refresh_token, $expires_in,$scope,$old_refresh_token ) = @args{qw/ mojo_controller client_id auth_code access_token refresh_token expires_in scopes old_refresh_token / }; my $access_tokens = $c->db->get_collection( 'access_tokens' ); my $refresh_tokens = $c->db->get_collection( 'refresh_tokens' ); my $user_id; if ( ! defined( $auth_code ) && $old_refresh_token ) { # must have generated an access token via a refresh token so revoke the old # access token and refresh token (also copy required data if missing) my $prt = $c->db->get_collection( 'refresh_tokens' )->find_one({ refresh_token => $old_refresh_token, }); my $pat = $c->db->get_collection( 'access_tokens' )->find_one({ access_token => $prt->{access_token}, }); # access tokens can be revoked, whilst refresh tokens can remain so we # need to get the data from the refresh token as the access token may # no longer exist at the point that the refresh token is used $scope //= $prt->{scope}; $user_id = $prt->{user_id}; $c->app->log->debug( "OAuth2::Server: Revoking old access tokens (refresh)" ); _revoke_access_token( $c,$pat->{access_token} ); } else { $user_id = $c->db->get_collection( 'auth_codes' )->find_one({ auth_code => $auth_code, })->{user_id}; } if ( ref( $client ) ) { $scope = $client->{scope}; $client = $client->{client_id}; } # if the client has en existing refresh token we need to revoke it $refresh_tokens->delete_many({ client_id => $client, user_id => $user_id }); $access_tokens->insert_one({ access_token => $access_token, scope => $scope, expires => time + $expires_in, refresh_token => $refresh_token, client_id => $client, user_id => $user_id, }); $refresh_tokens->insert_one({ refresh_token => $refresh_token, access_token => $access_token, scope => $scope, client_id => $client, user_id => $user_id, }); return; }; my $verify_access_token_sub = sub { my ( %args ) = @_; my ( $c,$access_token,$scopes_ref,$is_refresh_token ) = @args{qw/ mojo_controller access_token scopes is_refresh_token /}; my $rt = $c->db->get_collection( 'refresh_tokens' )->find_one({ refresh_token => $access_token }); if ( $is_refresh_token && $rt ) { if ( $scopes_ref ) { foreach my $scope ( @{ $scopes_ref // [] } ) { if ( ! exists( $rt->{scope}{$scope} ) or ! $rt->{scope}{$scope} ) { $c->app->log->debug( "OAuth2::Server: Refresh token does not have scope ($scope)" ); return ( 0,'invalid_grant' ); } } } return ( $rt, undef, $rt->{scope}, $rt->{user_id} ); } elsif ( my $at = $c->db->get_collection( 'access_tokens' )->find_one({ access_token => $access_token, }) ) { if ( $at->{expires} <= time ) { $c->app->log->debug( "OAuth2::Server: Access token has expired" ); _revoke_access_token( $c,$access_token ); return ( 0,'invalid_grant' ); } elsif ( $scopes_ref ) { foreach my $scope ( @{ $scopes_ref // [] } ) { if ( ! exists( $at->{scope}{$scope} ) or ! $at->{scope}{$scope} ) { $c->app->log->debug( "OAuth2::Server: Access token does not have scope ($scope)" ); return ( 0,'invalid_grant' ); } } } $c->app->log->debug( "OAuth2::Server: Access token is valid" ); return ( $at, undef, $at->{scope}, $at->{user_id} ); } $c->app->log->debug( "OAuth2::Server: Access token does not exist" ); return 0; }; sub _revoke_access_token { my ( $c,$access_token ) = @_; $c->db->get_collection( 'access_tokens' )->delete_many({ access_token => $access_token, }); } plugin 'OAuth2::Server' => { auth_code_ttl => 300, access_token_ttl => 600, login_resource_owner => $resource_owner_logged_in_sub, confirm_by_resource_owner => $resource_owner_confirm_scopes_sub, verify_client => $verify_client_sub, store_auth_code => $store_auth_code_sub, verify_auth_code => $verify_auth_code_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 ) = @_; if ( my $auth_info = $c->oauth ) { $c->stash( oauth_info => $auth_info ); return 1; } $c->render( status => 401, text => 'Unauthorized' ); return undef; }; any '/annoy_friends' => sub { my ( $c ) = @_; my $user_id = $c->stash( 'oauth_info' )->{user_id}; $c->render( text => "$user_id Annoyed Friends" ); }; any '/post_image' => sub { my ( $c ) = @_; my $user_id = $c->stash( 'oauth_info' )->{user_id}; $c->render( text => "$user_id Posted Image" ); }; }; any '/api/track_location' => sub { my ( $c ) = @_; my $auth_info = $c->oauth( 'track_location' ) || return $c->render( status => 401, text => 'You cannot track location' ); $c->render( text => "Target acquired: " . $auth_info->{user_id} ); }; get '/' => sub { my ( $c ) = @_; $c->render( text => "Welcome to Overly Attached Social Network" ); }; get '/oauth/login' => sub { my ( $c ) = @_; if ( my $redirect_uri = $c->flash( 'redirect_after_login' ) ) { $c->flash( 'redirect_after_login' => $redirect_uri ); } if ( $c->session( 'logged_in' ) ) { return $c->render( text => 'Logged in!' ) } else { return $c->render( error => undef ); } }; any '/logout' => sub { my ( $c ) = @_; $c->session( expires => 1 ); $c->redirect_to( '/' ); }; post '/oauth/login' => sub { my ( $c ) = @_; my $username = $c->param('username'); my $password = $c->param('password'); if ( my $redirect_uri = $c->flash( 'redirect_after_login' ) ) { $c->flash( 'redirect_after_login' => $redirect_uri ); } if ( $username eq 'Lee' and $password eq 'P@55w0rd' ) { $c->session( logged_in => 1 ); $c->session( user_id => $username ); if ( my $redirect_uri = $c->flash( 'redirect_after_login' ) ) { return $c->redirect_to( $redirect_uri ); } else { return $c->render( text => 'Logged in!' ) } } else { return $c->render( status => 401, error => 'Incorrect username/password', ); } }; any '/oauth/confirm_scopes' => sub { my ( $c ) = @_; # in theory we should only ever get here via a redirect from # a login (that was itself redirected to from /oauth/authorize if ( my $redirect_uri = $c->flash( 'redirect_after_login' ) ) { $c->flash( 'redirect_after_login' => $redirect_uri ); } else { return $c->render( text => "Got to /confirm_scopes without redirect_after_login?" ); } if ( $c->req->method eq 'POST' ) { my $client_id = $c->flash( 'client_id' ); my $allow = $c->param( 'allow' ); $c->flash( "oauth_${client_id}" => ( $allow eq 'Allow' ) ? 1 : 0 ); if ( my $redirect_uri = $c->flash( 'redirect_after_login' ) ) { return $c->redirect_to( $redirect_uri ); } } else { $c->flash( client_id => $c->flash( 'client_id' ) ); return $c->render( client_id => $c->flash( 'client_id' ), scopes => $c->flash( 'scopes' ), ); } }; app->secrets( ['Setec Astronomy'] ); app->sessions->cookie_name( 'oauth2_server' ); app->start; # vim: ts=2:sw=2:et __DATA__ @@ layouts/default.html.ep Overly Attached Social Network

Welcome to Overly Attached Social Network

<%== content %> @@ oauthlogin.html.ep % layout 'default'; % if ( $error ) { <%= $error %> % }

username: Lee
password: P@55w0rd

%= form_for '/oauth/login' => (method => 'POST') => begin %= label_for username => 'Username' %= text_field 'username' %= label_for password => 'Password' %= password_field 'password' %= submit_button 'Log me in', class => 'btn' % end @@ oauthconfirm_scopes.html.ep % layout 'default'; %= form_for 'confirm_scopes' => (method => 'POST') => begin <%= $client_id %> would like to be able to perform the following on your behalf:
    % for my $scope ( @{ $scopes } ) {
  • <%= $scope %>
  • % }
%= submit_button 'Allow', class => 'btn', name => 'allow' %= submit_button 'Deny', class => 'btn', name => 'allow' % end Net-OAuth2-AuthorizationServer-0.28/examples/oauth2_server_simple.pl000644 000770 000120 00000002231 13432550612 027116 0ustar00leejohnsonadmin000000 000000 #!/usr/bin/perl use strict; use warnings; use Mojolicious::Lite; plugin 'OAuth2::Server' => { clients => { TrendyNewService => { client_secret => 'boo', scopes => { "post_images" => 1, "annoy_friends" => 1, }, }, TrendyNewServiceImplicitGrant => { redirect_uri => 'https://localhost/cb', scopes => { "post_images" => 1, "annoy_friends" => 1, }, }, }, }; group { # /api - must be authorized under '/api' => sub { my ( $c ) = @_; return 1 if $c->oauth; $c->render( status => 401, text => 'Unauthorized' ); return undef; }; any '/annoy_friends' => sub { shift->render( text => "Annoyed Friends" ); }; any '/post_image' => sub { shift->render( text => "Posted Image" ); }; }; any '/api/track_location' => sub { my ( $c ) = @_; $c->oauth( 'track_location' ) || return $c->render( status => 401, text => 'You cannot track location' ); $c->render( text => "Target acquired" ); }; get '/' => sub { my ( $c ) = @_; $c->render( text => "Welcome to Overly Attached Social Network" ); }; app->start; # vim: ts=2:sw=2:et Net-OAuth2-AuthorizationServer-0.28/examples/oauth2_client.pl000644 000770 000120 00000003326 13475437655 025545 0ustar00leejohnsonadmin000000 000000 #!/usr/bin/perl use strict; use warnings; use Mojolicious::Lite; use Data::Dumper; use IO::Socket::SSL; # work around for example only (don't do this in PROD code) IO::Socket::SSL::set_defaults( SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE, ); my $host = $ENV{HOST} // '127.0.0.1'; plugin 'OAuth2', { overly_attached_social_network => { authorize_url => "https://$host:3000/oauth/authorize?response_type=code", token_url => "https://$host:3000/oauth/access_token", key => 'TrendyNewService', secret => 'boo', scope => 'post_images annoy_friends', }, }; app->helper( delay => sub { my $c = shift; my $tx = $c->render_later->tx; Mojo::IOLoop->delay(@_)->catch(sub { $c->helpers->reply->exception(pop) and undef $tx })->wait; } ); get '/auth' => sub { my $self = shift; if ( my $error = $self->param( 'error' ) ) { return $self->render( text => "Call to overly_attached_social_network returned: $error" ); } else { $self->delay( sub { my $delay = shift; $self->oauth2->get_token( overly_attached_social_network => $delay->begin ) }, sub { my( $delay,$error,$data ) = @_; return $self->render( error => $error ) if ! $data->{access_token}; return $self->render( json => $data ); }, ); } }; get '/' => sub { my ( $c ) = @_; $c->render( 'index' ); }; app->start; # vim: ts=2:sw=2:et __DATA__ @@ layouts/default.html.ep TrendyNewService

Welcome to TrendyNewService

<%== content %> @@ index.html.ep % layout 'default'; Connect to Overly Attached Social Network Net-OAuth2-AuthorizationServer-0.28/examples/oauth2_db.json000644 000770 000120 00000004561 13475437630 025205 0ustar00leejohnsonadmin000000 000000 {"access_tokens":{"MTU1OTY0MjAwOC03NDcxMTYtMC43NDg0MTUxNTg0NTc3NDMtWkVjUnR6M3BmQ00yNnZYa25TZEQyWlZnSkx3bHR3":{"client_id":"TrendyNewService","expires":1559642608,"refresh_token":"MTU1OTY0MjAwOC03NDczMjYtMC41MzgzMDc5MzM1MDU2NTYtZVZSUGNGWXV2R0dlNnBaMGtqeFR3OVFEeWdobGlk","scope":{"annoy_friends":1,"post_images":1},"user_id":"Lee"}},"auth_codes":{"MTU1OTY0MTcwNC00Nzg1OTMtMC42ODQ3NDE1MzQzMDg4ODEtak1NejI3U1VUdGhqeTE1TE1SZ21QTHZRYzIwUUts":{"client_id":"TrendyNewService","expires":1559642004,"redirect_uri":"https:\/\/127.0.0.1:3001\/auth","scope":{"annoy_friends":1,"post_images":1},"user_id":"Lee"},"MTU1OTY0MTg1My0zODY3MTEtMC4xODcwMDYyMTMwNjc4NDMtR1d2Z1o5VTE1WTBQRjdKWm5KV0pOenhMWFdqZFVl":{"client_id":"TrendyNewService","expires":1559642153,"redirect_uri":"https:\/\/127.0.0.1:3001\/auth","scope":{"annoy_friends":1,"post_images":1},"user_id":"Lee"},"MTU1OTY0MTgzNC0zNjQ1MzItMC44NTI5NjY4Mzg1ODk1MDYtOUVFaUVQd0RvZ1ZhVmdvRWNocTZNeDE1dHVLSDV6":{"client_id":"TrendyNewService","expires":1559642134,"redirect_uri":"https:\/\/127.0.0.1:3001\/auth","scope":{"annoy_friends":1,"post_images":1},"user_id":"Lee"},"MTU1OTY0MjAwOC01ODk3MDAtMC41Njk2MzcwMjM2NDcwNTgtYUNIM1dCVGNGZnJ5QUxyWnZhZGdYWm5qVWRId0cz":{"access_token":"MTU1OTY0MjAwOC03NDcxMTYtMC43NDg0MTUxNTg0NTc3NDMtWkVjUnR6M3BmQ00yNnZYa25TZEQyWlZnSkx3bHR3","client_id":"TrendyNewService","expires":1559642308,"redirect_uri":"https:\/\/127.0.0.1:3001\/auth","scope":{"annoy_friends":1,"post_images":1},"user_id":"Lee"}},"auth_codes_by_client":{"TrendyNewService":"MTU1OTY0MjAwOC01ODk3MDAtMC41Njk2MzcwMjM2NDcwNTgtYUNIM1dCVGNGZnJ5QUxyWnZhZGdYWm5qVWRId0cz"},"clients":{"TrendyNewService":{"client_secret":"boo","scopes":{"annoy_friends":true,"download_data":false,"post_images":true,"track_location":true}}},"refresh_tokens":{"MTU1OTY0MjAwOC03NDczMjYtMC41MzgzMDc5MzM1MDU2NTYtZVZSUGNGWXV2R0dlNnBaMGtqeFR3OVFEeWdobGlk":{"access_token":"MTU1OTY0MjAwOC03NDcxMTYtMC43NDg0MTUxNTg0NTc3NDMtWkVjUnR6M3BmQ00yNnZYa25TZEQyWlZnSkx3bHR3","auth_code":"MTU1OTY0MjAwOC01ODk3MDAtMC41Njk2MzcwMjM2NDcwNTgtYUNIM1dCVGNGZnJ5QUxyWnZhZGdYWm5qVWRId0cz","client_id":"TrendyNewService","scope":{"annoy_friends":1,"post_images":1},"user_id":"Lee"}},"refresh_tokens_by_client":{"TrendyNewService":"MTU1OTY0MjAwOC03NDczMjYtMC41MzgzMDc5MzM1MDU2NTYtZVZSUGNGWXV2R0dlNnBaMGtqeFR3OVFEeWdobGlk"},"verified_auth_codes":{"MTU1OTY0MjAwOC01ODk3MDAtMC41Njk2MzcwMjM2NDcwNTgtYUNIM1dCVGNGZnJ5QUxyWnZhZGdYWm5qVWRId0cz":1}}Net-OAuth2-AuthorizationServer-0.28/examples/oauth2_server_realistic.pl000644 000770 000120 00000034173 13723636127 027627 0ustar00leejohnsonadmin000000 000000 #!/usr/bin/perl use strict; use warnings; use Mojolicious::Lite; use Mojo::JSON qw/ decode_json encode_json /; use FindBin qw/ $Bin /; chdir( $Bin ); # N.B. this uses a little JSON file, which would not scale - in reality # you should be using a database of some sort my $storage_file = "oauth2_db.json"; sub save_oauth2_data { my ( $config ) = @_; my $json = encode_json( $config ); open( my $fh,'>',$storage_file ) || die "Couldn't open $storage_file for write: $!"; print $fh $json; close( $fh ); return 1; } sub load_oauth2_data { open( my $fh,'<',$storage_file ) || die "Couldn't open $storage_file for read: $!"; my $json; while ( my $line = <$fh> ) { $json .= $line; } close( $fh ); return decode_json( $json ); } app->config( hypnotoad => { listen => [ 'https://*:3000' ] } ); my $resource_owner_logged_in_sub = sub { my ( %args ) = @_; my $c = $args{mojo_controller}; if ( ! $c->session( 'logged_in' ) ) { # we need to redirect back to the /oauth/authorize route after # login (with the original params) my $uri = join( '?',$c->url_for('current'),$c->url_with->query ); $c->flash( 'redirect_after_login' => $uri ); $c->redirect_to( '/oauth/login' ); return 0; } return 1; }; my $resource_owner_confirm_scopes_sub = sub { my ( %args ) = @_; my ( $c,$client_id,$scopes_ref,$redirect_uri,$response_type ) = @args{ qw/ mojo_controller client_id scopes redirect_uri response_type / }; my $is_allowed = $c->flash( "oauth_${client_id}" ); # if user hasn't yet allowed the client access, or if they denied # access last time, we check [again] with the user for access if ( ! $is_allowed ) { $c->flash( client_id => $client_id ); $c->flash( scopes => $scopes_ref ); my $uri = join( '?',$c->url_for('current'),$c->url_with->query ); $c->flash( 'redirect_after_login' => $uri ); $c->redirect_to( '/oauth/confirm_scopes' ); } return ( $is_allowed,undef,$scopes_ref ); }; my $verify_client_sub = sub { my ( %args ) = @_; my ( $c,$client_id,$scopes_ref,$client_secret,$redirect_uri,$response_type ) = @args{ qw/ mojo_controller client_id scopes client_secret redirect_uri response_type / }; my $oauth2_data = load_oauth2_data(); if ( my $client = $oauth2_data->{clients}{$client_id} ) { foreach my $scope ( @{ $scopes_ref // [] } ) { if ( ! exists( $client->{scopes}{$scope} ) ) { $c->app->log->debug( "OAuth2::Server: Client lacks scope ($scope)" ); return ( 0,'invalid_scope' ); } elsif ( ! $client->{scopes}{$scope} ) { $c->app->log->debug( "OAuth2::Server: Client cannot scope ($scope)" ); return ( 0,'access_denied' ); } } return ( 1 ); } $c->app->log->debug( "OAuth2::Server: Client ($client_id) does not exist" ); return ( 0,'unauthorized_client' ); }; my $store_auth_code_sub = sub { my ( %args ) = @_; my ( $c,$auth_code,$client_id,$expires_in,$uri,$scopes_ref ) = @args{qw/ mojo_controller auth_code client_id expires_in redirect_uri scopes / }; my $oauth2_data = load_oauth2_data(); my $user_id = $c->session( 'user_id' ); $oauth2_data->{auth_codes}{$auth_code} = { client_id => $client_id, user_id => $user_id, expires => time + $expires_in, redirect_uri => $uri, scope => { map { $_ => 1 } @{ $scopes_ref } }, }; $oauth2_data->{auth_codes_by_client}{$client_id} = $auth_code; save_oauth2_data( $oauth2_data ); return; }; my $verify_auth_code_sub = sub { my ( %args ) = @_; my ( $c,$client_id,$client_secret,$auth_code,$uri ) = @args{qw/ mojo_controller client_id client_secret auth_code redirect_uri / }; my $oauth2_data = load_oauth2_data(); my $client = $oauth2_data->{clients}{$client_id} || return ( 0,'unauthorized_client' ); return ( 0,'invalid_grant' ) if ( $client_secret ne $client->{client_secret} ); if ( ! exists( $oauth2_data->{auth_codes}{$auth_code} ) or ! exists( $oauth2_data->{clients}{$client_id} ) or ( $client_secret ne $oauth2_data->{clients}{$client_id}{client_secret} ) or $oauth2_data->{auth_codes}{$auth_code}{access_token} or ( $uri && $oauth2_data->{auth_codes}{$auth_code}{redirect_uri} ne $uri ) or ( $oauth2_data->{auth_codes}{$auth_code}{expires} <= time ) ) { if ( $oauth2_data->{verified_auth_codes}{$auth_code} ) { # the auth code has been used before - we must revoke the auth code # and access tokens my $auth_code_data = delete( $oauth2_data->{auth_codes}{$auth_code} ); $oauth2_data = _revoke_access_token( $c,$auth_code_data->{access_token} ); save_oauth2_data( $oauth2_data ); } return ( 0,'invalid_grant' ); } # scopes are those that were requested in the authorization request, not # those stored in the client (i.e. what the auth request restriced scopes # to and not everything the client is capable of) my $scope = $oauth2_data->{auth_codes}{$auth_code}{scope}; my $user_id = $oauth2_data->{auth_codes}{$auth_code}{user_id}; $oauth2_data->{verified_auth_codes}{$auth_code} = 1; save_oauth2_data( $oauth2_data ); return ( $client_id,undef,$scope,$user_id ); }; my $store_access_token_sub = sub { my ( %args ) = @_; my ( $c,$client,$auth_code,$access_token,$refresh_token, $expires_in,$scope,$old_refresh_token ) = @args{qw/ mojo_controller client_id auth_code access_token refresh_token expires_in scopes old_refresh_token / }; my $oauth2_data = load_oauth2_data(); my $user_id; if ( ! defined( $auth_code ) && $old_refresh_token ) { # must have generated an access token via a refresh token so revoke the old # access token and refresh token and update the oauth2_data->{auth_codes} # hash to store the new one (also copy across scopes if missing) $auth_code = $oauth2_data->{refresh_tokens}{$old_refresh_token}{auth_code}; my $prev_access_token = $oauth2_data->{refresh_tokens}{$old_refresh_token}{access_token}; # access tokens can be revoked, whilst refresh tokens can remain so we # need to get the data from the refresh token as the access token may # no longer exist at the point that the refresh token is used $scope //= $oauth2_data->{refresh_tokens}{$old_refresh_token}{scope}; $user_id = $oauth2_data->{refresh_tokens}{$old_refresh_token}{user_id}; $c->app->log->debug( "OAuth2::Server: Revoking old access tokens (refresh)" ); $oauth2_data = _revoke_access_token( $c,$prev_access_token ); } else { $user_id = $oauth2_data->{auth_codes}{$auth_code}{user_id}; } if ( ref( $client ) ) { $scope = $client->{scope}; $client = $client->{client_id}; } # if the client has en existing refresh token we need to revoke it delete( $oauth2_data->{refresh_tokens}{$old_refresh_token} ) if $old_refresh_token; $oauth2_data->{access_tokens}{$access_token} = { scope => $scope, expires => time + $expires_in, refresh_token => $refresh_token, client_id => $client, user_id => $user_id, }; $oauth2_data->{refresh_tokens}{$refresh_token} = { scope => $scope, client_id => $client, user_id => $user_id, auth_code => $auth_code, access_token => $access_token, }; $oauth2_data->{auth_codes}{$auth_code}{access_token} = $access_token; $oauth2_data->{refresh_tokens_by_client}{$client} = $refresh_token; save_oauth2_data( $oauth2_data ); return; }; my $verify_access_token_sub = sub { my ( %args ) = @_; my ( $c,$access_token,$scopes_ref,$is_refresh_token ) = @args{qw/ mojo_controller access_token scopes is_refresh_token /}; my $oauth2_data = load_oauth2_data(); if ( $is_refresh_token && exists( $oauth2_data->{refresh_tokens}{$access_token} ) ) { if ( $scopes_ref ) { foreach my $scope ( @{ $scopes_ref // [] } ) { if ( ! exists( $oauth2_data->{refresh_tokens}{$access_token}{scope}{$scope} ) or ! $oauth2_data->{refresh_tokens}{$access_token}{scope}{$scope} ) { $c->app->log->debug( "OAuth2::Server: Refresh token does not have scope ($scope)" ); return ( 0,'invalid_grant' ); } } } return ( $oauth2_data->{refresh_tokens}{$access_token}, undef, $oauth2_data->{refresh_tokens}{$access_token}{scope}, $oauth2_data->{refresh_tokens}{$access_token}{user_id}, ); } if ( exists( $oauth2_data->{access_tokens}{$access_token} ) ) { if ( $oauth2_data->{access_tokens}{$access_token}{expires} <= time ) { $c->app->log->debug( "OAuth2::Server: Access token has expired" ); $oauth2_data = _revoke_access_token( $c,$access_token ); return ( 0,'invalid_grant' ); } elsif ( $scopes_ref ) { foreach my $scope ( @{ $scopes_ref // [] } ) { if ( ! exists( $oauth2_data->{access_tokens}{$access_token}{scope}{$scope} ) or ! $oauth2_data->{access_tokens}{$access_token}{scope}{$scope} ) { $c->app->log->debug( "OAuth2::Server: Access token does not have scope ($scope)" ); return ( 0,'invalid_grant' ); } } } $c->app->log->debug( "OAuth2::Server: Access token is valid" ); return ( $oauth2_data->{access_tokens}{$access_token}, undef, $oauth2_data->{access_tokens}{$access_token}{scope}, $oauth2_data->{access_tokens}{$access_token}{user_id}, ); } $c->app->log->debug( "OAuth2::Server: Access token does not exist" ); return 0; }; sub _revoke_access_token { my ( $c,$access_token ) = @_; my $oauth2_data = load_oauth2_data(); delete( $oauth2_data->{access_tokens}{$access_token} ); save_oauth2_data( $oauth2_data ); return $oauth2_data; } plugin 'OAuth2::Server' => { auth_code_ttl => 300, access_token_ttl => 600, login_resource_owner => $resource_owner_logged_in_sub, confirm_by_resource_owner => $resource_owner_confirm_scopes_sub, verify_client => $verify_client_sub, store_auth_code => $store_auth_code_sub, verify_auth_code => $verify_auth_code_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 ) = @_; if ( my $auth_info = $c->oauth ) { $c->stash( oauth_info => $auth_info ); return 1; } $c->render( status => 401, text => 'Unauthorized' ); return undef; }; any '/annoy_friends' => sub { my ( $c ) = @_; my $user_id = $c->stash( 'oauth_info' )->{user_id}; $c->render( text => "$user_id Annoyed Friends" ); }; any '/post_image' => sub { my ( $c ) = @_; my $user_id = $c->stash( 'oauth_info' )->{user_id}; $c->render( text => "$user_id Posted Image" ); }; }; any '/api/track_location' => sub { my ( $c ) = @_; my $auth_info = $c->oauth( 'track_location' ) || return $c->render( status => 401, text => 'You cannot track location' ); $c->render( text => "Target acquired: " . $auth_info->{user_id} ); }; get '/' => sub { my ( $c ) = @_; $c->render( text => "Welcome to Overly Attached Social Network" ); }; get '/oauth/login' => sub { my ( $c ) = @_; if ( my $redirect_uri = $c->flash( 'redirect_after_login' ) ) { $c->flash( 'redirect_after_login' => $redirect_uri ); } if ( $c->session( 'logged_in' ) ) { return $c->render( text => 'Logged in!' ) } else { return $c->render( error => undef ); } }; any '/logout' => sub { my ( $c ) = @_; $c->session( expires => 1 ); $c->redirect_to( '/' ); }; post '/oauth/login' => sub { my ( $c ) = @_; my $username = $c->param('username'); my $password = $c->param('password'); if ( my $redirect_uri = $c->flash( 'redirect_after_login' ) ) { $c->flash( 'redirect_after_login' => $redirect_uri ); } if ( $username eq 'Lee' and $password eq 'P@55w0rd' ) { $c->session( logged_in => 1 ); $c->session( user_id => $username ); if ( my $redirect_uri = $c->flash( 'redirect_after_login' ) ) { return $c->redirect_to( $redirect_uri ); } else { return $c->render( text => 'Logged in!' ) } } else { return $c->render( status => 401, error => 'Incorrect username/password', ); } }; any '/oauth/confirm_scopes' => sub { my ( $c ) = @_; # in theory we should only ever get here via a redirect from # a login (that was itself redirected to from /oauth/authorize if ( my $redirect_uri = $c->flash( 'redirect_after_login' ) ) { $c->flash( 'redirect_after_login' => $redirect_uri ); } else { return $c->render( text => "Got to /confirm_scopes without redirect_after_login?" ); } if ( $c->req->method eq 'POST' ) { my $client_id = $c->flash( 'client_id' ); my $allow = $c->param( 'allow' ); $c->flash( "oauth_${client_id}" => ( $allow eq 'Allow' ) ? 1 : 0 ); if ( my $redirect_uri = $c->flash( 'redirect_after_login' ) ) { return $c->redirect_to( $redirect_uri ); } } else { $c->flash( client_id => $c->flash( 'client_id' ) ); return $c->render( client_id => $c->flash( 'client_id' ), scopes => $c->flash( 'scopes' ), ); } }; app->secrets( ['Setec Astronomy'] ); app->sessions->cookie_name( 'oauth2_server' ); app->start; # vim: ts=2:sw=2:et __DATA__ @@ layouts/default.html.ep Overly Attached Social Network

Welcome to Overly Attached Social Network

<%== content %> @@ oauthlogin.html.ep % layout 'default'; % if ( $error ) { <%= $error %> % }

username: Lee
password: P@55w0rd

%= form_for '/oauth/login' => (method => 'POST') => begin %= label_for username => 'Username' %= text_field 'username' %= label_for password => 'Password' %= password_field 'password' %= submit_button 'Log me in', class => 'btn' % end @@ oauthconfirm_scopes.html.ep % layout 'default'; %= form_for 'confirm_scopes' => (method => 'POST') => begin <%= $client_id %> would like to be able to perform the following on your behalf:
    % for my $scope ( @{ $scopes } ) {
  • <%= $scope %>
  • % }
%= submit_button 'Allow', class => 'btn', name => 'allow' %= submit_button 'Deny', class => 'btn', name => 'allow' % end Net-OAuth2-AuthorizationServer-0.28/t/003_changes.t000644 000770 000120 00000000251 13432550612 023224 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; use Test::More; eval 'use Test::CPAN::Changes'; plan skip_all => 'Test::CPAN::Changes required for this test' if $@; changes_ok(); Net-OAuth2-AuthorizationServer-0.28/t/net/000755 000770 000120 00000000000 13750017447 021644 5ustar00leejohnsonadmin000000 000000 Net-OAuth2-AuthorizationServer-0.28/t/001_compiles_pod.t000644 000770 000120 00000001027 13432550612 024271 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; use Test::More; use File::Find; if(($ENV{HARNESS_PERL_SWITCHES} || '') =~ /Devel::Cover/) { plan skip_all => 'HARNESS_PERL_SWITCHES =~ /Devel::Cover/'; } my @files; find( { wanted => sub { /\.pm$/ and push @files, $File::Find::name }, no_chdir => 1 }, -e 'blib' ? 'blib' : 'lib', ); plan tests => @files * 1; for my $file (@files) { my $module = $file; $module =~ s,\.pm$,,; $module =~ s,.*/?lib/,,; $module =~ s,/,::,g; ok eval "use $module; 1", "use $module" or diag $@; } Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/000755 000770 000120 00000000000 13750017447 023046 5ustar00leejohnsonadmin000000 000000 Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/000755 000770 000120 00000000000 13750017447 027175 5ustar00leejohnsonadmin000000 000000 Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver.t000644 000770 000120 00000001570 13432550612 027356 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; use Test::Most; use_ok( 'Net::OAuth2::AuthorizationServer' ); isa_ok( my $Server = Net::OAuth2::AuthorizationServer->new( ), 'Net::OAuth2::AuthorizationServer' ); can_ok( $Server, qw/ auth_code_grant password_grant / ); isa_ok( my $Grant = $Server->auth_code_grant( clients => { foo => {} }, ), 'Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant' ); isa_ok( $Grant = $Server->password_grant( clients => { foo => {} }, ), 'Net::OAuth2::AuthorizationServer::PasswordGrant' ); isa_ok( $Grant = $Server->implicit_grant( clients => { foo => {} }, ), 'Net::OAuth2::AuthorizationServer::ImplicitGrant' ); isa_ok( $Grant = $Server->client_credentials_grant( clients => { foo => {} }, ), 'Net::OAuth2::AuthorizationServer::ClientCredentialsGrant' ); done_testing(); Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/passwordgrant.t000644 000770 000120 00000004156 13460615265 032266 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; use Test::Most; use Test::Exception; use Crypt::JWT qw/ decode_jwt /; use FindBin qw/ $Bin /; use lib "$Bin"; use passwordgrant_tests; use_ok( 'Net::OAuth2::AuthorizationServer::PasswordGrant' ); throws_ok( sub { Net::OAuth2::AuthorizationServer::PasswordGrant->new; }, qr/requires either clients or overrides/, 'constructor with no args throws' ); my $Grant; foreach my $with_callbacks ( 0,1 ) { isa_ok( $Grant = Net::OAuth2::AuthorizationServer::PasswordGrant->new( jwt_secret => 'Some Secret Key', clients => passwordgrant_tests::clients(), users => passwordgrant_tests::users(), # am passing in a reference to the modules subs to ensure we hit # the code paths to call callbacks ( $with_callbacks ? ( passwordgrant_tests::callbacks( $Grant ) ) : () ), ), 'Net::OAuth2::AuthorizationServer::PasswordGrant' ); my $access_token = passwordgrant_tests::run_tests( $Grant,{ token_format_tests => \&token_format_tests, cannot_revoke => 1, # because we set jwt_secret } ); my ( $res,$error ) = $Grant->verify_token_and_scope( auth_header => undef, scopes => [ qw/ eat sleep / ], refresh_token => 0, ); ok( ! $res,'->verify_token_and_scope, no auth header' ); is( $error,'invalid_request','has error' ); chop( $access_token ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ eat sleep / ], is_refresh_token => 0, ); ok( ! $res,'->verify_access_token, access token revoked' ); is( $error,'invalid_grant','has error' ); } done_testing(); sub token_format_tests { my ( $token,$type ) = @_; like( $token,qr/\./,'token looks like a JWT' ); cmp_deeply( decode_jwt( alg => 'HS256', key => 'Some Secret Key', token => $token ), { 'aud' => $type eq 'auth' ? 'https://come/back' : undef, 'client' => 'test_client', ( $type eq 'refresh' ? () : ( 'exp' => ignore() ) ), 'iat' => ignore(), 'jti' => ignore(), 'scopes' => [ 'eat', 'sleep' ], 'type' => $type, 'user_id' => 'test_user', }, 'auth code decodes correctly', ); } Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/clientcredentialsgrant_no_jwt.t000644 000770 000120 00000001362 13432550612 035465 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; use Test::Most; use Test::Exception; use FindBin qw/ $Bin /; use lib "$Bin"; use clientcredentialsgrant_tests; use_ok( 'Net::OAuth2::AuthorizationServer::ClientCredentialsGrant' ); my $Grant; foreach my $with_callbacks ( 0,1 ) { isa_ok( $Grant = Net::OAuth2::AuthorizationServer::ClientCredentialsGrant->new( clients => clientcredentialsgrant_tests::clients(), # am passing in a reference to the modules subs to ensure we hit # the code paths to call callbacks ( $with_callbacks ? ( clientcredentialsgrant_tests::callbacks( $Grant ) ) : () ), ), 'Net::OAuth2::AuthorizationServer::ClientCredentialsGrant' ); clientcredentialsgrant_tests::run_tests( $Grant ); } done_testing(); Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/authorizationcodegrant_tests.pm000644 000770 000120 00000017542 13750017020 035540 0ustar00leejohnsonadmin000000 000000 package authorizationcodegrant_tests; use strict; use warnings; use Test::Most; use Test::Exception; sub callbacks { my ( $Grant ) = @_; return ( verify_client_cb => sub { return Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant::_verify_client( $Grant,@_ ) }, store_auth_code_cb => sub { return Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant::_store_auth_code( $Grant,@_ ) }, verify_auth_code_cb => sub { return Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant::_verify_auth_code( $Grant,@_ ) }, store_access_token_cb => sub { return Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant::_store_access_token( $Grant,@_ ) }, verify_access_token_cb => sub { return Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant::_verify_access_token( $Grant,@_ ) }, login_resource_owner_cb => sub { return Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant::_login_resource_owner( $Grant,@_ ) }, confirm_by_resource_owner_cb => sub { return Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant::_confirm_by_resource_owner( $Grant,@_ ) }, ); } sub clients { return { test_client => { client_secret => 'letmein', scopes => { eat => 1, drink => 0, sleep => 1, }, }, }; } sub run_tests { my ( $Grant,$args ) = @_; $args //= {}; can_ok( $Grant, qw/ clients / ); ok( $Grant->login_resource_owner,'login_resource_owner' ); my ( $confirmed,$confirm_error,$scopes_ref ) = $Grant->confirm_by_resource_owner( client_id => 'test_client', scopes => [ qw/ eat sleep / ], ); ok( $confirmed,'confirm_by_resource_owner' ); ok( !$confirm_error,' ... no error' ); cmp_deeply( $scopes_ref,[ qw/ eat sleep / ],' ... returned scopes ref' ); note( "verify_client" ); my %valid_client = ( client_id => 'test_client', scopes => $scopes_ref, ); my ( $res,$error ) = $Grant->verify_client( %valid_client ); ok( $res,'->verify_client, allowed scopes' ); ok( ! $error,'has no error' ); foreach my $t ( [ { scopes => [ qw/ eat sleep yawn / ] },'invalid_scope','invalid scopes' ], [ { client_id => 'another_client' },'unauthorized_client','invalid client' ], ) { ( $res,$error ) = $Grant->verify_client( %valid_client,%{ $t->[0] } ); ok( ! $res,'->verify_client, ' . $t->[2] ); is( $error,$t->[1],'has error' ); } foreach my $t ( [ { scopes => [ qw/ eat sleep drink / ] },[ qw / eat sleep / ],'disallowed scopes' ], ) { my $scopes; ( $res, $error, $scopes ) = $Grant->verify_client( %valid_client,%{ $t->[0] } ); ok ( $res, '->verify_client, ' . $t->[2] ); cmp_deeply( $scopes, $t->[1], 'has reduced scopes' ); } note( "store_auth_code" ); ok( my $auth_code = $Grant->token( client_id => 'test_client', scopes => $scopes_ref, type => 'auth', redirect_uri => 'https://come/back', user_id => 1, ),'->token (auth code)' ); $args->{token_format_tests}->( $auth_code,'auth' ) if $args->{token_format_tests}; ok( $Grant->store_auth_code( client_id => 'test_client', auth_code => $auth_code, redirect_uri => 'https://come/back', scopes => $scopes_ref, ),'->store_auth_code' ); note( "verify_auth_code" ); my %valid_auth_code = ( client_id => 'test_client', client_secret => 'letmein', auth_code => $auth_code, redirect_uri => 'https://come/back', ); my ( $client,$vac_error,$scopes,$user_id ) = $Grant->verify_auth_code( %valid_auth_code ); ok( $client,'->verify_auth_code, correct args' ); ok( ! $vac_error,'has no error' ); is( $user_id,$args->{no_jwt} ? undef : 1,'user_id' ); cmp_deeply( $scopes,[ qw/ eat sleep / ],'has scopes' ); foreach my $t ( [ { client_id => 'another_client' },'unauthorized_client','invalid client' ], [ { client_secret => 'bad secret' },'invalid_grant','bad client secret' ], [ { redirect_uri => 'http://not/this' },'invalid_grant','bad redirect uri' ], ) { ( $client,$vac_error,$scopes ) = $Grant->verify_auth_code( %valid_auth_code,%{ $t->[0] }, ); ok( ! $client,'->verify_auth_code, ' . $t->[2] ); is( $vac_error,$t->[1],'has error' ); ok( ! $scopes,'has no scopes' ); } my $og_auth_code = $auth_code; chop( $auth_code ); ( $client,$vac_error,$scopes ) = $Grant->verify_auth_code( %valid_auth_code, auth_code => $auth_code, ); ok( ! $client,'->verify_auth_code, token fiddled with' ); is( $vac_error,'invalid_grant','has error' ); ok( ! $scopes,'has no scopes' ); note( "store_access_token" ); ok( my $access_token = $Grant->token( client_id => 'test_client', scopes => $scopes_ref, type => 'access', user_id => 1, ),'->token (access token)' ); $args->{token_format_tests}->( $access_token,'access' ) if $args->{token_format_tests}; ok( my $refresh_token = $Grant->token( client_id => 'test_client', scopes => $scopes_ref, type => 'refresh', user_id => 1, ),'->token (refresh token)' ); $args->{token_format_tests}->( $refresh_token,'refresh' ) if $args->{token_format_tests}; ok( $Grant->store_access_token( client_id => 'test_client', auth_code => $og_auth_code, access_token => $access_token, refresh_token => $refresh_token, scopes => $scopes_ref, ),'->store_access_token' ); note( "verify_access_token" ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => $scopes_ref, is_refresh_token => 0, ); ok( $res,'->verify_access_token, valid access token' ); ok( ! $error,'has no error' ); ( $res,$error ) = $Grant->verify_access_token( access_token => $refresh_token, scopes => $scopes_ref, is_refresh_token => 1, ); ok( $res,'->verify_access_token, valid refresh token' ); ok( ! $error,'has no error' ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ drink / ], is_refresh_token => 0, ); ok( ! $res,'->verify_access_token, invalid scope' ); is( $error,'invalid_grant','has error' ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ drink / ], is_refresh_token => 1, ); ok( ! $res,'->verify_access_token, refresh token is not access token' ); is( $error,'invalid_grant','has error' ); ( $res,$error ) = $Grant->verify_token_and_scope( auth_header => "Bearer ", scopes => [ qw/ drink / ], is_refresh_token => 0, ); ok( ! $res,'->verify_access_token, empty Bearer token' ); is( $error,'invalid_request','has error' ); ( $res,$error ) = $Grant->verify_token_and_scope( auth_header => "Bearer $access_token", scopes => $scopes_ref, is_refresh_token => 0, ); ok( $res,'->verify_token_and_scope, valid access token' ); ok( ! $error,'has no error' ); ( $res,$error ) = $Grant->verify_token_and_scope( auth_header => "Bearer $access_token", scopes => $scopes_ref, refresh_token => $refresh_token, ); ok( $res,'->verify_token_and_scope, valid refresh token' ); ok( ! $error,'has no error' ); my $og_access_token = $access_token; chop( $access_token ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => $scopes_ref, is_refresh_token => 0, ); ok( ! $res,'->verify_access_token, token fiddled with' ); is( $error,'invalid_grant','has error' ); unless ( $args->{cannot_revoke} ) { note( "verify_auth_code" ); ( $client,$vac_error,$scopes ) = $Grant->verify_auth_code( %valid_auth_code ); ok( ! $client,'->verify_auth_code, correct args but second time' ); is( $vac_error,'invalid_grant','has no error' ); ok( ! $scopes,'has no scopes' ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => $scopes_ref, is_refresh_token => 0, ); ok( ! $res,'->verify_access_token, access token revoked' ); is( $error,'invalid_grant','has error' ); } return $og_access_token; } 1; Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/implicitgrant_tests.pm000644 000770 000120 00000011322 13432550612 033613 0ustar00leejohnsonadmin000000 000000 package implicitgrant_tests; use strict; use warnings; use Test::Most; use Test::Exception; sub callbacks { my ( $Grant ) = @_; return ( verify_client_cb => sub { return Net::OAuth2::AuthorizationServer::ImplicitGrant::_verify_client( $Grant,@_ ) }, store_access_token_cb => sub { return Net::OAuth2::AuthorizationServer::ImplicitGrant::_store_access_token( $Grant,@_ ) }, verify_access_token_cb => sub { return Net::OAuth2::AuthorizationServer::ImplicitGrant::_verify_access_token( $Grant,@_ ) }, ); } sub clients { return { test_client => { scopes => { eat => 1, drink => 0, sleep => 1, }, }, test_client_with_redirect_uri => { redirect_uri => 'https://foo.bar.baz.com', scopes => { eat => 1, drink => 0, sleep => 1, }, }, test_client_must_use_auth_code => { client_secret => 'weeee', scopes => { eat => 1, drink => 0, sleep => 1, }, }, }; } sub run_tests { my ( $Grant,$args ) = @_; $args //= {}; can_ok( $Grant, qw/ clients / ); note( "verify_client" ); my %invalid_client = ( client_id => 'test_client_must_use_auth_code', scopes => [ qw/ eat sleep / ], ); my ( $res,$error ) = $Grant->verify_client( %invalid_client ); ok( ! $res,'->verify_client, client cannot use implicit grant' ); is( $error,'unauthorized_client','has error' ); %invalid_client = ( client_id => 'test_client_with_redirect_uri', scopes => [ qw/ eat sleep / ], ); ( $res,$error ) = $Grant->verify_client( %invalid_client ); ok( ! $res,'->verify_client, lacks redirect_uri' ); is( $error,'invalid_request','has error' ); $invalid_client{'redirect_uri'} = 'http://not.right.com'; ( $res,$error ) = $Grant->verify_client( %invalid_client ); ok( ! $res,'->verify_client, incorrect redirect_uri' ); is( $error,'invalid_request','has error' ); $invalid_client{'redirect_uri'} = 'https://foo.bar.baz.com'; ( $res,$error ) = $Grant->verify_client( %invalid_client ); ok( $res,'->verify_client, correct redirect_uri' ); ok( ! $error,'has no error' ); my %valid_client = ( client_id => 'test_client', scopes => [ qw/ eat sleep / ], ); ( $res,$error ) = $Grant->verify_client( %valid_client ); ok( $res,'->verify_client, allowed scopes' ); ok( ! $error,'has no error' ); foreach my $t ( [ { scopes => [ qw/ eat sleep yawn / ] },'invalid_scope','invalid scopes' ], [ { client_id => 'another_client' },'unauthorized_client','invalid client' ], ) { ( $res,$error ) = $Grant->verify_client( %valid_client,%{ $t->[0] } ); ok( ! $res,'->verify_client, ' . $t->[2] ); is( $error,$t->[1],'has error' ); } foreach my $t ( [ { scopes => [ qw/ eat sleep drink / ] },[ qw / eat sleep / ],'disallowed scopes' ], ) { my $scopes; ( $res, $error, $scopes ) = $Grant->verify_client( %valid_client,%{ $t->[0] } ); ok ( $res, '->verify_client, ' . $t->[2] ); cmp_deeply( $scopes, $t->[1], 'has reduced scopes' ); } note( "store_access_token" ); ok( my $access_token = $Grant->token( client_id => 'test_client', scopes => [ qw/ eat sleep / ], type => 'access', user_id => 1, ),'->token (access token)' ); $args->{token_format_tests}->( $access_token,'access' ) if $args->{token_format_tests}; ok( $Grant->store_access_token( client_id => 'test_client', access_token => $access_token, scopes => [ qw/ eat sleep / ], ),'->store_access_token' ); note( "verify_access_token" ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ eat sleep / ], ); ok( $res,'->verify_access_token, valid access token' ); ok( ! $error,'has no error' ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ drink / ], ); ok( ! $res,'->verify_access_token, invalid scope' ); is( $error,'invalid_grant','has error' ); ( $res,$error ) = $Grant->verify_token_and_scope( auth_header => "Bearer $access_token", scopes => [ qw/ eat sleep / ], ); ok( $res,'->verify_token_and_scope, valid access token' ); ok( ! $error,'has no error' ); my $og_access_token = $access_token; chop( $access_token ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ eat sleep / ], ); ok( ! $res,'->verify_access_token, token fiddled with' ); is( $error,'invalid_grant','has error' ); unless ( $args->{cannot_revoke} ) { ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ eat sleep / ], ); ok( ! $res,'->verify_access_token, access token revoked' ); is( $error,'invalid_grant','has error' ); } return $og_access_token; } 1; Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/passwordgrant_tests.pm000644 000770 000120 00000012151 13432550612 033644 0ustar00leejohnsonadmin000000 000000 package passwordgrant_tests; use strict; use warnings; use Test::Most; use Test::Exception; use authorizationcodegrant_tests; sub callbacks { my ( $Grant ) = @_; return ( verity_user_password_cb => sub { return Net::OAuth2::AuthorizationServer::PasswordGrant::_verify_user_password( $Grant,@_ ) }, store_access_token_cb => sub { return Net::OAuth2::AuthorizationServer::PasswordGrant::_store_access_token( $Grant,@_ ) }, verify_access_token_cb => sub { return Net::OAuth2::AuthorizationServer::PasswordGrant::_verify_access_token( $Grant,@_ ) }, login_resource_owner_cb => sub { return Net::OAuth2::AuthorizationServer::PasswordGrant::_login_resource_owner( $Grant,@_ ) }, confirm_by_resource_owner_cb => sub { return Net::OAuth2::AuthorizationServer::PasswordGrant::_confirm_by_resource_owner( $Grant,@_ ) }, ); } sub clients { return authorizationcodegrant_tests::clients(); } sub users { return { test_user => 'reallyletmein', }; } sub run_tests { my ( $Grant,$args ) = @_; $args //= {}; can_ok( $Grant, qw/ clients / ); ok( $Grant->login_resource_owner,'login_resource_owner' ); my ( $confirmed,$confirm_error,$scopes_ref ) = $Grant->confirm_by_resource_owner( client_id => 'test_client', scopes => [ qw/ eat sleep / ], ); ok( $confirmed,'confirm_by_resource_owner' ); ok( !$confirm_error,' ... no error' ); cmp_deeply( $scopes_ref,[ qw/ eat sleep / ],' ... returned scopes ref' ); note( "verify_user_password" ); my %valid_user_password = ( client_id => 'test_client', client_secret => 'letmein', username => 'test_user', password => 'reallyletmein', scopes => $scopes_ref, ); my ( $og_client,$vac_error,$scopes,$user_id ) = $Grant->verify_user_password( %valid_user_password ); ok( $og_client,'->verify_user_password, correct args' ); ok( ! $vac_error,'has no error' ); cmp_deeply( $scopes,$scopes_ref,'has scopes' ); foreach my $t ( [ { client_id => 'another_client' },'unauthorized_client','invalid client' ], [ { client_secret => 'bad secret' },'invalid_grant','bad client secret' ], [ { username => 'i_do_not_exist' },'invalid_grant','bad username' ], [ { password => 'bad_password' },'invalid_grant','bad password' ], ) { my ( $rt_client,$vac_error,$scopes ) = $Grant->verify_user_password( %valid_user_password,%{ $t->[0] }, ); ok( ! $rt_client,'->verify_user_password, ' . $t->[2] ); is( $vac_error,$t->[1],'has error' ); ok( ! $scopes,'has no scopes' ); } my $client = $og_client; note( "store_access_token" ); ok( my $access_token = $Grant->token( client_id => 'test_client', scopes => $scopes_ref, type => 'access', user_id => $user_id, ),'->token (access token)' ); $args->{token_format_tests}->( $access_token,'access' ) if $args->{token_format_tests}; ok( my $refresh_token = $Grant->token( client_id => 'test_client', scopes => $scopes_ref, type => 'refresh', user_id => $user_id, ),'->token (refresh token)' ); $args->{token_format_tests}->( $refresh_token,'refresh' ) if $args->{token_format_tests}; ok( $Grant->store_access_token( client_id => 'test_client', access_token => $access_token, refresh_token => $refresh_token, scopes => $scopes_ref, ),'->store_access_token' ); note( "verify_access_token" ); my ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => $scopes_ref, is_refresh_token => 0, ); ok( $res,'->verify_access_token, valid access token' ); ok( ! $error,'has no error' ); ( $res,$error ) = $Grant->verify_access_token( access_token => $refresh_token, scopes => $scopes_ref, is_refresh_token => 1, ); ok( $res,'->verify_access_token, valid refresh token' ); ok( ! $error,'has no error' ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ drink / ], is_refresh_token => 0, ); ok( ! $res,'->verify_access_token, invalid scope' ); is( $error,'invalid_grant','has error' ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ drink / ], is_refresh_token => 1, ); ok( ! $res,'->verify_access_token, refresh token is not access token' ); is( $error,'invalid_grant','has error' ); ( $res,$error ) = $Grant->verify_token_and_scope( auth_header => "Bearer $access_token", scopes => $scopes_ref, is_refresh_token => 0, ); ok( $res,'->verify_token_and_scope, valid access token' ); ok( ! $error,'has no error' ); ( $res,$error ) = $Grant->verify_token_and_scope( auth_header => "Bearer $access_token", scopes => $scopes_ref, refresh_token => $refresh_token, ); ok( $res,'->verify_token_and_scope, valid refresh token' ); ok( ! $error,'has no error' ); my $og_access_token = $access_token; chop( $access_token ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => $scopes_ref, is_refresh_token => 0, ); ok( ! $res,'->verify_access_token, token fiddled with' ); is( $error,'invalid_grant','has error' ); return $og_access_token; } 1; Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/authorizationcodegrant.t000644 000770 000120 00000004174 13460615265 034157 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; use Test::Most; use Test::Exception; use Crypt::JWT qw/ decode_jwt /; use FindBin qw/ $Bin /; use lib "$Bin"; use authorizationcodegrant_tests; use_ok( 'Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant' ); throws_ok( sub { Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant->new; }, qr/requires either clients or overrides/, 'constructor with no args throws' ); my $Grant; foreach my $with_callbacks ( 0,1 ) { isa_ok( $Grant = Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant->new( jwt_secret => 'Some Secret Key', clients => authorizationcodegrant_tests::clients(), # am passing in a reference to the modules subs to ensure we hit # the code paths to call callbacks ( $with_callbacks ? ( authorizationcodegrant_tests::callbacks( $Grant ) ) : () ), ), 'Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant' ); my $access_token = authorizationcodegrant_tests::run_tests( $Grant,{ token_format_tests => \&token_format_tests, cannot_revoke => 1, # because we set jwt_secret } ); my ( $res,$error ) = $Grant->verify_token_and_scope( auth_header => undef, scopes => [ qw/ eat sleep / ], refresh_token => 0, ); ok( ! $res,'->verify_token_and_scope, no auth header' ); is( $error,'invalid_request','has error' ); chop( $access_token ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ eat sleep / ], is_refresh_token => 0, ); ok( ! $res,'->verify_access_token, access token revoked' ); is( $error,'invalid_grant','has error' ); } done_testing(); sub token_format_tests { my ( $token,$type ) = @_; like( $token,qr/\./,'token looks like a JWT' ); cmp_deeply( decode_jwt( alg => 'HS256', key => 'Some Secret Key', token => $token ), { 'aud' => $type eq 'auth' ? 'https://come/back' : undef, 'client' => 'test_client', ( $type eq 'refresh' ? () : ( 'exp' => ignore() ) ), 'iat' => ignore(), 'jti' => ignore(), 'scopes' => [ 'eat', 'sleep' ], 'type' => $type, 'user_id' => 1 }, 'auth code decodes correctly', ); } Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/implicitgrant.t000644 000770 000120 00000004064 13460615265 032234 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; use Test::Most; use Test::Exception; use Crypt::JWT qw/ decode_jwt /; use FindBin qw/ $Bin /; use lib "$Bin"; use implicitgrant_tests; use_ok( 'Net::OAuth2::AuthorizationServer::ImplicitGrant' ); throws_ok( sub { Net::OAuth2::AuthorizationServer::ImplicitGrant->new; }, qr/requires either clients or overrides/, 'constructor with no args throws' ); my $Grant; foreach my $with_callbacks ( 0,1 ) { isa_ok( $Grant = Net::OAuth2::AuthorizationServer::ImplicitGrant->new( jwt_secret => 'Some Secret Key', clients => implicitgrant_tests::clients(), # am passing in a reference to the modules subs to ensure we hit # the code paths to call callbacks ( $with_callbacks ? ( implicitgrant_tests::callbacks( $Grant ) ) : () ), ), 'Net::OAuth2::AuthorizationServer::ImplicitGrant' ); my $access_token = implicitgrant_tests::run_tests( $Grant,{ token_format_tests => \&token_format_tests, cannot_revoke => 1, # because we set jwt_secret } ); my ( $res,$error ) = $Grant->verify_token_and_scope( auth_header => undef, scopes => [ qw/ eat sleep / ], refresh_token => 0, ); ok( ! $res,'->verify_token_and_scope, no auth header' ); is( $error,'invalid_request','has error' ); chop( $access_token ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ eat sleep / ], is_refresh_token => 0, ); ok( ! $res,'->verify_access_token, access token revoked' ); is( $error,'invalid_grant','has error' ); } done_testing(); sub token_format_tests { my ( $token,$type ) = @_; like( $token,qr/\./,'token looks like a JWT' ); cmp_deeply( decode_jwt( alg => 'HS256', key => 'Some Secret Key', token => $token ), { 'aud' => $type eq 'auth' ? 'https://come/back' : undef, 'client' => 'test_client', ( $type eq 'refresh' ? () : ( 'exp' => ignore() ) ), 'iat' => ignore(), 'jti' => ignore(), 'scopes' => [ 'eat', 'sleep' ], 'type' => $type, 'user_id' => 1 }, 'auth code decodes correctly', ); } Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/defaults.t000644 000770 000120 00000007750 13705304306 031173 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; package Test::Defaults; use Moo; with 'Net::OAuth2::AuthorizationServer::Defaults'; use Test::Most; use Test::Exception; use Crypt::JWT qw/ decode_jwt /; isa_ok( my $t = Test::Defaults->new,'Test::Defaults' ); throws_ok( sub { $t->_uses_auth_codes }, qr/You must override _uses_auth_codes/, '_uses_auth_codes must be overriden in subclass', ); ok( ! $t->_has_clients,'! _has_clients' ); my ( $res,$error ) = $t->verify_token_and_scope( auth_header => "Foo Bar" ); is( $error,'invalid_request','verify_token_and_scope with bad auth_header' ); no warnings 'redefine'; no warnings 'once'; *Test::Defaults::_uses_auth_codes = sub { 0 }; *Test::Defaults::jwt_secret = sub { 'Some Secret Key' }; my $jwt = $t->token( client_id => 1, scopes => [ qw/ eat sleep / ], type => 'access', redirect_uri => 'https://foo.com/cb', user_id => 2, jwt_claims_cb => sub { my ( $args ) = @_; return ( user_id => $args->{user_id} + 1, iss => "some iss", sub => "not the passed user_id", ); } ); my $details = decode_jwt( alg => 'HS256', key => 'Some Secret Key', token => $jwt ); cmp_deeply( $details, { 'exp' => ignore(), 'type' => 'access', 'aud' => 'https://foo.com/cb',, 'client' => 1, 'user_id' => 3, 'iat' => re( '^\d{10}$' ), 'jti' => re( '^.{32}$' ), 'iss' => "some iss", 'sub' => "not the passed user_id", 'scopes' => [ 'eat', 'sleep', ] }, 'jwt_claims_cb used (JWT)', ); SKIP: { skip "Couldn't load Mojo::JWT: $@", 1 unless eval 'use Mojo::JWT 0.04; 1'; my $mj_details = Mojo::JWT->new( secret => 'Some Secret Key' )->decode( $jwt ); cmp_deeply( $details,$mj_details,'backwards compat with Mojo::JWT' ); } *Test::Defaults::jwt_algorithm = sub { 'PBES2-HS512+A256KW' }; *Test::Defaults::jwt_encryption = sub { 'A256CBC-HS512' }; my $jwe = $t->token( client_id => 1, scopes => [ qw/ eat sleep / ], type => 'access', redirect_uri => 'https://foo.com/cb', user_id => 2, jwt_claims_cb => sub { my ( $args ) = @_; return ( user_id => $args->{user_id} + 1, iss => "some iss", sub => "not the passed user_id", ); } ); $details = decode_jwt( alg => 'PBES2-HS512+A256KW', key => 'Some Secret Key', token => $jwe ); cmp_deeply( $details, { 'exp' => ignore(), 'type' => 'access', 'aud' => 'https://foo.com/cb',, 'client' => 1, 'user_id' => 3, 'iat' => re( '^\d{10}$' ), 'jti' => re( '^.{32}$' ), 'iss' => "some iss", 'sub' => "not the passed user_id", 'scopes' => [ 'eat', 'sleep', ] }, 'jwt_claims_cb used (JWE)', ); *Test::Defaults::jwt_algorithm = sub { 'none' }; dies_ok( sub { $t->token( client_id => 1, scopes => [ qw/ eat sleep / ], type => 'access', redirect_uri => 'https://foo.com/cb', user_id => 2, jwt_claims_cb => sub { my ( $args ) = @_; return ( user_id => $args->{user_id} + 1, iss => "some iss", sub => "not the passed user_id", ); } ); }, 'algorithm none throws exception' ); subtest '->access_token_ttl' => sub { subtest 'Int' => sub { my $Defaults = Test::Defaults->new( access_token_ttl => 999, ); is $Defaults->access_token_ttl, 999, 'attribute is numeric'; is $Defaults->get_access_token_ttl(), 999, '->get_access_token_ttl() returned default numeric TTL'; }; subtest 'CodeRef' => sub { my $Defaults = Test::Defaults->new( access_token_ttl => sub { my ( %args ) = @_; return { Test => 12345, }->{ $args{client_id} // '' } // 42; }, ); is ref( $Defaults->access_token_ttl ), 'CODE', 'attribute is code ref'; is $Defaults->get_access_token_ttl(), 42, '->get_access_token_ttl() returned our default TTL'; is $Defaults->get_access_token_ttl( client_id => 'Test' ), 12345, '->get_access_token_ttl() returned our custom TTL'; }; }; like( $@,qr/'none' is not supported/,'with expected error' ); done_testing(); Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/clientcredentialsgrant_tests.pm000644 000770 000120 00000010167 13432550612 035503 0ustar00leejohnsonadmin000000 000000 package clientcredentialsgrant_tests; use strict; use warnings; use Test::Most; use Test::Exception; sub callbacks { my ( $Grant ) = @_; return ( verify_client_cb => sub { return Net::OAuth2::AuthorizationServer::ClientCredentialsGrant::_verify_client( $Grant,@_ ) }, store_access_token_cb => sub { return Net::OAuth2::AuthorizationServer::ClientCredentialsGrant::_store_access_token( $Grant,@_ ) }, verify_access_token_cb => sub { return Net::OAuth2::AuthorizationServer::ClientCredentialsGrant::_verify_access_token( $Grant,@_ ) }, ); } sub clients { return { test_client => { client_secret => 'weeee', scopes => { eat => 1, drink => 0, sleep => 1, }, }, }; } sub run_tests { my ( $Grant,$args ) = @_; $args //= {}; can_ok( $Grant, qw/ clients / ); note( "verify_client" ); # no client_secret my %invalid_client = ( client_id => 'test_client', scopes => [ qw/ eat sleep / ], ); my ( $res,$error ) = $Grant->verify_client( %invalid_client ); ok( ! $res,'->verify_client, missing client_secret' ); is( $error,'invalid_grant','has error' ); # bad client_secret %invalid_client = ( client_id => 'test_client', client_secret => 'woooo', scopes => [ qw/ eat sleep / ], ); ( $res,$error ) = $Grant->verify_client( %invalid_client ); ok( ! $res,'->verify_client, bad client_secret' ); is( $error,'invalid_grant','has error' ); my %valid_client = ( client_id => 'test_client', client_secret => 'weeee', scopes => [ qw/ eat sleep / ], ); ( $res,$error ) = $Grant->verify_client( %valid_client ); ok( $res,'->verify_client, allowed scopes' ); ok( ! $error,'has no error' ) || diag( $error ); foreach my $t ( [ { scopes => [ qw/ eat sleep yawn / ] },'invalid_scope','invalid scopes' ], [ { client_id => 'another_client' },'unauthorized_client','invalid client' ], ) { ( $res,$error ) = $Grant->verify_client( %valid_client,%{ $t->[0] } ); ok( ! $res,'->verify_client, ' . $t->[2] ); is( $error,$t->[1],'has error' ); } foreach my $t ( [ { scopes => [ qw/ eat sleep drink / ] },[ qw / eat sleep / ],'disallowed scopes' ], ) { my $scopes; ( $res, $error, $scopes ) = $Grant->verify_client( %valid_client,%{ $t->[0] } ); ok ( $res, '->verify_client, ' . $t->[2] ); cmp_deeply( $scopes, $t->[1], 'has reduced scopes' ); } note( "store_access_token" ); ok( my $access_token = $Grant->token( client_id => 'test_client', scopes => [ qw/ eat sleep / ], type => 'access', user_id => 1, ),'->token (access token)' ); $args->{token_format_tests}->( $access_token,'access' ) if $args->{token_format_tests}; ok( $Grant->store_access_token( client_id => 'test_client', access_token => $access_token, scopes => [ qw/ eat sleep / ], ),'->store_access_token' ); note( "verify_access_token" ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ eat sleep / ], ); ok( $res,'->verify_access_token, valid access token' ); ok( ! $error,'has no error' ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ drink / ], ); ok( ! $res,'->verify_access_token, invalid scope' ); is( $error,'invalid_grant','has error' ); ( $res,$error ) = $Grant->verify_token_and_scope( auth_header => "Bearer $access_token", scopes => [ qw/ eat sleep / ], ); ok( $res,'->verify_token_and_scope, valid access token' ); ok( ! $error,'has no error' ); my $og_access_token = $access_token; chop( $access_token ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ eat sleep / ], ); ok( ! $res,'->verify_access_token, token fiddled with' ); is( $error,'invalid_grant','has error' ); unless ( $args->{cannot_revoke} ) { ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ eat sleep / ], ); ok( ! $res,'->verify_access_token, access token revoked' ); is( $error,'invalid_grant','has error' ); } return $og_access_token; } 1; Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/authorizationcodegrant_no_jwt.t000644 000770 000120 00000001377 13461017712 035533 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; use Test::Most; use Test::Exception; use FindBin qw/ $Bin /; use lib "$Bin"; use authorizationcodegrant_tests; use_ok( 'Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant' ); my $Grant; foreach my $with_callbacks ( 0,1 ) { isa_ok( $Grant = Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant->new( clients => authorizationcodegrant_tests::clients(), # am passing in a reference to the modules subs to ensure we hit # the code paths to call callbacks ( $with_callbacks ? ( authorizationcodegrant_tests::callbacks( $Grant ) ) : () ), ), 'Net::OAuth2::AuthorizationServer::AuthorizationCodeGrant' ); authorizationcodegrant_tests::run_tests( $Grant,{ no_jwt => 1 } ); } done_testing(); Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/implicitgrant_no_jwt.t000644 000770 000120 00000001263 13432550612 033603 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; use Test::Most; use Test::Exception; use FindBin qw/ $Bin /; use lib "$Bin"; use implicitgrant_tests; use_ok( 'Net::OAuth2::AuthorizationServer::ImplicitGrant' ); my $Grant; foreach my $with_callbacks ( 0,1 ) { isa_ok( $Grant = Net::OAuth2::AuthorizationServer::ImplicitGrant->new( clients => implicitgrant_tests::clients(), # am passing in a reference to the modules subs to ensure we hit # the code paths to call callbacks ( $with_callbacks ? ( implicitgrant_tests::callbacks( $Grant ) ) : () ), ), 'Net::OAuth2::AuthorizationServer::ImplicitGrant' ); implicitgrant_tests::run_tests( $Grant ); } done_testing(); Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/clientcredentialsgrant.t000644 000770 000120 00000004174 13460615265 034120 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; use Test::Most; use Test::Exception; use Crypt::JWT qw/ decode_jwt /; use FindBin qw/ $Bin /; use lib "$Bin"; use clientcredentialsgrant_tests; use_ok( 'Net::OAuth2::AuthorizationServer::ClientCredentialsGrant' ); throws_ok( sub { Net::OAuth2::AuthorizationServer::ClientCredentialsGrant->new; }, qr/requires either clients or overrides/, 'constructor with no args throws' ); my $Grant; foreach my $with_callbacks ( 0,1 ) { isa_ok( $Grant = Net::OAuth2::AuthorizationServer::ClientCredentialsGrant->new( jwt_secret => 'Some Secret Key', clients => clientcredentialsgrant_tests::clients(), # am passing in a reference to the modules subs to ensure we hit # the code paths to call callbacks ( $with_callbacks ? ( clientcredentialsgrant_tests::callbacks( $Grant ) ) : () ), ), 'Net::OAuth2::AuthorizationServer::ClientCredentialsGrant' ); my $access_token = clientcredentialsgrant_tests::run_tests( $Grant,{ token_format_tests => \&token_format_tests, cannot_revoke => 1, # because we set jwt_secret } ); my ( $res,$error ) = $Grant->verify_token_and_scope( auth_header => undef, scopes => [ qw/ eat sleep / ], refresh_token => 0, ); ok( ! $res,'->verify_token_and_scope, no auth header' ); is( $error,'invalid_request','has error' ); chop( $access_token ); ( $res,$error ) = $Grant->verify_access_token( access_token => $access_token, scopes => [ qw/ eat sleep / ], is_refresh_token => 0, ); ok( ! $res,'->verify_access_token, access token revoked' ); is( $error,'invalid_grant','has error' ); } done_testing(); sub token_format_tests { my ( $token,$type ) = @_; like( $token,qr/\./,'token looks like a JWT' ); cmp_deeply( decode_jwt( alg => 'HS256', key => 'Some Secret Key', token => $token ), { 'aud' => $type eq 'auth' ? 'https://come/back' : undef, 'client' => 'test_client', ( $type eq 'refresh' ? () : ( 'exp' => ignore() ) ), 'iat' => ignore(), 'jti' => ignore(), 'scopes' => [ 'eat', 'sleep' ], 'type' => $type, 'user_id' => 1 }, 'auth code decodes correctly', ); } Net-OAuth2-AuthorizationServer-0.28/t/net/oauth2/authorizationserver/passwordgrant_no_jwt.t000644 000770 000120 00000001335 13432550612 033633 0ustar00leejohnsonadmin000000 000000 #!perl use strict; use warnings; use Test::Most; use Test::Exception; use FindBin qw/ $Bin /; use lib "$Bin"; use passwordgrant_tests; use_ok( 'Net::OAuth2::AuthorizationServer::PasswordGrant' ); my $Grant; foreach my $with_callbacks ( 0,1 ) { isa_ok( $Grant = Net::OAuth2::AuthorizationServer::PasswordGrant->new( clients => passwordgrant_tests::clients(), users => passwordgrant_tests::users(), # am passing in a reference to the modules subs to ensure we hit # the code paths to call callbacks ( $with_callbacks ? ( passwordgrant_tests::callbacks( $Grant ) ) : () ), ), 'Net::OAuth2::AuthorizationServer::PasswordGrant' ); passwordgrant_tests::run_tests( $Grant ); } done_testing();