File Coverage

blib/lib/Dancer2/Plugin/Auth/OAuth.pm
Criterion Covered Total %
statement 11 11 100.0
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 15 15 100.0


line stmt bran cond sub pod time code
1              
2             use strict;
3 2     2   899883 use 5.008_005;
  2         11  
  2         90  
4 2     2   43 our $VERSION = '0.20';
  2         5  
5              
6             use Dancer2::Plugin;
7 2     2   819 use Module::Load;
  2         193286  
  2         11  
8 2     2   34176  
  2         790  
  2         10  
9             # setup the plugin
10             on_plugin_import {
11             my $dsl = shift;
12             my $settings = plugin_setting;
13              
14             $settings->{prefix} ||= '/auth';
15              
16             for my $provider ( keys %{$settings->{providers} || {}} ) {
17              
18             # load the provider plugin
19             my $provider_class = __PACKAGE__."::Provider::".$provider;
20             eval { load $provider_class; 1; } or do {
21             $dsl->app->log(debug => "Couldn't load $provider_class");
22             next;
23             };
24             $dsl->app->{_oauth}{$provider} ||= $provider_class->new($settings, $dsl);
25              
26             # add the routes
27             $dsl->app->add_route(
28             method => 'get',
29             regexp => sprintf( "%s/%s", $settings->{prefix}, lc($provider) ),
30             code => sub {
31             $dsl->app->redirect(
32             $dsl->app->{_oauth}{$provider}->authentication_url(
33             $dsl->app->request->uri_base
34             )
35             )
36             },
37             );
38             $dsl->app->add_route(
39             method => 'get',
40             regexp => sprintf( "%s/%s/callback", $settings->{prefix}, lc($provider) ),
41             code => sub {
42             my $redirect;
43             if( $dsl->app->{_oauth}{$provider}->callback($dsl->app->request, $dsl->app->session) ) {
44             $redirect = $settings->{success_url} || '/';
45             } else {
46             $redirect = $settings->{error_url} || '/';
47             }
48              
49             $dsl->app->redirect( $redirect );
50             },
51             );
52             $dsl->app->add_route(
53             method => 'get',
54             regexp => sprintf( "%s/%s/refresh", $settings->{prefix}, lc($provider) ),
55             code => sub {
56             my $redirect;
57             if( $dsl->app->{_oauth}{$provider}->refresh($dsl->app->request, $dsl->app->session) ) {
58             $redirect = $settings->{success_url} || '/';
59             } else {
60             if ($settings->{reauth_on_refresh_fail}) {
61             $redirect = $settings->{prefix}."/".lc($provider);
62             } else {
63             $redirect = $settings->{error_url} || '/';
64             }
65             }
66             $dsl->app->redirect( $redirect );
67             },
68             );
69             }
70             };
71              
72             register_plugin;
73              
74             1;
75              
76             =encoding utf-8
77              
78             =head1 NAME
79              
80             Dancer2::Plugin::Auth::OAuth - OAuth for your Dancer2 app
81              
82             =head1 SYNOPSIS
83              
84             # just 'use' the plugin, that's all.
85             use Dancer2::Plugin::Auth::OAuth;
86              
87             =head1 DESCRIPTION
88              
89             Dancer2::Plugin::Auth::OAuth is a Dancer2 plugin which tries to make OAuth
90             authentication easy.
91              
92             The module is highly influenced by L<Plack::Middleware::OAuth> and Dancer 1
93             OAuth modules, but unlike the Dancer 1 versions, this plugin only needs
94             configuration (look mom, no code needed!). It automatically sets up the
95             needed routes (defaults to C</auth/$provider> and C</auth/$provider/callback>).
96             So if you define the Twitter provider in your config, you should automatically
97             get C</auth/twitter> and C</auth/twitter/callback>.
98              
99             After a successful OAuth dance, the user info is stored in the session "oauth".
100             What you do with it afterwards is up to you. Please note the user will
101             continue to be authenticated until the Dancer2 session has expired,
102             whenever that might be.
103              
104             =head1 CONFIGURATION
105              
106             The plugin comes with support for Facebook, Google, Twitter, GitHub, Stack
107             Exchange, LinkedIn and several more (other providers aren't hard to add,
108             send me a pull request when you add more!).
109              
110             All it takes to use OAuth authentication for a given provider, is to add
111             the configuration for it. You don't need anything else.
112              
113             The YAML below shows all available options.
114              
115             plugins:
116             "Auth::OAuth":
117             reauth_on_refresh_fail: 0 [*]
118             prefix: /auth [*]
119             success_url: / [*]
120             error_url: / [*]
121             providers:
122             Facebook:
123             tokens:
124             client_id: your_client_id
125             client_secret: your_client_secret
126             fields: id,email,name,gender,picture
127             # Original default Facebook scope was 'email,public_profile,user_friends'
128             # Since March 2018 'user_friends' requires an app review.
129             # Add the following three lines if you don't have it reviewed.
130             query_params:
131             authorize:
132             scope: email,public_profile
133             Google:
134             tokens:
135             client_id: your_client_id
136             client_secret: your_client_secret
137             AzureAD:
138             tokens:
139             client_id: your_client_id
140             client_secret: your_client_secret
141             Twitter:
142             tokens:
143             consumer_key: your_consumer_token
144             consumer_secret: your_consumer_secret
145             Github:
146             tokens:
147             client_id: your_client_id
148             client_secret: your_client_secret
149             Stackexchange:
150             tokens:
151             client_id: your_client_id
152             client_secret: your_client_secret
153             key: your_key
154             site: stackoverflow
155             Linkedin:
156             tokens:
157             client_id: your_client_id
158             client_secret: your_client_secret
159             fields: id,num-connections,picture-url,email-address
160             VKontakte: # https://vk.com
161             tokens:
162             client_id: your_client_id
163             client_secret: your_client_secret
164             fields: 'first_name,last_name,about,bdate,city,country,photo_max_orig,sex,site'
165             api_version: '5.8'
166             Odnoklassniki: # https://ok.ru
167             tokens:
168             client_id: your_client_id
169             client_secret: your_client_secret
170             application_key: your_application_key
171             method: 'users.getCurrentUser'
172             format: 'json'
173             fields: 'email,name,gender,birthday,location,uid,pic_full'
174             MailRU:
175             tokens:
176             client_id: your_client_id
177             client_private: your_client_private
178             client_secret: your_client_secret
179             method: 'users.getInfo'
180             format: 'json'
181             secure: 1
182             Yandex:
183             tokens:
184             client_id: your_client_id
185             client_secret: your_client_secret
186             format: 'json'
187             SalesForce:
188             tokens:
189             client_id: your_client_id
190             client_secret: your_client_secret
191              
192             [*] default value, may be omitted.
193              
194              
195             =head2 FUNCTIONAL LOGIN
196              
197             The main purpose of this module is simply to authenticate against a third
198             party Identity Provider (IdP).
199              
200             However you can get a bit more than that.
201              
202             Your Dancer2 app might additionally use the "id_token" to access the API of
203             the same (or other) third parties to enable you to do cool stuff with your
204             apps, like show a feed, access data, etc.
205              
206             Because access to the third party systems would be cut off when the "id_token"
207             expires, Dancer2::Plugin::Auth::OAuth will automatically set up the route
208             C</auth/$provider/refresh>. Call this when the token has expired to try to
209             refresh the token without bumping the user back to log in. You can optionally
210             tell Dancer2::Plugin::Auth::OAuth to bump the user back to the login page if
211             for whatever reason the refresh fails.
212              
213             In addition, Dancer2::Plugin::Auth::OAuth will save or generate an auth session
214             key called "expires", which is (usually) number of seconds from epoch. Check
215             this to determine if the "id_token" has expired (see examples below).
216              
217             Authenticate using one of the examples below but be sure to use the
218             'refresh' functionality, as the logged in user will need to have a
219             valid "id_token" at all times.
220              
221             Also make sure that you set the scope of your authentication to tell the third
222             party what you wish to access (and for Microsoft/Azure also set the resource,
223             for the same reason).
224              
225             Once you've got an active session you can get the "id_token" to use in further
226             calls to the providers backend systems with:
227              
228             my $session_data = session->read('oauth');
229             my $token = $session_data->{$provider}{id_token};
230              
231             =head1 SETTING THE SCOPE
232              
233             If you're authenticating in order to use the "id_token" issued, or if login
234             requires a specific 'scope' setting, you can change these values in the initial
235             calls like this within your YAML config (example provided for AzureAD plugin).
236              
237             Auth::OAuth:
238             providers:
239             AzureAD:
240             query_params:
241             authorize:
242             scope: 'Calendars.ReadWrite Contacts.Read Directory.Read.All Files.Read.All Group.Read.All GroupMember.Read.All Mail.ReadWrite openid People.Read Sites.Read.All Sites.ReadWrite.All User.Read User.ReadBasic.All Files.Read.All'
243              
244             You do not need to list all other authorize attributes sent to the server,
245             unless you want to change them from the default values set in the provider.
246             Please view the provider source/documentation for what these default values are.
247              
248             You may also need to set a value for "resource" in the same way. Refer to your
249             providers OAuth documentation.
250              
251             =head1 AUTHENTICATION EXAMPLES
252              
253             The response from the IdP is stored as a hash in the session with key "oauth".
254             An example of a Facebook response:
255              
256             {
257             facebook {
258             access_token "...",
259             expires 1662472004,
260             expires_in 5183933,
261             issued_at 1657288071,
262             token_type "bearer",
263             user_info {
264             email "someone@example.com",
265             id 12345678901234567,
266             name "José do Telhado",
267             picture {
268             data {
269             height 50,
270             is_silhouette 0,
271             url "https://platform-lookaside.fbsbx.com/platform/profilepic/...",
272             width 50
273             }
274             },
275             }
276             }
277             }
278              
279             =over
280              
281             =item Full site needs a user authentication for a specific IdP.
282              
283             An example of a simple single system authentication.
284              
285             hook before => sub {
286             my $session_data = session->read('oauth');
287             my $provider = "facebook"; # Lower case of the authentication plugin used
288              
289             if ((!defined $session_data || !defined $session_data->{$provider} || !defined $session_data->{$provider}{id_token}) && request->path !~ m{^/auth}) {
290             return forward "/auth/$provider";
291             }
292             };
293              
294             If you want to be sure they have a valid "id_token" at all times:
295              
296             hook before => sub {
297             my $session_data = session->read('oauth');
298             my $provider = "facebook"; # Lower case of the authentication plugin used
299              
300             my $now = DateTime->now->epoch;
301              
302             if ((!defined $session_data || !defined $session_data->{$provider} || !defined $session_data->{$provider}{id_token}) && request->path !~ m{^/auth}) {
303             return forward '/auth/$provider';
304              
305             } elsif (defined $session_data->{$provider}{refresh_token} && defined $session_data->{$provider}{expires} && $session_data->{$provider}{expires} < $now && request->path !~ m{^/auth}) {
306             return forward "/auth/$provider/refresh";
307              
308             }
309             };
310              
311             in the case where you're using the refresh functionality, a failure of the
312             refresh will send the user back to the "error_url". If you want to them
313             to instead be directed back to the main authentication (log in page) then
314             please set the configuration option C<reauth_on_refresh_fail>.
315              
316             If the provider(s) you are using don't have the "id_token"
317             change the example accordingly.
318              
319             =item Site has a mix of public zones and private or needing use authentication
320              
321             1. You only use one provider
322              
323             get '/we/need/a/user/here' => sub {
324             my $session_data = session->read('oauth');
325             my $provider = "facebook";
326              
327             redirect '/auth/$provider' unless $session_data && defined $session_data->{$provider};
328              
329             ...
330             }
331              
332              
333              
334             2. You also have a login page to choose from a list of
335             providers accepted by the site
336              
337             You may update the configuration file:
338              
339             "Auth::OAuth":
340             success_url: /login/ok
341             error_url: /login/fail
342              
343             And on your code
344              
345             get '/we/need/a/user/here' => sub {
346             my $session_data = session->read('oauth');
347              
348             redirect '/login' unless $session_data;
349              
350             ...
351             }
352              
353             get '/login/ok' => sub {
354             my $session_data = session->read('oauth');
355              
356             redirect '/login' unless $session_data;
357              
358             # Do something with the user data, update DB,
359             # update session, etc
360              
361             }
362              
363             The login page can just have a list of the providers with
364             a link to "/auth/<lc-name-of-the-provider>"
365              
366             You can mix this plugin with C<Dancer2::Plugin::Auth::Tiny> and
367             on '/login/ok' you just define the 'user' session. Afterwards
368             all validation can be against 'user' and not 'oauth'.
369              
370             =back
371              
372             =head1 AUTHOR
373              
374             Menno Blom E<lt>blom@cpan.orgE<gt>
375              
376             =head1 COPYRIGHT
377              
378             Copyright 2014- Menno Blom
379              
380             =head1 LICENSE
381              
382             This library is free software; you can redistribute it and/or modify
383             it under the same terms as Perl itself.
384              
385             =cut