Mojolicious-Plugin-OAuth2-2.02/000755 000765 000024 00000000000 14200436242 017431 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OAuth2-2.02/cpanfile000644 000765 000024 00000000627 14136225637 021156 0ustar00jhthorsenstaff000000 000000 # You can install this projct with curl -L http://cpanmin.us | perl - https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2/archive/master.tar.gz requires 'Mojolicious' => '8.25'; requires 'IO::Socket::SSL' => '1.94'; test_requires 'Test::More' => '0.88'; recommends 'Mojo::JWT' => '0.09'; recommends 'Crypt::OpenSSL::Bignum' => '0.09'; recommends 'Crypt::OpenSSL::RSA' => '0.31'; Mojolicious-Plugin-OAuth2-2.02/Changes000644 000765 000024 00000007602 14200436241 020730 0ustar00jhthorsenstaff000000 000000 Revision history for perl distribution Mojolicious-Plugin-OAuth2 2.02 2022-02-08T18:49:21+0900 - Add support for passing in custom "ua" - Add support for "providers" key in plugin config - Add support for "proxy" in plugin config 2.01 2021-10-28T18:29:45+0900 - Test suite is compatible with older versions of Mojolicious - OpenID Connect require Mojo::JWT 0.09 2.00 2021-10-27T19:36:44+0900 - Removed $c->oauth2->get_token() https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2/blob/07e214eb556093de8691b145116b60ab64a4a21a/t/delayed.t#L23-L28 - Add support for "OpenID Connect" #65 Contributor: Roy Storey - Add "debian_salsa" as an OAuth2 id provider #62 Contributor: Gregor Herrmann - Moved mock code to Mojolicious::Plugin::OAuth2::Mock - Bumped Mojolicious version to 8.25 1.59 2021-02-17T08:33:17+0900 - Fix invalid "=item" in documentation. - Compatible with Mojolicious 9.0 #61 Contributor: Joel Berger 1.58 2019-07-03T14:22:38+0200 - Add new Oauth2 providers: instagram.com - Add new Oauth2 providers: vk.com 1.57 2018-09-24T10:54:40+0200 - 1.56 was broken because of an error in a pre-release git ship version. 1.56 2018-09-24T00:03:28+0200 - Restore response_type to google authorize url. 1.55 2018-09-08T20:29:56+0200 - Made it possible to disable automatic redirect 1.54 2018-08-30T12:48:19+0200 - Add oauth2.get_token_p() helper - Compatible with Mojolicious 7.90 #53 - Removed deprecated helper get_authorize_url() - Removed deprecated helper get_token() 1.53 2015-09-08T09:27:17Z - Removed fix_get_token hack - Documented deprecation for get_authorize_url() and get_token() - Fix documentation regarding return value to oauth2->get_token() #45 - Fix liftetime isn't a valid paramater in mocked response #47 - Add more data to mocked response #47 1.52 2015-04-06T22:45:17Z - Add oauth2->get_token() works in blocking mode (Alexander Karelas) 1.51 2015-03-18T17:38:19Z - Fix get_token() need to return the whole data structure to the callback and not just $token. 1.5 2015-03-02T08:31:47Z - Able to mock interface for easy testing 1.4 2015-03-01T21:03:47Z - Fix handling of error in param, #27 - Add new helper oauth2->auth_url - Add new helper oauth2->get_token - Add new helper oauth2->providers - Add eventbrite and github as providers - Deprecate on_xxx handlers - Started deprecation process for get_authorize_url() and get_token() 1.3 2014-10-07T08:00:00Z - Fix param injection security issue 1.2 2014-09-06T15:48:00Z - Tidy up the code with .perltidyrc and githook-perltidy - Started deprecation process of on_xxx handlers 1.1 2014-03-13T05:55:00Z - Update test suite to latest Mojo (Valcho Nedelchev) - Bugfix: When "charset" is included in Content-Type header, token extraction failed. (Colin Cyr) - permit altering the host on a request by request basis (Colin Cyr) - fix typos (David Steinbrunner) 1.0 2013-11-02T16:34:00Z - Add state parameter - Handle user's refusal to auth - Doc updates 0.9 2013-05-20T07:51:00Z - Update to use new Mojolicious UA format (jht) - Remove deprecated render_text calls 0.8 2012-08-23T22:07:00Z - Add get_authorize_url() helper (Batman) - Support sync without callback. - Better Mojo::Delay integration (Batman) 0.7 2012-05-30T12:00:00Z - separate ua for oauth (Batman) - support extra auth parameters (Judofyr) - Fix broken tests (Peder Stray) 0.6 2012-03-09T00:00:00Z - Change to use app_url 0.5 2011-10-19T13:54Z - Add SSL dependency 0.4 2011-07-06T18:57Z - Add support for google oauth2 provider 0.3 2011-09-04T21:39Z - Fix tests for recent Mojolicious 0.2 2011-04-03T02:01:00Z - Fix test suite on newer mojolicious (minimalist) - Fix live test to check for 301 redirect. 0.1 2011-04-03T02:01:00Z - Update to use UserAgent rather than the deprecated Client. 0.02 2011-01-09T18:58:00Z - Fixed code example in synopsis - Rename callback* to on_* 0.01 2011-01-08T11:55:00Z - Initial release Mojolicious-Plugin-OAuth2-2.02/MANIFEST000644 000765 000024 00000000776 14200436242 020574 0ustar00jhthorsenstaff000000 000000 .github/workflows/linux.yml .github/workflows/macos.yml .perltidyrc .pls_cache/index .ship.conf Changes cpanfile lib/Mojolicious/Plugin/OAuth2.pm lib/Mojolicious/Plugin/OAuth2/Mock.pm Makefile.PL MANIFEST This list of files README README.md t/00-basic.t t/auth_url.t t/delayed.t t/error.t t/Helper.pm t/live.t t/mocked.t t/openid-connect.t META.yml Module YAML meta-data (added by MakeMaker) META.json Module JSON meta-data (added by MakeMaker) Mojolicious-Plugin-OAuth2-2.02/t/000755 000765 000024 00000000000 14200436242 017674 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OAuth2-2.02/.pls_cache/000755 000765 000024 00000000000 14200436242 021430 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OAuth2-2.02/README000644 000765 000024 00000021521 13341711623 020316 0ustar00jhthorsenstaff000000 000000 NAME Mojolicious::Plugin::OAuth2 - Auth against OAuth2 APIs DESCRIPTION This Mojolicious plugin allows you to easily authenticate against a OAuth2 provider. It includes configurations for a few popular providers, but you can add your own easily as well. Note that OAuth2 requires https, so you need to have the optional Mojolicious dependency required to support it. Run the command below to check if IO::Socket::SSL is installed. $ mojo version References * * * * SYNOPSIS Example web application use Mojolicious::Lite; plugin "OAuth2" => { facebook => { key => "some-public-app-id", secret => $ENV{OAUTH2_FACEBOOK_SECRET}, }, }; get "/connect" => sub { my $c = shift; $c->delay( sub { my $delay = shift; my $args = {redirect_uri => $c->url_for('connect')->userinfo(undef)->to_abs}; $c->oauth2->get_token(facebook => $args, $delay->begin); }, sub { my ($delay, $err, $data) = @_; return $c->render("connect", error => $err) unless $data->{access_token}; return $c->session(token => $c->redirect_to('profile')); }, ); }; Example blocking web application use Mojolicious::Lite; plugin "OAuth2" => { facebook => { key => "some-public-app-id", secret => $ENV{OAUTH2_FACEBOOK_SECRET}, }, }; get "/connect" => sub { my $c = shift; my $args = {redirect_uri => $c->url_for('connect')->userinfo(undef)->to_abs}; if (my $err = $c->param('error')) { # do stuff with error from OAuth2 provider } elsif (my $data = $c->oauth2->get_token(facebook => $args)) { # do stuff with $data->{access_token}; } else { # already redirected by OAuth2 plugin return; } }; Custom connect button You can add a "connect link" to your template using the "oauth2.auth_url" helper. Example template: Click here to log in: <%= link_to "Connect!", $c->oauth2->auth_url("facebook", scope => "user_about_me email") %> Configuration This plugin takes a hash as config, where the keys are provider names and the values are configuration for each provider. Here is a complete example: plugin "OAuth2" => { custom_provider => { key => "APP_ID", secret => "SECRET_KEY", authorize_url => "https://provider.example.com/auth", token_url => "https://provider.example.com/token", }, }; To make it a bit easier, Mojolicious::Plugin::OAuth2 has already values for "authorize_url" and "token_url" for the following providers: * dailymotion Authentication for Dailymotion video site. * eventbrite Authentication for event site. See also . * facebook OAuth2 for Facebook's graph API, . You can find "key" (App ID) and "secret" (App Secret) from the app dashboard here: . See also . * github Authentication with Github. See also * google OAuth2 for Google. You can find the "key" (CLIENT ID) and "secret" (CLIENT SECRET) from the app console here under "APIs & Auth" and "Credentials" in the menu at . See also . Testing THIS API IS EXPERIMENTAL AND CAN CHANGE WITHOUT NOTICE. To enable a "mocked" OAuth2 api, you need to give the special "mocked" provider a "key": plugin "OAuth2" => { mocked => {key => 42} }; The code above will add two new routes to your application: * GET /mocked/oauth/authorize This route is a web page which contains a link that takes you back to "redirect_uri", with a "code". The "code" default to "fake_code", but can be configured: $c->app->oauth2->providers->{mocked}{return_code} = "..."; The route it self can also be customized: plugin "OAuth2" => { mocked => {authorize_url => '...'} }; * POST /mocked/oauth/token This route is will return a "access_token" which is available in your "oauth2.get_token" callback. The default is "fake_token", but it can be configured: $c->app->oauth2->providers->{mocked}{return_token} = "..."; The route it self can also be customized: plugin "OAuth2" => { mocked => {token_url => '...'} }; HELPERS oauth2.auth_url $url = $c->oauth2->auth_url($provider => \%args); Returns a Mojo::URL object which contain the authorize URL. This is useful if you want to add the authorize URL as a link to your webpage instead of doing a redirect like "oauth2.get_token" does. %args is optional, but can contain: * host Useful if your provider uses different hosts for accessing different accounts. The default is specified in the provider configuration. $url->host($host); * authorize_query Either a hash-ref or an array-ref which can be used to give extra query params to the URL. $url->query($authorize_url); * redirect_uri Useful if you want to go back to a different page than what you came from. The default is: $c->url_for->to_abs->to_string * scope Scope to ask for credentials to. Should be a space separated list. * state A string that will be sent to the identity provider. When the user returns from the identity provider, this exact same string will be carried with the user, as a GET parameter called "state" in the URL that the user will return to. oauth2.get_token $data = $c->oauth2->get_token($provider_name => \%args); $c = $c->oauth2->get_token($provider_name => \%args, sub { my ($c, $err, $data) = @_; }); "oauth2.get_token" is used to either fetch access token from OAuth2 provider, handle errors or redirect to OAuth2 provider. This method can be called in either blocking or non-blocking mode. $err holds a error description if something went wrong. Blocking mode will "die($err)" instead of returning it to caller. $data is a hash-ref containing the access token from the OAauth2 provider. $data in blocking mode can also be "undef" if a redirect has been issued by this module. In more detail, this method will do one of two things: 1. If called from an action on your site, it will redirect you to the $provider_name's "authorize_url". This site will probably have some sort of "Connect" and "Reject" button, allowing the visitor to either connect your site with his/her profile on the OAuth2 provider's page or not. 2. The OAuth2 provider will redirect the user back to your site after clicking the "Connect" or "Reject" button. $data will then contain a key "access_token" on "Connect" and a false value on "Reject" mode, or will die in blocking mode. Will redirect to the provider to allow for authorization, then fetch the token. The token gets provided as a parameter to the callback function. Usually you want to store the token in a session or similar to use for API requests. Supported arguments: * host Useful if your provider uses different hosts for accessing different accounts. The default is specified in the provider configuration. * scope Scope to ask for credentials to. Should be a space separated list. oauth2.providers This helper allow you to access the raw providers mapping, which looks something like this: { facebook => { authorize_url => "https://graph.facebook.com/oauth/authorize", token_url => "https://graph.facebook.com/oauth/access_token", key => ..., secret => ..., }, ... } ATTRIBUTES providers Holds a hash of provider information. See oauth2.providers. METHODS register Will register this plugin in your application. See "SYNOPSIS". AUTHOR Marcus Ramberg - "mramberg@cpan.org" Jan Henning Thorsen - "jhthorsen@cpan.org" LICENSE This software is licensed under the same terms as Perl itself. Mojolicious-Plugin-OAuth2-2.02/README.md000644 000765 000024 00000027453 14200436241 020722 0ustar00jhthorsenstaff000000 000000 # NAME Mojolicious::Plugin::OAuth2 - Auth against OAuth2 APIs including OpenID Connect # SYNOPSIS ## Example application use Mojolicious::Lite; plugin OAuth2 => { providers => { facebook => { key => 'some-public-app-id', secret => $ENV{OAUTH2_FACEBOOK_SECRET}, }, }, }; get '/connect' => sub { my $c = shift; my %get_token = (redirect_uri => $c->url_for('connect')->userinfo(undef)->to_abs); return $c->oauth2->get_token_p(facebook => \%get_token)->then(sub { # Redirected to Facebook return unless my $provider_res = shift; # Token received $c->session(token => $provider_res->{access_token}); $c->redirect_to('profile'); })->catch(sub { $c->render('connect', error => shift); }); }; See ["register"](#register) for more details about the configuration this plugin takes. ## Testing Code using this plugin can perform offline testing, using the "mocked" provider: $app->plugin(OAuth2 => {mocked => {key => 42}}); $app->routes->get('/profile' => sub { my $c = shift; state $mocked = $ENV{TEST_MOCKED} && 'mocked'; return $c->oauth2->get_token_p($mocked || 'facebook')->then(sub { ... }); }); See [Mojolicious::Plugin::OAuth2::Mock](https://metacpan.org/pod/Mojolicious%3A%3APlugin%3A%3AOAuth2%3A%3AMock) for more details. ## Connect button You can add a "connect link" to your template using the ["oauth2.auth\_url"](#oauth2-auth_url) helper. Example template: Click here to log in: <%= link_to 'Connect!', $c->oauth2->auth_url('facebook', scope => 'user_about_me email') %> # DESCRIPTION This Mojolicious plugin allows you to easily authenticate against a [OAuth2](http://oauth.net) or [OpenID Connect](https://openid.net/connect/) provider. It includes configurations for a few popular [providers](#register), but you can add your own as well. See ["register"](#register) for a full list of bundled providers. To support "OpenID Connect", the following optional modules must be installed manually: [Crypt::OpenSSL::Bignum](https://metacpan.org/pod/Crypt%3A%3AOpenSSL%3A%3ABignum), [Crypt::OpenSSL::RSA](https://metacpan.org/pod/Crypt%3A%3AOpenSSL%3A%3ARSA) and [Mojo::JWT](https://metacpan.org/pod/Mojo%3A%3AJWT). The modules can be installed with [App::cpanminus](https://metacpan.org/pod/App%3A%3Acpanminus): $ cpanm Crypt::OpenSSL::Bignum Crypt::OpenSSL::RSA Mojo::JWT # HELPERS ## oauth2.auth\_url $url = $c->oauth2->auth_url($provider_name => \%args); Returns a [Mojo::URL](https://metacpan.org/pod/Mojo%3A%3AURL) object which contain the authorize URL. This is useful if you want to add the authorize URL as a link to your webpage instead of doing a redirect like ["oauth2.get\_token"](#oauth2-get_token) does. `%args` is optional, but can contain: - host Useful if your provider uses different hosts for accessing different accounts. The default is specified in the provider configuration. $url->host($host); - authorize\_query Either a hash-ref or an array-ref which can be used to give extra query params to the URL. $url->query($authorize_url); - redirect\_uri Useful if you want to go back to a different page than what you came from. The default is: $c->url_for->to_abs->to_string - scope Scope to ask for credentials to. Should be a space separated list. - state A string that will be sent to the identity provider. When the user returns from the identity provider, this exact same string will be carried with the user, as a GET parameter called `state` in the URL that the user will return to. ## oauth2.get\_refresh\_token\_p $promise = $c->oauth2->get_refresh_token_p($provider_name => \%args); When [Mojolicious::Plugin::OAuth2](https://metacpan.org/pod/Mojolicious%3A%3APlugin%3A%3AOAuth2) is being used in OpenID Connect mode this helper allows for a token to be refreshed by specifying a `refresh_token` in `%args`. Usage is similar to ["oauth2.get\_token\_p"](#oauth2-get_token_p). ## oauth2.get\_token\_p $promise = $c->oauth2->get_token_p($provider_name => \%args) ->then(sub { my $provider_res = shift }) ->catch(sub { my $err = shift; }); ["oauth2.get\_token\_p"](#oauth2-get_token_p) is used to either fetch an access token from an OAuth2 provider, handle errors or redirect to OAuth2 provider. `$err` in the rejection handler holds a error description if something went wrong. `$provider_res` is a hash-ref containing the access token from the OAauth2 provider or `undef` if this plugin performed a 302 redirect to the provider's connect website. In more detail, this method will do one of two things: 1. When called from an action on your site, it will redirect you to the provider's `authorize_url`. This site will probably have some sort of "Connect" and "Reject" button, allowing the visitor to either connect your site with his/her profile on the OAuth2 provider's page or not. 2. The OAuth2 provider will redirect the user back to your site after clicking the "Connect" or "Reject" button. `$provider_res` will then contain a key "access\_token" on "Connect" and a false value on "Reject". The method takes these arguments: `$provider_name` need to match on of the provider names under ["Configuration"](#configuration) or a custom provider defined when [registering](#synopsis) the plugin. `%args` can have: - host Useful if your provider uses different hosts for accessing different accounts. The default is specified in the provider configuration. - redirect Set `redirect` to 0 to disable automatic redirect. - scope Scope to ask for credentials to. Should be a space separated list. ## oauth2.jwt\_decode $claims = $c->oauth2->jwt_decode($provider, sub { my $jwt = shift; ... }); $claims = $c->oauth2->jwt_decode($provider); When [Mojolicious::Plugin::OAuth2](https://metacpan.org/pod/Mojolicious%3A%3APlugin%3A%3AOAuth2) is being used in OpenID Connect mode this helper allows you to decode the response data encoded with the JWKS discovered from `well_known_url` configuration. ## oauth2.logout\_url $url = $c->oauth2->logout_url($provider_name => \%args); When [Mojolicious::Plugin::OAuth2](https://metacpan.org/pod/Mojolicious%3A%3APlugin%3A%3AOAuth2) is being used in OpenID Connect mode this helper creates the url to redirect to end the session. The OpenID Connect Provider will redirect to the `post_logout_redirect_uri` provided in `%args`. Additional keys for `%args` are `id_token_hint` and `state`. ## oauth2.providers $hash_ref = $c->oauth2->providers; This helper allow you to access the raw providers mapping, which looks something like this: { facebook => { authorize_url => "https://graph.facebook.com/oauth/authorize", token_url => "https://graph.facebook.com/oauth/access_token", key => ..., secret => ..., }, ... } # ATTRIBUTES ## providers $hash_ref = $oauth2->providers; Holds a hash of provider information. See ["oauth2.providers"](#oauth2-providers). # METHODS ## register $app->plugin(OAuth2 => \%provider_config); $app->plugin(OAuth2 => {providers => \%provider_config, proxy => 1, ua => Mojo::UserAgent->new}); Will register this plugin in your application with a given `%provider_config`. The keys in `%provider_config` are provider names and the values are configuration for each provider. Note that the value will be merged with the predefined providers below. Instead of just passing in `%provider_config`, it is possible to pass in a more complex config, with these keys: - providers The `%provider_config` must be present under this key. - proxy Setting this to a true value will automatically detect proxy settings using ["detect" in Mojo::UserAgent::Proxy](https://metacpan.org/pod/Mojo%3A%3AUserAgent%3A%3AProxy#detect). - ua A custom [Mojo::UserAgent](https://metacpan.org/pod/Mojo%3A%3AUserAgent), in case you want to change proxy settings, timeouts or other attributes. Instead of just passing in `%provider_config`, it is possible to pass in a hash-ref "providers" (`%provider_config`) and "ua" (a custom [Mojo::UserAgent](https://metacpan.org/pod/Mojo%3A%3AUserAgent) object). Here is an example to add adddition information like "key" and "secret": $app->plugin(OAuth2 => { providers => { custom_provider => { key => 'APP_ID', secret => 'SECRET_KEY', authorize_url => 'https://provider.example.com/auth', token_url => 'https://provider.example.com/token', }, github => { key => 'APP_ID', secret => 'SECRET_KEY', }, }, }); For [OpenID Connect](https://openid.net/connect/), `authorize_url` and `token_url` are configured from the `well_known_url` so these are replaced by the `well_known_url` key. $app->plugin(OAuth2 => { providers => { azure_ad => { key => 'APP_ID', secret => 'SECRET_KEY', well_known_url => 'https://login.microsoftonline.com/tenant-id/v2.0/.well-known/openid-configuration', }, }, }); To make it a bit easier the are already some predefined providers bundled with this plugin: ### dailymotion Authentication for [https://www.dailymotion.com/](https://www.dailymotion.com/) video site. ### debian\_salsa Authentication for [https://salsa.debian.org/](https://salsa.debian.org/). ### eventbrite Authentication for [https://www.eventbrite.com](https://www.eventbrite.com) event site. See also [http://developer.eventbrite.com/docs/auth/](http://developer.eventbrite.com/docs/auth/). ### facebook OAuth2 for Facebook's graph API, [http://graph.facebook.com/](http://graph.facebook.com/). You can find `key` (App ID) and `secret` (App Secret) from the app dashboard here: [https://developers.facebook.com/apps](https://developers.facebook.com/apps). See also [https://developers.facebook.com/docs/reference/dialogs/oauth/](https://developers.facebook.com/docs/reference/dialogs/oauth/). ### instagram OAuth2 for Instagram API. You can find `key` (Client ID) and `secret` (Client Secret) from the app dashboard here: [https://www.instagram.com/developer/clients/manage/](https://www.instagram.com/developer/clients/manage/). See also [https://www.instagram.com/developer/authentication/](https://www.instagram.com/developer/authentication/). ### github Authentication with Github. See also [https://developer.github.com/v3/oauth/](https://developer.github.com/v3/oauth/). ### google OAuth2 for Google. You can find the `key` (CLIENT ID) and `secret` (CLIENT SECRET) from the app console here under "APIs & Auth" and "Credentials" in the menu at [https://console.developers.google.com/project](https://console.developers.google.com/project). See also [https://developers.google.com/+/quickstart/](https://developers.google.com/+/quickstart/). ### vkontakte OAuth2 for Vkontakte. You can find `key` (App ID) and `secret` (Secure key) from the app dashboard here: [https://vk.com/apps?act=manage](https://vk.com/apps?act=manage). See also [https://vk.com/dev/authcode\_flow\_user](https://vk.com/dev/authcode_flow_user). # AUTHOR Marcus Ramberg - `mramberg@cpan.org` Jan Henning Thorsen - `jhthorsen@cpan.org` # LICENSE This software is licensed under the same terms as Perl itself. # SEE ALSO - [http://oauth.net/documentation/](http://oauth.net/documentation/) - [http://aaronparecki.com/articles/2012/07/29/1/oauth2-simplified](http://aaronparecki.com/articles/2012/07/29/1/oauth2-simplified) - [http://homakov.blogspot.jp/2013/03/oauth1-oauth2-oauth.html](http://homakov.blogspot.jp/2013/03/oauth1-oauth2-oauth.html) - [http://en.wikipedia.org/wiki/OAuth#OAuth\_2.0](http://en.wikipedia.org/wiki/OAuth#OAuth_2.0) - [https://openid.net/connect/](https://openid.net/connect/) Mojolicious-Plugin-OAuth2-2.02/META.yml000664 000765 000024 00000002052 14200436242 020703 0ustar00jhthorsenstaff000000 000000 --- abstract: 'Auth against OAuth2 APIs including OpenID Connect' author: - 'Jan Henning Thorsen ' build_requires: Test::More: '0.88' configure_requires: ExtUtils::MakeMaker: '0' dynamic_config: 0 generated_by: 'ExtUtils::MakeMaker version 7.62, CPAN::Meta::Converter version 2.150010' license: artistic_2 meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: Mojolicious-Plugin-OAuth2 no_index: directory: - t - inc recommends: Crypt::OpenSSL::Bignum: '0.09' Crypt::OpenSSL::RSA: '0.31' Mojo::JWT: '0.09' requires: IO::Socket::SSL: '1.94' Mojolicious: '8.25' resources: bugtracker: https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2/issues homepage: https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2 repository: https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2.git version: '2.02' x_contributors: - 'Marcus Ramberg ' - 'Jan Henning Thorsen ' x_serialization_backend: 'CPAN::Meta::YAML version 0.018' Mojolicious-Plugin-OAuth2-2.02/.github/000755 000765 000024 00000000000 14200436242 020771 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OAuth2-2.02/lib/000755 000765 000024 00000000000 14200436242 020177 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OAuth2-2.02/.ship.conf000644 000765 000024 00000000542 13507117070 021326 0ustar00jhthorsenstaff000000 000000 # Generated by git-ship. See 'git-ship --man' for help or https://github.com/jhthorsen/app-git-ship class = App::git::ship::perl project_name = homepage = https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2 bugtracker = https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2/issues license = artistic_2 build_test_options = # Example: -l -j8 Mojolicious-Plugin-OAuth2-2.02/Makefile.PL000644 000765 000024 00000003343 14200436241 021405 0ustar00jhthorsenstaff000000 000000 # Generated by git-ship. See 'git-ship --man' for help or https://github.com/jhthorsen/app-git-ship use utf8; use ExtUtils::MakeMaker; my %WriteMakefileArgs = ( NAME => 'Mojolicious::Plugin::OAuth2', AUTHOR => 'Jan Henning Thorsen ', LICENSE => 'artistic_2', ABSTRACT_FROM => 'lib/Mojolicious/Plugin/OAuth2.pm', VERSION_FROM => 'lib/Mojolicious/Plugin/OAuth2.pm', EXE_FILES => [qw()], OBJECT => '', BUILD_REQUIRES => {} , TEST_REQUIRES => { 'Test::More' => '0.88' } , PREREQ_PM => { 'IO::Socket::SSL' => '1.94', 'Mojolicious' => '8.25' } , META_MERGE => { 'dynamic_config' => 0, 'meta-spec' => {version => 2}, 'prereqs' => { 'runtime' => {'recommends' => { 'Crypt::OpenSSL::Bignum' => '0.09', 'Crypt::OpenSSL::RSA' => '0.31', 'Mojo::JWT' => '0.09' } } }, 'resources' => { bugtracker => {web => 'https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2/issues'}, homepage => 'https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2', repository => { type => 'git', url => 'https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2.git', web => 'https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2', }, }, 'x_contributors' => [ 'Marcus Ramberg ', 'Jan Henning Thorsen ' ] , }, test => {TESTS => (-e 'META.yml' ? 't/*.t' : 't/*.t xt/*.t')}, ); unless (eval { ExtUtils::MakeMaker->VERSION('6.63_03') }) { my $test_requires = delete $WriteMakefileArgs{TEST_REQUIRES}; @{$WriteMakefileArgs{PREREQ_PM}}{keys %$test_requires} = values %$test_requires; } WriteMakefile(%WriteMakefileArgs); Mojolicious-Plugin-OAuth2-2.02/.perltidyrc000644 000765 000024 00000001024 14135752446 021626 0ustar00jhthorsenstaff000000 000000 -pbp # Start with Perl Best Practices -w # Show all warnings -iob # Ignore old breakpoints -l=120 # 120 characters per line -mbl=2 # No more than 2 blank lines -i=2 # Indentation is 2 columns -ci=2 # Continuation indentation is 2 columns -vt=0 # Less vertical tightness -pt=2 # High parenthesis tightness -bt=2 # High brace tightness -sbt=2 # High square bracket tightness -isbc # Don't indent comments without leading space -wn # Weld nested containers -nst # Don't output to STDOUT Mojolicious-Plugin-OAuth2-2.02/META.json000664 000765 000024 00000003376 14200436242 021065 0ustar00jhthorsenstaff000000 000000 { "abstract" : "Auth against OAuth2 APIs including OpenID Connect", "author" : [ "Jan Henning Thorsen " ], "dynamic_config" : 0, "generated_by" : "ExtUtils::MakeMaker version 7.62, CPAN::Meta::Converter version 2.150010", "license" : [ "artistic_2" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Mojolicious-Plugin-OAuth2", "no_index" : { "directory" : [ "t", "inc" ] }, "prereqs" : { "build" : { "requires" : {} }, "configure" : { "requires" : { "ExtUtils::MakeMaker" : "0" } }, "runtime" : { "recommends" : { "Crypt::OpenSSL::Bignum" : "0.09", "Crypt::OpenSSL::RSA" : "0.31", "Mojo::JWT" : "0.09" }, "requires" : { "IO::Socket::SSL" : "1.94", "Mojolicious" : "8.25" } }, "test" : { "requires" : { "Test::More" : "0.88" } } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2/issues" }, "homepage" : "https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2", "repository" : { "type" : "git", "url" : "https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2.git", "web" : "https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2" } }, "version" : "2.02", "x_contributors" : [ "Marcus Ramberg ", "Jan Henning Thorsen " ], "x_serialization_backend" : "JSON::PP version 4.06" } Mojolicious-Plugin-OAuth2-2.02/lib/Mojolicious/000755 000765 000024 00000000000 14200436242 022473 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OAuth2-2.02/lib/Mojolicious/Plugin/000755 000765 000024 00000000000 14200436242 023731 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OAuth2-2.02/lib/Mojolicious/Plugin/OAuth2.pm000644 000765 000024 00000045437 14200436241 025405 0ustar00jhthorsenstaff000000 000000 package Mojolicious::Plugin::OAuth2; use Mojo::Base 'Mojolicious::Plugin'; use Carp qw(croak); use Mojo::Promise; use Mojo::URL; use Mojo::UserAgent; use constant MOJO_JWT => eval 'use Mojo::JWT 0.09; use Crypt::OpenSSL::RSA; use Crypt::OpenSSL::Bignum; 1'; our @CARP_NOT = qw(Mojolicious::Plugin::OAuth2 Mojolicious::Renderer); our $VERSION = '2.02'; has providers => sub { return { dailymotion => { authorize_url => 'https://api.dailymotion.com/oauth/authorize', token_url => 'https://api.dailymotion.com/oauth/token' }, debian_salsa => { authorize_url => 'https://salsa.debian.org/oauth/authorize?response_type=code', token_url => 'https://salsa.debian.org/oauth/token', }, eventbrite => { authorize_url => 'https://www.eventbrite.com/oauth/authorize', token_url => 'https://www.eventbrite.com/oauth/token', }, facebook => { authorize_url => 'https://graph.facebook.com/oauth/authorize', token_url => 'https://graph.facebook.com/oauth/access_token', }, instagram => { authorize_url => 'https://api.instagram.com/oauth/authorize/?response_type=code', token_url => 'https://api.instagram.com/oauth/access_token', }, github => { authorize_url => 'https://github.com/login/oauth/authorize', token_url => 'https://github.com/login/oauth/access_token', }, google => { authorize_url => 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code', token_url => 'https://www.googleapis.com/oauth2/v4/token', }, vkontakte => {authorize_url => 'https://oauth.vk.com/authorize', token_url => 'https://oauth.vk.com/access_token',}, mocked => {authorize_url => '/mocked/oauth/authorize', token_url => '/mocked/oauth/token', secret => 'fake_secret'}, }; }; has _ua => sub { Mojo::UserAgent->new }; sub register { my ($self, $app, $config) = @_; if ($config->{providers}) { $self->_config_to_providers($config->{providers}); $self->_ua($config->{ua}) if $config->{ua}; $self->_ua->proxy->detect if $config->{proxy}; } else { $self->_config_to_providers($config); } $app->helper('oauth2.auth_url' => sub { $self->_call(_auth_url => @_) }); $app->helper('oauth2.get_refresh_token_p' => sub { $self->_call(_get_refresh_token_p => @_) }); $app->helper('oauth2.get_token_p' => sub { $self->_call(_get_token_p => @_) }); $app->helper('oauth2.jwt_decode' => sub { $self->_call(_jwt_decode => @_) }); $app->helper('oauth2.logout_url' => sub { $self->_call(_logout_url => @_) }); $app->helper('oauth2.providers' => sub { $self->providers }); $self->_apply_mock($self->providers->{mocked}) if $self->providers->{mocked}{key}; $self->_warmup_openid($app); } sub _apply_mock { my ($self, $provider_args) = @_; require Mojolicious::Plugin::OAuth2::Mock; require Mojolicious; my $app = $self->_ua->server->app || Mojolicious->new; Mojolicious::Plugin::OAuth2::Mock->apply_to($app, $provider_args); $self->_ua->server->app($app); } sub _auth_url { my ($self, $c, $args) = @_; my $provider_args = $self->providers->{$args->{provider}}; my $authorize_url; $args->{scope} ||= $provider_args->{scope}; $args->{redirect_uri} ||= $c->url_for->to_abs->to_string; $authorize_url = Mojo::URL->new($provider_args->{authorize_url}); $authorize_url->host($args->{host}) if exists $args->{host}; $authorize_url->query->append(client_id => $provider_args->{key}, redirect_uri => $args->{redirect_uri}); $authorize_url->query->append(scope => $args->{scope}) if defined $args->{scope}; $authorize_url->query->append(state => $args->{state}) if defined $args->{state}; $authorize_url->query($args->{authorize_query}) if exists $args->{authorize_query}; $authorize_url; } sub _call { my ($self, $method, $c, $provider) = (shift, shift, shift, shift); my $args = @_ % 2 ? shift : {@_}; $args->{provider} = $provider || 'unknown'; croak "Invalid provider: $args->{provider}" unless $self->providers->{$args->{provider}}; return $self->$method($c, $args); } sub _config_to_providers { my ($self, $config) = @_; for my $provider (keys %$config) { my $p = $self->providers->{$provider} ||= {}; for my $key (keys %{$config->{$provider}}) { $p->{$key} = $config->{$provider}{$key}; } } } sub _get_refresh_token_p { my ($self, $c, $args) = @_; # TODO: Handle error response from oidc provider callback URL, if possible my $err = $c->param('error_description') || $c->param('error'); return Mojo::Promise->reject($err) if $err; my $provider_args = $self->providers->{$args->{provider}}; my $params = { client_id => $provider_args->{key}, client_secret => $provider_args->{secret}, grant_type => 'refresh_token', refresh_token => $args->{refresh_token}, scope => $provider_args->{scope}, }; my $token_url = Mojo::URL->new($provider_args->{token_url}); $token_url->host($args->{host}) if exists $args->{host}; return $self->_ua->post_p($token_url, form => $params)->then(sub { $self->_parse_provider_response(@_) }); } sub _get_token_p { my ($self, $c, $args) = @_; # Handle error response from provider callback URL my $err = $c->param('error_description') || $c->param('error'); return Mojo::Promise->reject($err) if $err; # No error or code response from provider callback URL unless ($c->param('code')) { $c->redirect_to($self->_auth_url($c, $args)) if $args->{redirect} // 1; return Mojo::Promise->resolve(undef); } # Handle "code" from provider callback my $provider_args = $self->providers->{$args->{provider}}; my $params = { client_id => $provider_args->{key}, client_secret => $provider_args->{secret}, code => scalar($c->param('code')), grant_type => 'authorization_code', redirect_uri => $args->{redirect_uri} || $c->url_for->to_abs->to_string, }; $params->{state} = $c->param('state') if $c->param('state'); my $token_url = Mojo::URL->new($provider_args->{token_url}); $token_url->host($args->{host}) if exists $args->{host}; return $self->_ua->post_p($token_url, form => $params)->then(sub { $self->_parse_provider_response(@_) }); } sub _jwt_decode { my $peek = ref $_[-1] eq 'CODE' && pop; my ($self, $c, $args) = @_; croak 'Provider does not have "jwt" defined.' unless my $jwt = $self->providers->{$args->{provider}}{jwt}; return $jwt->decode($args->{data}, $peek); } sub _logout_url { my ($self, $c, $args) = @_; return Mojo::URL->new($self->providers->{$args->{provider}}{end_session_url})->tap( query => { post_logout_redirect_uri => $args->{post_logout_redirect_uri}, id_token_hint => $args->{id_token_hint}, state => $args->{state} } ); } sub _parse_provider_response { my ($self, $tx) = @_; my $code = $tx->res->code || 'No response'; # Will cause the promise to be rejected return Mojo::Promise->reject(sprintf '%s == %s', $tx->req->url, $tx->error->{message} // $code) if $code ne '200'; return $tx->res->headers->content_type =~ m!^(application/json|text/javascript)(;\s*charset=\S+)?$! ? $tx->res->json : Mojo::Parameters->new($tx->res->body)->to_hash; } sub _warmup_openid { my ($self, $app) = (shift, shift); my ($providers, @p) = ($self->providers); for my $provider (values %$providers) { next unless $provider->{well_known_url}; $app->log->debug("Fetching OpenID configuration from $provider->{well_known_url}"); push @p, $self->_warmup_openid_provider_p($app, $provider); } return @p && Mojo::Promise->all(@p)->wait; } sub _warmup_openid_provider_p { my ($self, $app, $provider) = @_; return $self->_ua->get_p($provider->{well_known_url})->then(sub { my $tx = shift; my $res = $tx->result->json; $provider->{authorize_url} = $res->{authorization_endpoint}; $provider->{end_session_url} = $res->{end_session_endpoint}; $provider->{issuer} = $res->{issuer}; $provider->{token_url} = $res->{token_endpoint}; $provider->{userinfo_url} = $res->{userinfo_endpoint}; $provider->{scope} //= 'openid'; return $self->_ua->get_p($res->{jwks_uri}); })->then(sub { my $tx = shift; $provider->{jwt} = Mojo::JWT->new->add_jwkset($tx->result->json); return $provider; })->catch(sub { my $err = shift; $app->log->error("[OAuth2] Failed to warm up $provider->{well_known_url}: $err"); }); } 1; =head1 NAME Mojolicious::Plugin::OAuth2 - Auth against OAuth2 APIs including OpenID Connect =head1 SYNOPSIS =head2 Example application use Mojolicious::Lite; plugin OAuth2 => { providers => { facebook => { key => 'some-public-app-id', secret => $ENV{OAUTH2_FACEBOOK_SECRET}, }, }, }; get '/connect' => sub { my $c = shift; my %get_token = (redirect_uri => $c->url_for('connect')->userinfo(undef)->to_abs); return $c->oauth2->get_token_p(facebook => \%get_token)->then(sub { # Redirected to Facebook return unless my $provider_res = shift; # Token received $c->session(token => $provider_res->{access_token}); $c->redirect_to('profile'); })->catch(sub { $c->render('connect', error => shift); }); }; See L for more details about the configuration this plugin takes. =head2 Testing Code using this plugin can perform offline testing, using the "mocked" provider: $app->plugin(OAuth2 => {mocked => {key => 42}}); $app->routes->get('/profile' => sub { my $c = shift; state $mocked = $ENV{TEST_MOCKED} && 'mocked'; return $c->oauth2->get_token_p($mocked || 'facebook')->then(sub { ... }); }); See L for more details. =head2 Connect button You can add a "connect link" to your template using the L helper. Example template: Click here to log in: <%= link_to 'Connect!', $c->oauth2->auth_url('facebook', scope => 'user_about_me email') %> =head1 DESCRIPTION This Mojolicious plugin allows you to easily authenticate against a L or L provider. It includes configurations for a few popular L, but you can add your own as well. See L for a full list of bundled providers. To support "OpenID Connect", the following optional modules must be installed manually: L, L and L. The modules can be installed with L: $ cpanm Crypt::OpenSSL::Bignum Crypt::OpenSSL::RSA Mojo::JWT =head1 HELPERS =head2 oauth2.auth_url $url = $c->oauth2->auth_url($provider_name => \%args); Returns a L object which contain the authorize URL. This is useful if you want to add the authorize URL as a link to your webpage instead of doing a redirect like L does. C<%args> is optional, but can contain: =over 2 =item * host Useful if your provider uses different hosts for accessing different accounts. The default is specified in the provider configuration. $url->host($host); =item * authorize_query Either a hash-ref or an array-ref which can be used to give extra query params to the URL. $url->query($authorize_url); =item * redirect_uri Useful if you want to go back to a different page than what you came from. The default is: $c->url_for->to_abs->to_string =item * scope Scope to ask for credentials to. Should be a space separated list. =item * state A string that will be sent to the identity provider. When the user returns from the identity provider, this exact same string will be carried with the user, as a GET parameter called C in the URL that the user will return to. =back =head2 oauth2.get_refresh_token_p $promise = $c->oauth2->get_refresh_token_p($provider_name => \%args); When L is being used in OpenID Connect mode this helper allows for a token to be refreshed by specifying a C in C<%args>. Usage is similar to L. =head2 oauth2.get_token_p $promise = $c->oauth2->get_token_p($provider_name => \%args) ->then(sub { my $provider_res = shift }) ->catch(sub { my $err = shift; }); L is used to either fetch an access token from an OAuth2 provider, handle errors or redirect to OAuth2 provider. C<$err> in the rejection handler holds a error description if something went wrong. C<$provider_res> is a hash-ref containing the access token from the OAauth2 provider or C if this plugin performed a 302 redirect to the provider's connect website. In more detail, this method will do one of two things: =over 2 =item 1. When called from an action on your site, it will redirect you to the provider's C. This site will probably have some sort of "Connect" and "Reject" button, allowing the visitor to either connect your site with his/her profile on the OAuth2 provider's page or not. =item 2. The OAuth2 provider will redirect the user back to your site after clicking the "Connect" or "Reject" button. C<$provider_res> will then contain a key "access_token" on "Connect" and a false value on "Reject". =back The method takes these arguments: C<$provider_name> need to match on of the provider names under L or a custom provider defined when L the plugin. C<%args> can have: =over 2 =item * host Useful if your provider uses different hosts for accessing different accounts. The default is specified in the provider configuration. =item * redirect Set C to 0 to disable automatic redirect. =item * scope Scope to ask for credentials to. Should be a space separated list. =back =head2 oauth2.jwt_decode $claims = $c->oauth2->jwt_decode($provider, sub { my $jwt = shift; ... }); $claims = $c->oauth2->jwt_decode($provider); When L is being used in OpenID Connect mode this helper allows you to decode the response data encoded with the JWKS discovered from C configuration. =head2 oauth2.logout_url $url = $c->oauth2->logout_url($provider_name => \%args); When L is being used in OpenID Connect mode this helper creates the url to redirect to end the session. The OpenID Connect Provider will redirect to the C provided in C<%args>. Additional keys for C<%args> are C and C. =head2 oauth2.providers $hash_ref = $c->oauth2->providers; This helper allow you to access the raw providers mapping, which looks something like this: { facebook => { authorize_url => "https://graph.facebook.com/oauth/authorize", token_url => "https://graph.facebook.com/oauth/access_token", key => ..., secret => ..., }, ... } =head1 ATTRIBUTES =head2 providers $hash_ref = $oauth2->providers; Holds a hash of provider information. See L. =head1 METHODS =head2 register $app->plugin(OAuth2 => \%provider_config); $app->plugin(OAuth2 => {providers => \%provider_config, proxy => 1, ua => Mojo::UserAgent->new}); Will register this plugin in your application with a given C<%provider_config>. The keys in C<%provider_config> are provider names and the values are configuration for each provider. Note that the value will be merged with the predefined providers below. Instead of just passing in C<%provider_config>, it is possible to pass in a more complex config, with these keys: =over 2 =item * providers The C<%provider_config> must be present under this key. =item * proxy Setting this to a true value will automatically detect proxy settings using L. =item * ua A custom L, in case you want to change proxy settings, timeouts or other attributes. =back Instead of just passing in C<%provider_config>, it is possible to pass in a hash-ref "providers" (C<%provider_config>) and "ua" (a custom L object). Here is an example to add adddition information like "key" and "secret": $app->plugin(OAuth2 => { providers => { custom_provider => { key => 'APP_ID', secret => 'SECRET_KEY', authorize_url => 'https://provider.example.com/auth', token_url => 'https://provider.example.com/token', }, github => { key => 'APP_ID', secret => 'SECRET_KEY', }, }, }); For L, C and C are configured from the C so these are replaced by the C key. $app->plugin(OAuth2 => { providers => { azure_ad => { key => 'APP_ID', secret => 'SECRET_KEY', well_known_url => 'https://login.microsoftonline.com/tenant-id/v2.0/.well-known/openid-configuration', }, }, }); To make it a bit easier the are already some predefined providers bundled with this plugin: =head3 dailymotion Authentication for L video site. =head3 debian_salsa Authentication for L. =head3 eventbrite Authentication for L event site. See also L. =head3 facebook OAuth2 for Facebook's graph API, L. You can find C (App ID) and C (App Secret) from the app dashboard here: L. See also L. =head3 instagram OAuth2 for Instagram API. You can find C (Client ID) and C (Client Secret) from the app dashboard here: L. See also L. =head3 github Authentication with Github. See also L. =head3 google OAuth2 for Google. You can find the C (CLIENT ID) and C (CLIENT SECRET) from the app console here under "APIs & Auth" and "Credentials" in the menu at L. See also L. =head3 vkontakte OAuth2 for Vkontakte. You can find C (App ID) and C (Secure key) from the app dashboard here: L. See also L. =head1 AUTHOR Marcus Ramberg - C Jan Henning Thorsen - C =head1 LICENSE This software is licensed under the same terms as Perl itself. =head1 SEE ALSO =over 2 =item * L =item * L =item * L =item * L =item * L =back =cut Mojolicious-Plugin-OAuth2-2.02/lib/Mojolicious/Plugin/OAuth2/000755 000765 000024 00000000000 14200436242 025033 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OAuth2-2.02/lib/Mojolicious/Plugin/OAuth2/Mock.pm000644 000765 000024 00000020117 14135762102 026267 0ustar00jhthorsenstaff000000 000000 package Mojolicious::Plugin::OAuth2::Mock; use Mojo::Base -base; require Mojolicious::Plugin::OAuth2; use constant DEBUG => $ENV{MOJO_OAUTH2_DEBUG} || 0; has provider => sub { return { authorization_endpoint_url => '/mocked/oauth2/authorize', end_session_endpoint_url => '/mocked/oauth2/logout', issuer_url => '/mocked/oauth2/v2.0', jwks_url => '/mocked/oauth2/keys', return_code => 'fake_code', return_token => 'fake_token', token_endpoint_url => '/mocked/oauth2/token', }; }; has _rsa => sub { require Crypt::OpenSSL::RSA; Crypt::OpenSSL::RSA->generate_key(2048) }; sub apply_to { my $self = ref $_[0] ? shift : shift->SUPER::new; my ($app, $provider) = @_; map { $self->provider->{$_} = $provider->{$_} } keys %$provider if $provider; push @{$app->renderer->classes}, __PACKAGE__; # Add mocked routes for "authorize", "token", ... for my $k (keys %{$self->provider}) { next unless $k =~ m!^([a-z].+)_url$!; my $method = "_action_$1"; my $url = $self->provider->{$k}; warn "[Oauth2::Mock] $url => $method()\n" if DEBUG; $app->routes->any($url => sub { $self->$method(@_) }); } } sub _action_authorization_endpoint { my ($self, $c) = @_; if ($c->param('response_mode') eq 'form_post') { return $c->render( template => 'oauth2/mock/form_post', format => 'html', code => "authorize-code", redirect_uri => $c->param('redirect_uri'), state => $c->param('state') ); } # $c->param('response_mode') eq 'query' my $url = Mojo::URL->new($c->param('redirect_uri')); $url->query({code => 'authorize-code', state => $c->param('state')}); return $c->redirect_to($url); } sub _action_authorize { my ($self, $c) = @_; if ($c->param('client_id') and $c->param('redirect_uri')) { my $url = Mojo::URL->new($c->param('redirect_uri')); $url->query->append(code => $self->provider->{return_code}); $c->render(text => $c->tag('a', href => $url, sub {'Connect'})); } else { $c->render(text => "Invalid request\n", status => 400); } } sub _action_end_session_endpoint { my ($self, $c) = @_; my $rp_url = Mojo::URL->new($c->param('post_logout_redirect_uri')) ->query({id_token_hint => $c->param('id_token_hint'), state => $c->param('state')}); $c->redirect_to($rp_url); } sub _action_issuer { my ($self, $c) = @_; } sub _action_jwks { my ($self, $c) = @_; my ($n, $e) = $self->_rsa->get_key_parameters; my $x5c = $self->_rsa->get_public_key_string; $x5c =~ s/\n/\\n/g; require MIME::Base64; return $c->render( template => 'oauth2/mock/keys', format => 'json', n => MIME::Base64::encode_base64url($n->to_bin), e => MIME::Base64::encode_base64url($e->to_bin), x5c => $x5c, issuer => $c->url_for($self->provider->{issuer_url})->to_abs, ); } sub _action_token { my ($self, $c) = @_; return $c->render(text => 'FAIL OVERFLOW', status => 404) unless 3 == grep { $c->param($_) } qw(client_secret redirect_uri code); $c->render( text => Mojo::Parameters->new( access_token => $self->provider->{return_token}, expires_in => 3600, refresh_token => Mojo::Util::md5_sum(rand), scope => $self->provider->{scopes} || 'some list of scopes', token_type => 'bearer', )->to_string ); } sub _action_token_endpoint { my ($self, $c) = @_; return $c->render(json => {error => 'invalid_request'}, status => 500) unless (($c->param('client_secret') and $c->param('redirect_uri') and $c->param('code')) || ($c->param('grant_type') eq 'refresh_token' and $c->param('refresh_token'))); my $claims = { aud => $c->param('client_id'), email => 'foo.bar@example.com', iss => $c->url_for($self->provider->{issuer_url})->to_abs, name => 'foo bar', preferred_username => 'foo.bar@example.com', sub => 'foo.bar' }; require Mojo::JWT; my $id_token = Mojo::JWT->new( algorithm => 'RS256', secret => $self->_rsa->get_private_key_string, set_iat => 1, claims => $claims, header => {kid => 'TEST_SIGNING_KEY'} ); return $c->render( template => 'oauth2/mock/token', format => 'json', id_token => $id_token->expires(Mojo::JWT->now + 3600)->encode, refresh_token => $c->param('refresh_token') // 'refresh-token', ); } sub _action_well_known { my ($self, $c) = @_; my $provider = $self->provider; my $req_url = $c->req->url->to_abs; my $to_abs = sub { $req_url->path(Mojo::URL->new(shift)->path)->to_abs }; $c->render( template => 'oauth2/mock/configuration', format => 'json', authorization_endpoint => $to_abs->($provider->{authorization_endpoint_url}), end_session_endpoint => $to_abs->($provider->{end_session_endpoint_url}), issuer => $to_abs->($provider->{issuer_url}), jwks_uri => $to_abs->($provider->{jwks_url}), token_endpoint => $to_abs->($provider->{token_endpoint_url}), ); } 1; =encoding utf8 =head1 NAME Mojolicious::Plugin::OAuth2::Mock - Mock an Oauth2 and/or OpenID Connect provider =head1 SYNOPSIS use Mojolicious::Plugin::OAuth2::Mock; use Mojolicious; my $app = Mojolicious->new; Mojolicious::Plugin::OAuth2::Mock->apply_to($app); =head1 DESCRIPTION L is an EXPERIMENTAL module to make it easier to test your L based code. =head1 METHODS =head2 apply_to Mojolicious::Plugin::OAuth2::Mock->apply_to($app, \%provider_args); $mock->apply_to($app, \%provider_args); Used to add mocked routes to a L application, based on all the keys in C<%provider_args> that end with "_url". Example: * authorize_url => /mocked/oauth/authorize * authorization_endpoint_url => /mocked/oauth2/authorize * end_session_endpoint_url => /mocked/oauth2/logout * issuer_url => /mocked/oauth2/v2.0 * jwks_url => /mocked/oauth2/keys * token_url => /mocked/oauth/token * token_endpoint_url => /mocked/oauth2/token =head1 SEE ALSO L. =cut __DATA__ @@ oauth2/mock/configuration.json.ep { "authorization_endpoint":"<%= $authorization_endpoint %>", "claims_supported":["sub","iss","aud","exp","iat","auth_time","acr","nonce","name","ver","at_hash","c_hash","email"], "end_session_endpoint":"<%= $end_session_endpoint %>", "id_token_signing_alg_values_supported":["RS256"], "issuer":"<%= $issuer %>", "jwks_uri":"<%= $jwks_uri %>", "request_uri_parameter_supported":0, "response_modes_supported":["query","fragment","form_post"], "response_types_supported":["code","id_token","code id_token","id_token token"], "scopes_supported":["openid","profile","email","offline_access"], "subject_types_supported":["pairwise"], "token_endpoint":"<%= $token_endpoint %>", "token_endpoint_auth_methods_supported":["client_secret_post","private_key_jwt","client_secret_basic"] } @@ oauth2/mock/keys.json.ep { "keys":[{ "e":"<%= $e %>", "issuer":"<%= $issuer %>", "kid":"TEST_SIGNING_KEY", "kty":"RSA", "n":"<%= $n %>", "use":"sig", "x5c":"<%= $x5c %>", "x5t":"TEST_SIGNING_KEY" }] } @@ oauth2/mock/token.json.ep { "access_token":"access", "expires_in":3599, "ext_expires_in":3599, "id_token":"<%= $id_token %>", "refresh_token":"<%= $refresh_token %>", "scope":"openid", "token_type":"Bearer" } @@ oauth2/mock/form_post.html.ep In progress...
Mojolicious-Plugin-OAuth2-2.02/.github/workflows/000755 000765 000024 00000000000 14200436242 023026 5ustar00jhthorsenstaff000000 000000 Mojolicious-Plugin-OAuth2-2.02/.github/workflows/macos.yml000644 000765 000024 00000000654 14135752446 024676 0ustar00jhthorsenstaff000000 000000 name: macos on: push: branches: - '*' tags-ignore: - '*' pull_request: jobs: perl: runs-on: macOS-latest steps: - uses: actions/checkout@v2 - name: Set up Perl run: brew install perl - name: perl -V run: perl -V - name: Install Dependencies run: curl -L https://cpanmin.us | perl - --installdeps . - name: Run Tests run: prove -l t Mojolicious-Plugin-OAuth2-2.02/.github/workflows/linux.yml000644 000765 000024 00000001576 14136226710 024726 0ustar00jhthorsenstaff000000 000000 name: linux on: push: branches: - '*' tags-ignore: - '*' pull_request: jobs: perl: runs-on: ubuntu-latest strategy: matrix: perl-version: - '5.16' - '5.18' - '5.20' - '5.22' - '5.30' - '5.32' container: image: perl:${{ matrix.perl-version }} steps: - uses: actions/checkout@v2 - name: perl -V run: perl -V - name: Fix ExtUtils::MakeMaker (for Perl 5.16 and 5.18) run: cpanm -n App::cpanminus ExtUtils::MakeMaker - name: Install dependencies run: | cpanm -n --installdeps . cpanm -n Crypt::OpenSSL::Bignum Crypt::OpenSSL::RSA IO::Socket::SSL Mojo::JWT cpanm -n Test::Pod Test::Pod::Coverage - name: Run tests run: prove -l t env: TEST_POD: 1 TEST_EV: 1 Mojolicious-Plugin-OAuth2-2.02/.pls_cache/index000644 000765 000024 00000020174 14200433550 022465 0ustar00jhthorsenstaff000000 000000 pst0  column_number line_numberlocation ?/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/t/Helper.pmfile t::Helper line_number column_numberlocation Y/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2/Mock.pmfile!Mojolicious::Plugin::OAuth2::Mock column_number line_numberlocation T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfileMojolicious::Plugin::OAuth2packages Mojolicious::Plugin::OAuth2packages register _args _get_authorize_url _get_logout_url _get_refresh_token _get_token _parse_provider_response _mock_interface _mock_interface_oidc _token_url_transact _warmup_openid _warmup_openid_provider_p MOJO_JWTsubs 1635124925 last_mtimeT/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pm 1635243302 last_mtime apply_to _action_authorization_endpoint _action_authorize _action_end_session_endpoint _action_issuer _action_jwks _action_token _action_token_endpoint _action_well_known DEBUGsubs !Mojolicious::Plugin::OAuth2::MockpackagesY/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2/Mock.pm 1535624518 last_mtime make_app importsubs t::Helperpackages?/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/t/Helper.pmfiles column_number line_numberlocation ?/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/t/Helper.pmfilemake_app $selflabel $clabel $argslabel parameters my ($self, $c, $args) = @_;label signature column_number line_numberlocation T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile_get_logout_url T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile column_number line_numberlocation my ($self, $tx) = @_;label $selflabel $txlabel parameters signature_parse_provider_response column_number line_numberlocation $selflabel $clabel parameters my ($self, $c) = @_;label signature Y/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2/Mock.pmfile_action_token_endpoint my ($self, $c, $args, $p) = @_;label $selflabel $clabel $argslabel $plabel parameters signature line_number column_numberlocation T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile_get_refresh_token T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile my ($self, $c, $args) = @_;label $selflabel $clabel $argslabel parameters signature column_number line_numberlocation_get_authorize_url ?/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/t/Helper.pmfile column_number line_numberlocationimport !my ($self, $app, $provider) = @_;label $selflabel $applabel $providerlabel parameters signature column_number line_numberlocation T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile_mock_interface_oidc T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile column_number line_numberlocation my ($self, $app) = @_;label $selflabel $applabel parameters signature_mock_interface Y/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2/Mock.pmfileconstant column_number line_numberlocationDEBUG line_number column_numberlocation T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile_args line_number column_numberlocation $selflabel $clabel parameters my ($self, $c) = @_;label signature Y/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2/Mock.pmfile_action_end_session_endpoint Y/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2/Mock.pmfile column_number line_numberlocation $selflabel $clabel parameters my ($self, $c) = @_;label signature _action_token T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile k line_number column_numberlocation_warmup_openid_provider_p column_number ` line_numberlocation T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile_warmup_openid T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile line_number column_numberlocation my ($self, $app, $config) = @_;label $selflabel $applabel $configlabel parameters signatureregister line_number column_numberlocation $selflabel $clabel parameters my ($self, $c) = @_;label signature Y/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2/Mock.pmfile_action_authorization_endpoint $selflabel $clabel parameters my ($self, $c) = @_;label signature line_number column_numberlocation Y/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2/Mock.pmfile _action_jwks T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile column_number line_numberlocationconstantMOJO_JWT -my ($self, $token_url, $params, $p, $c) = @_;label $selflabel $token_urllabel $paramslabel $plabel $clabel parameters signature T line_number column_numberlocation T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile_token_url_transact Y/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2/Mock.pmfile my ($self, $c) = @_;label $selflabel $clabel parameters signature line_number column_numberlocation_action_well_known Y/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2/Mock.pmfile line_number column_numberlocationapply_to T/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2.pmfile $selflabel $clabel $argslabel $plabel parameters my ($self, $c, $args, $p) = @_;label signature line_number column_numberlocation _get_token Y/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2/Mock.pmfile column_number line_numberlocation my ($self, $c) = @_;label $selflabel $clabel parameters signature_action_authorize line_number column_numberlocation my ($self, $c) = @_;label $selflabel $clabel parameters signature Y/Users/jhthorsen/git/_old/mojolicious-plugin-oauth2/lib/Mojolicious/Plugin/OAuth2/Mock.pmfile_action_issuersubsMojolicious-Plugin-OAuth2-2.02/t/error.t000644 000765 000024 00000001224 14200427235 021213 0ustar00jhthorsenstaff000000 000000 use lib '.'; use t::Helper; my $app = t::Helper->make_app; my $t = Test::Mojo->new($app); $t->app->ua->server($t->ua->server); $app->routes->get( '/oauth-error' => sub { my $c = shift; $c->oauth2->get_token_p('test')->then(sub { return unless my $provider_res = shift; return $c->render(text => "Token $provider_res->{access_token}"); })->catch(sub { return $c->render(text => shift, status => 500); }); } ); $t->get_ok('/oauth-error')->status_is(302); # ->content_like(qr/bar/); $t->get_ok('/oauth-error?code=123')->status_is(200); $t->get_ok('/oauth-error?error=access_denied')->status_is(500); done_testing; Mojolicious-Plugin-OAuth2-2.02/t/mocked.t000644 000765 000024 00000003217 14200426121 021321 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Mojolicious; use Test::Mojo; use Test::More; { use Mojolicious::Lite; plugin OAuth2 => {mocked => {key => '42'}}; get '/no-redirect' => sub { my $c = shift; return $c->oauth2->get_token_p('mocked', {redirect => 0})->then(sub { return $c->render(text => 'No token') unless my $provider_res = shift; # Redirect return $c->render(text => "Token $provider_res->{access_token}"); })->catch(sub { return $c->render(text => shift, status => 500); }); }; get '/profile' => sub { my $c = shift; return $c->oauth2->get_token_p('mocked')->then(sub { return unless my $provider_res = shift; # Redirect return $c->render(text => "Token $provider_res->{access_token}"); })->catch(sub { return $c->render(text => shift, status => 500); }); }; } my $t = Test::Mojo->new; $t->get_ok('/profile')->status_is(302); my $location = Mojo::URL->new($t->tx->res->headers->location); my $callback_url = Mojo::URL->new($location->query->param('redirect_uri')); is($location->query->param('client_id'), '42', 'got client_id'); $t->get_ok($location)->status_is(200)->element_exists('a'); my $res = Mojo::URL->new($t->tx->res->dom->at('a')->{href}); is($res->path, $callback_url->path, 'Returns to the right place'); is($res->query->param('code'), 'fake_code', 'Includes fake code'); $t->get_ok($res)->status_is(200)->content_is('Token fake_token'); $t->get_ok('/profile?error=access_denied')->status_is(500)->content_is('access_denied'); $t->get_ok('/no-redirect')->status_is(200)->content_like(qr{No token}); done_testing; Mojolicious-Plugin-OAuth2-2.02/t/00-basic.t000644 000765 000024 00000002134 13341712236 021364 0ustar00jhthorsenstaff000000 000000 use Test::More; use File::Find; if(($ENV{HARNESS_PERL_SWITCHES} || '') =~ /Devel::Cover/) { plan skip_all => 'HARNESS_PERL_SWITCHES =~ /Devel::Cover/'; } if(!eval 'use Test::Pod; 1') { *Test::Pod::pod_file_ok = sub { SKIP: { skip "pod_file_ok(@_) (Test::Pod is required)", 1 } }; } if(!eval 'use Test::Pod::Coverage; 1') { *Test::Pod::Coverage::pod_coverage_ok = sub { SKIP: { skip "pod_coverage_ok(@_) (Test::Pod::Coverage is required)", 1 } }; } if(!eval 'use Test::CPAN::Changes; 1') { *Test::CPAN::Changes::changes_file_ok = sub { SKIP: { skip "changes_ok(@_) (Test::CPAN::Changes is required)", 4 } }; } find( { wanted => sub { /\.pm$/ and push @files, $File::Find::name }, no_chdir => 1 }, -e 'blib' ? 'blib' : 'lib', ); plan tests => @files * 3 + 4; 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 $@; Test::Pod::pod_file_ok($file); Test::Pod::Coverage::pod_coverage_ok($module, { also_private => [ qr/^[A-Z_]+$/ ], }); } Test::CPAN::Changes::changes_file_ok(); Mojolicious-Plugin-OAuth2-2.02/t/Helper.pm000644 000765 000024 00000002720 14200427163 021454 0ustar00jhthorsenstaff000000 000000 package t::Helper; use Mojo::Base -strict; use Mojolicious; use Test::Mojo; use Test::More; sub make_app { my $app = Mojolicious->new; $app->plugin( OAuth2 => { ua => $app->ua, providers => { test => { authorize_url => '/oauth/authorize', token_url => '/oauth/token', key => 'fake_key', secret => 'fake_secret', scope => 'a,b,c', }, }, } ); $app->routes->get( '/oauth/authorize' => sub { my $c = shift; if ($c->param('client_id') and $c->param('redirect_uri') and $c->param('scope')) { my $return = Mojo::URL->new($c->param('redirect_uri')); $return->query->append(code => 'fake_code'); $c->redirect_to($return); } else { $c->render(status => 404, text => 'REJECTED'); } } ); $app->routes->post( '/oauth/token' => sub { my $c = shift; if ($c->param('client_secret') and $c->param('redirect_uri') and $c->param('code')) { my $qp = Mojo::Parameters->new(access_token => 'fake_token', lifetime => 3600); $c->render(text => $qp->to_string); } else { $c->render(status => 404, text => 'FAIL OVERFLOW'); } } ); return $app; } sub import { my $class = shift; my $caller = caller; strict->import; warnings->import; eval <<"HERE" or die; package $caller; use Test::Mojo; use Test::More; 1; HERE } 1; Mojolicious-Plugin-OAuth2-2.02/t/openid-connect.t000644 000765 000024 00000020161 14136466376 023011 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::More; use Test::Mojo; use MIME::Base64 qw(encode_base64url); use Mojo::JSON qw(decode_json encode_json); use Mojo::URL; use Mojolicious::Plugin::OAuth2; plan skip_all => "Mojo::JWT, Crypt::OpenSSL::RSA and Crypt::OpenSSL::Bignum required for openid tests" unless Mojolicious::Plugin::OAuth2::MOJO_JWT; use Mojolicious::Lite; plugin OAuth2 => {mocked => {key => 'c0e71b99-2c66-42e7-8589-6502153a7e3', well_known_url => '/mocked/oauth2/.well-known/configuration'}}; get '/' => sub { shift->render('index') }; any '/connect' => sub { my $c = shift; $c->render_later; my $get_token_args = { redirect_uri => $c->req->url->to_abs, authorize_query => { response_mode => $ENV{'OAUTH2_MOCK_RESPONSE_MODE'} // 'form_post', response_type => 'code', state => $c->param('oauth2.state') // 'test' } }; $c->oauth2->get_token_p(mocked => $get_token_args)->then(sub { return unless my $provider_res = shift; # Redirect to IdP $c->session(token => $provider_res->{access_token}, refresh_token => $provider_res->{refresh_token}); my $user = $c->oauth2->jwt_decode(mocked => data => $provider_res->{id_token}); $c->signed_cookie(id_token => $provider_res->{id_token}); return $c->redirect_to($c->param('state')) if $c->param('state') ne 'test'; $c->render(json => $user); })->catch(sub { $c->render(text => "Error $_[0]", status => 500); }); }; # exercise end_session_endpoint get '/end_session' => sub { my $c = shift; my $home = $c->req->url->base->clone->tap(path => '/'); # require id_token to calculate logout_url return $c->redirect_to($home) unless my $id_token = $c->signed_cookie('id_token'); my $end_session_url = $c->oauth2->logout_url( mocked => {post_logout_redirect_uri => $c->req->url->to_abs, id_token_hint => $id_token, state => time}); return $c->redirect_to($end_session_url) unless $c->param('id_token_hint') and my $state = $c->param('state'); $c->signed_cookie('id_token' => $id_token, {expires => time - 1}); delete $c->session->{$_} for (qw(token refresh_token)); return $c->redirect_to($home); }; # refresh access token using refresh_token get '/refresh' => sub { my $c = shift; $c->render_later; $c->oauth2->get_refresh_token_p(mocked => {refresh_token => $c->session('refresh_token') . '+'})->then(sub { my $res = shift; $c->session(refresh_token => $res->{refresh_token}); $c->render(json => $res); })->catch(sub { $c->render(text => "Error $_[0]", status => 500); }); }; group { under '/protect' => sub { my $c = shift; Mojo::IOLoop->timer( 0.1 => sub { $c->redirect_to($c->url_for('connect')->query({'oauth2.state' => $c->req->url})); } ); return undef unless $c->session('token'); return 1; }; get '/next' => sub { shift->render(text => 'ok') }; }; my $t = Test::Mojo->new; subtest 'warmup of provider data' => sub { my $provider_conf = $t->app->oauth2->providers->{mocked}; is $provider_conf->{scope}, 'openid', 'scope'; is $provider_conf->{userinfo_url}, undef, 'userinfo_url'; ok $provider_conf->{jwt}, 'resolved from configuration'; ok +Mojo::URL->new($provider_conf->{$_})->scheme, $_ for qw(authorize_url end_session_url issuer token_url); }; subtest 'Authorize and obtain token - form_post response_mode' => sub { $t->get_ok('/connect')->status_is(302); my $location = Mojo::URL->new($t->tx->res->headers->location); is $location->query->param('scope'), 'openid', 'scope set'; is $location->query->param('response_mode'), 'form_post', 'response mode set'; my ($action, $form); $t->get_ok($location)->status_is(200)->tap(sub { my $dom = shift->tx->res->dom; $action = $dom->at('form')->attr('action'); $form = {code => $dom->at('input[name=code]')->attr('value'), state => $dom->at('input[name=state]')->attr('value')}; ok +Mojo::URL->new($action)->is_abs, 'absolute url'; }); $t->post_ok($action, form => $form)->status_is(200)->json_is('/aud' => 'c0e71b99-2c66-42e7-8589-6502153a7e3') ->json_is('/email' => 'foo.bar@example.com') ->json_is('/iss' => $t->app->oauth2->providers->{mocked}{issuer}, 'OIDC valid (MUST)') ->json_is('/name' => 'foo bar')->json_is('/preferred_username' => 'foo.bar@example.com') ->json_is('/sub' => 'foo.bar')->json_has('/iat')->json_has('/exp'); }; subtest 'Refresh token' => sub { $t->get_ok('/refresh')->status_is(200)->json_is('/refresh_token', 'refresh-token+'); $t->get_ok('/refresh')->status_is(200)->json_is('/refresh_token', 'refresh-token++'); $t->get_ok('/refresh?error=bad')->status_is(500)->content_is('Error bad'); }; subtest 'Authorize and obtain token - query response_mode' => sub { local $ENV{OAUTH2_MOCK_RESPONSE_MODE} = 'query'; local $t->app->oauth2->providers->{mocked}{scope} = 'openid email profile'; $t->get_ok('/connect')->status_is(302); my $location = Mojo::URL->new($t->tx->res->headers->location); is $location->query->param('scope'), 'openid email profile', 'scope set'; is $location->query->param('response_mode'), 'query', 'response mode set'; is $location->query->param('state'), 'test', 'state propagates'; my ($action, $form); $t->get_ok($location)->status_is(302); $location = Mojo::URL->new($t->tx->res->headers->location); is $location->path, '/connect', 'redirect_uri'; is $location->query->param('code'), 'authorize-code', 'code set'; is $location->query->param('state'), 'test', 'state returned'; $t->get_ok("$location")->status_is(200)->json_is('/aud' => 'c0e71b99-2c66-42e7-8589-6502153a7e3') ->json_is('/email' => 'foo.bar@example.com') ->json_is('/iss' => $t->app->oauth2->providers->{mocked}{issuer}, 'OIDC valid (MUST)') ->json_is('/name' => 'foo bar')->json_is('/preferred_username' => 'foo.bar@example.com') ->json_is('/sub' => 'foo.bar')->json_has('/iat')->json_has('/exp'); }; subtest 'Logout' => sub { my $end_session_url = $t->ua->server->url->clone->tap(path => '/end_session'); # obtain signed cookie from user agent my $c = $t->app->build_controller->tap(sub { $_->tx->req->cookies(@{$t->ua->cookie_jar->all}) }); my $id_token = $c->signed_cookie('id_token'); ok $id_token, 'Have a current id token'; $t->get_ok($end_session_url)->status_is(302); my $op_location = Mojo::URL->new($t->tx->res->headers->location); is $op_location->path, '/mocked/oauth2/logout', 'correct'; is $op_location->query->param('id_token_hint'), $id_token, 'correct id token'; is $op_location->query->param('post_logout_redirect_uri'), $end_session_url, 'post_logout_redirect_uri set'; is $op_location->query->param('state'), time, 'state set'; $t->get_ok($op_location)->status_is(302); my $rp_location = Mojo::URL->new($t->tx->res->headers->location); is $rp_location->path, '/end_session', 'correct'; is $rp_location->query->param('id_token_hint'), $id_token, 'correct id token'; is $rp_location->query->param('state'), time, 'state set'; $t->get_ok($rp_location)->status_is(302); my $logged_out = Mojo::URL->new($t->tx->res->headers->location); is $logged_out->path, '/', 'home'; my @cookies = grep { $_->name eq 'id_token' } @{$t->ua->cookie_jar->find($end_session_url) || []}; is_deeply \@cookies, [], 'removed'; $t->get_ok($end_session_url)->status_is(302); $logged_out = Mojo::URL->new($t->tx->res->headers->location); is $logged_out->path, '/', 'home'; }; subtest 'Redirects with under' => sub { local $ENV{OAUTH2_MOCK_RESPONSE_MODE} = 'query'; my $max = $t->ua->max_redirects; my $url = $t->ua->server->url->clone->tap(path => '/protect/next'); $t->reset_session->ua->max_redirects($max + 5); $t->get_ok('/protect/next')->status_is(200)->content_is('ok'); is_deeply [map { $_->name } grep { $_->name eq 'id_token' } @{$t->ua->cookie_jar->find($url) || []}], ['id_token'], 'set cookie'; is_deeply [map { $_->req->url->path } @{$t->tx->redirects}], [qw(/protect/next /connect /mocked/oauth2/authorize /connect)], 'login chain'; $t->ua->max_redirects($max); }; done_testing; __DATA__ @@ index.html.ep %= link_to 'Connect', $c->url_for('connect'); Mojolicious-Plugin-OAuth2-2.02/t/live.t000644 000765 000024 00000002277 13341741767 021050 0ustar00jhthorsenstaff000000 000000 use Test::More; use Mojolicious::Lite; use Test::Mojo; use Test::More; unless ($ENV{OAUTH_FB_KEY} && $ENV{OAUTH_FB_SECRET}) { plan skip_all => 'OAUTH_FB_KEY and OAUTH_FB_SECRET must be set for oauth tests'; } plugin 'OAuth2', facebook => {key => $ENV{OAUTH_FB_KEY}, secret => $ENV{OAUTH_FB_SECRET}}; my ($token, $tx); get '/connect' => sub { my $self = shift; $self->oauth2->get_token_p('facebook')->then( sub { return unless my $provider_res = shift; # Redirect $self->render( json => $self->ua->get("https://graph.facebook.com/me?access_token=$provider_res->{access_token}")->res->json); } )->catch( sub { $self->render(json => {message => shift, status => 500}); } ); }; my $t = Test::Mojo->new; $t->get_ok('/connect')->status_is(302)->header_like(Location => qr|https://graph\.facebook\.com/oauth/authorize|) ->header_like(Location => qr|\bclient_id=$ENV{OAUTH_FB_KEY}\b|) ->header_like(Location => qr|\bredirect_uri=https?://[^/]+/connect\b|); # This i a bit ugly. Maybe it should be factored out in a different test? if ($ENV{OAUTH_FB_KEY} eq 'fail') { $t->get_ok('/connect?code=123')->status_is(500)->json_has('/message'); } done_testing; Mojolicious-Plugin-OAuth2-2.02/t/delayed.t000644 000765 000024 00000002452 14200427235 021475 0ustar00jhthorsenstaff000000 000000 use lib '.'; use t::Helper; my $app = t::Helper->make_app; my $t = Test::Mojo->new($app); $t->app->ua->server($t->ua->server); $app->routes->get( '/oauth-delayed' => sub { my $c = shift; $c->oauth2->get_token( test => sub { my (undef, $err, $provider_res) = @_; return $c->render(text => $err) unless $provider_res; return $c->render(text => "Token $provider_res->{access_token}"); } ); } ); $app->helper( 'oauth2.get_token' => sub { my ($c, $args, $cb) = @_; $c->oauth2->get_token_p($args)->then(sub { $c->$cb('', shift) }, sub { $c->$cb(shift, {}) }); } ); $t->get_ok('/oauth-delayed')->status_is(302); # ->content_like(qr/bar/); my $location = Mojo::URL->new($t->tx->res->headers->location); my $redirect_uri = Mojo::URL->new($location->query->param('redirect_uri')); is($location->query->param('client_id'), 'fake_key', 'got client_id'); note $location; $t->get_ok($location)->status_is(302); $location = Mojo::URL->new($t->tx->res->headers->location || '/not/302'); is($location->path, $redirect_uri->path, 'Returns to the right place'); is($location->query->param('code'), 'fake_code', 'Includes fake code'); note $location; $t->get_ok($location)->status_is(200)->content_is('Token fake_token'); done_testing; Mojolicious-Plugin-OAuth2-2.02/t/auth_url.t000644 000765 000024 00000002630 14135762126 021717 0ustar00jhthorsenstaff000000 000000 use Mojo::Base -strict; use Test::Mojo; use Test::More; my $authorize_args = {}; use Mojolicious::Lite; plugin 'OAuth2' => {facebook => {key => 'KEY'}}; get '/test123', sub { $_[0]->render(text => $_[0]->oauth2->auth_url('facebook', $authorize_args)); }; my $t = Test::Mojo->new; eval { $t->app->oauth2->auth_url }; like $@, qr{Invalid provider}, 'provider_id is required'; $t->get_ok('/test123')->status_is(200); my $url = Mojo::URL->new($t->tx->res->body); like $url, qr{^https://graph\.facebook\.com/oauth/authorize}, 'base url'; is $url->query->param('client_id'), 'KEY', 'client_id'; like $url->query->param('redirect_uri'), qr{/test123$}, 'redirect_uri'; $authorize_args = { scope => 'email,age', redirect_uri => 'https://example.com', host => 'oauth2.example.com', state => '42', authorize_query => {foo => 123}, }; $t->get_ok('/test123')->status_is(200); $url = Mojo::URL->new($t->tx->res->body); like $url, qr{^https://oauth2\.example\.com/oauth/authorize}, 'custom.host'; is $url->query->param('foo'), '123', 'foo'; is $url->query->param('client_id'), 'KEY', 'client_id'; is $url->query->param('redirect_uri'), 'https://example.com', 'custom.redirect_uri'; is $url->query->param('scope'), 'email,age', 'scope'; is $url->query->param('state'), '42', 'state'; done_testing;