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   931656 use 5.008_005;
  2         20  
  2         54  
4 2     2   54 our $VERSION = '0.19';
  2         6  
5              
6             use Dancer2::Plugin;
7 2     2   991 use Module::Load;
  2         231712  
  2         18  
8 2     2   40733  
  2         941  
  2         16  
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. What
100             you do with it afterwards is up to you.
101              
102             =head1 CONFIGURATION
103              
104             The plugin comes with support for Facebook, Google, Twitter, GitHub, Stack
105             Exchange and LinkedIn (other providers aren't hard to add, send me a pull
106             request when you add more!)
107              
108             All it takes to use OAuth authentication for a given provider, is to add
109             the configuration for it.
110              
111             The YAML below shows all available options.
112              
113             plugins:
114             "Auth::OAuth":
115             reauth_on_refresh_fail: 0 [*]
116             prefix: /auth [*]
117             success_url: / [*]
118             error_url: / [*]
119             providers:
120             Facebook:
121             tokens:
122             client_id: your_client_id
123             client_secret: your_client_secret
124             fields: id,email,name,gender,picture
125             # Original default Facebook scope was 'email,public_profile,user_friends'
126             # Now 'user_friends' require an app review
127             query_params:
128             authorize:
129             scope: email,public_profile
130             Google:
131             tokens:
132             client_id: your_client_id
133             client_secret: your_client_secret
134             AzureAD:
135             tokens:
136             client_id: your_client_id
137             client_secret: your_client_secret
138             Twitter:
139             tokens:
140             consumer_key: your_consumer_token
141             consumer_secret: your_consumer_secret
142             Github:
143             tokens:
144             client_id: your_client_id
145             client_secret: your_client_secret
146             Stackexchange:
147             tokens:
148             client_id: your_client_id
149             client_secret: your_client_secret
150             key: your_key
151             site: stackoverflow
152             Linkedin:
153             tokens:
154             client_id: your_client_id
155             client_secret: your_client_secret
156             fields: id,num-connections,picture-url,email-address
157             VKontakte: # https://vk.com
158             tokens:
159             client_id: your_client_id
160             client_secret: your_client_secret
161             fields: 'first_name,last_name,about,bdate,city,country,photo_max_orig,sex,site'
162             api_version: '5.8'
163             Odnoklassniki: # https://ok.ru
164             tokens:
165             client_id: your_client_id
166             client_secret: your_client_secret
167             application_key: your_application_key
168             method: 'users.getCurrentUser'
169             format: 'json'
170             fields: 'email,name,gender,birthday,location,uid,pic_full'
171             MailRU:
172             tokens:
173             client_id: your_client_id
174             client_private: your_client_private
175             client_secret: your_client_secret
176             method: 'users.getInfo'
177             format: 'json'
178             secure: 1
179             Yandex:
180             tokens:
181             client_id: your_client_id
182             client_secret: your_client_secret
183             format: 'json'
184              
185             [*] default value, may be omitted.
186              
187              
188             =head1 AUTHENTICATION VS. FUNCTIONAL THIRD PARTY LOGIN
189              
190             The main purpose of this module is simply to authenticate against third party
191             identify providers, but your Dancer2 app might additionally use the id_token to
192             access the API of the same (or other) third parties to enable you to do cool
193             stuff with your apps, like show a feed, access data etc.
194              
195             Because access to the third party systems would be cut off when the id_token
196             expires, Dancer2::Plugin::Auth::OAuth will automatically set up the route
197             C</auth/$provider/refresh>. Call this when the token has expired to try to
198             refresh the token without bumping the user back to log in. You can optionally
199             tell Dancer2::Plugin::Auth::OAuth to bump the user back to the login page if
200             for whatever reason the refresh fails.
201              
202             In addition, Dancer2::Plugin::Auth::OAuth will save or generate a auth session
203             key called "expires", which is (usually) number of seconds from epoch. Check
204             this to determine if the id_token has expired (see examples below).
205              
206             =head2 AUTHENTICATION
207              
208             An example of a simple single system authentication. Note that once
209             authenticated the user will continue to be authenticated until the Dancer2
210             session has expired, whenever that might be:
211              
212             hook before => sub {
213             my $session_data = session->read('oauth');
214             my $provider = "facebook"; # Lower case of the authentication plugin used
215              
216             if ((!defined $session_data || !defined $session_data->{$provider} || !defined $session_data->{$provider}{id_token}) && request->path !~ m{^/auth}) {
217             return forward "/auth/$provider";
218             }
219             };
220              
221             If you want to be sure they have a valid id_token at all times:
222              
223             hook before => sub {
224             my $session_data = session->read('oauth');
225             my $provider = "facebook"; # Lower case of the authentication plugin used
226              
227             my $now = DateTime->now->epoch;
228              
229             if ((!defined $session_data || !defined $session_data->{$provider} || !defined $session_data->{$provider}{id_token}) && request->path !~ m{^/auth}) {
230             return forward '/auth/$provider';
231              
232             } elsif (defined $session_data->{$provider}{refresh_token} && defined $session_data->{$provider}{expires} && $session_data->{$provider}{expires} < $now && request->path !~ m{^/auth}) {
233             return forward "/auth/$provider/refresh";
234              
235             }
236             };
237              
238             in the case where you're using the refresh functionality, a failure of the
239             refresh will send the user back to the error_url. If you want to them to instead
240             be directed back to the main authentication (log in page) then please set the
241             configuration option C<reauth_on_refresh_fail>.
242              
243             =head2 FUNCTIONAL THIRD PARTY LOGIN
244              
245             Authenticate using the same method as above, but be sure to use the 'refresh'
246             functionality, as the logged in user will need to have a valid id_token at all
247             times.
248              
249             Also make sure that you set the scope of your authentication to tell the third
250             party what you wish to access (and for Microsoft/Azure also set the resource,
251             for the same reason).
252              
253             Once you've got an active session you can get the id_token to use in further
254             calls to the providers backend systems with:
255              
256             my $session_data = session->read('oauth');
257             my $token = $session_data->{$provider}{id_token};
258              
259             =head1 SETTING THE SCOPE
260              
261             If you're authenticating in order to use the id_token issued, or if login
262             requires a specific 'scope' setting, you can change these values in the initial
263             calls like this within your yml config (example provided for AzureAD plugin).
264              
265             Auth::OAuth:
266             providers:
267             AzureAD:
268             query_params:
269             authorize:
270             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'
271              
272             You do not need to list all other authorize attributes sent to the server,
273             unless you want to change them from the default values set in the provider.
274             Please view the provider source/documentation for what these default values are.
275              
276             You may also need to set a value for "resource" in the same way. Refer to your
277             providers OAuth documentation.
278              
279             =head1 AUTHOR
280              
281             Menno Blom E<lt>blom@cpan.orgE<gt>
282              
283             =head1 COPYRIGHT
284              
285             Copyright 2014- Menno Blom
286              
287             =head1 LICENSE
288              
289             This library is free software; you can redistribute it and/or modify
290             it under the same terms as Perl itself.
291              
292             =cut