File Coverage

blib/lib/Yancy/Plugin/Auth/OAuth2.pm
Criterion Covered Total %
statement 57 66 86.3
branch 11 14 78.5
condition 8 13 61.5
subroutine 13 15 86.6
pod 5 6 83.3
total 94 114 82.4


line stmt bran cond sub pod time code
1             package Yancy::Plugin::Auth::OAuth2;
2             our $VERSION = '1.087';
3             # ABSTRACT: Authenticate using an OAuth2 provider
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod use Mojolicious::Lite;
8             #pod plugin Yancy => {
9             #pod backend => 'sqlite://myapp.db',
10             #pod };
11             #pod app->yancy->plugin( 'Auth::OAuth2' => {
12             #pod client_id => 'CLIENT_ID',
13             #pod client_secret => 'SECRET',
14             #pod authorize_url => 'https://example.com/auth',
15             #pod token_url => 'https://example.com/token',
16             #pod } );
17             #pod
18             #pod =head1 DESCRIPTION
19             #pod
20             #pod This module allows authenticating using a standard OAuth2 provider by
21             #pod implementing L.
22             #pod
23             #pod OAuth2 provides no mechanism for transmitting any information about the
24             #pod user in question, so this auth may require some customization to be
25             #pod useful. Without some kind of information about the user, it is
26             #pod impossible to know if this is a new user or a returning user or to
27             #pod maintain any kind of account information for the user.
28             #pod
29             #pod This module composes the L role
30             #pod to provide the
31             #pod L
32             #pod authorization method.
33             #pod
34             #pod =head1 CONFIGURATION
35             #pod
36             #pod This plugin has the following configuration options.
37             #pod
38             #pod =head2 client_id
39             #pod
40             #pod The client ID, provided by the OAuth2 provider.
41             #pod
42             #pod =head2 client_secret
43             #pod
44             #pod The client secret, provided by the OAuth2 provider.
45             #pod
46             #pod =head2 authorize_url
47             #pod
48             #pod The URL to start the OAuth2 authorization process.
49             #pod
50             #pod =head2 token_url
51             #pod
52             #pod The URL to get an access token. The second step of the auth process.
53             #pod
54             #pod =head2 login_label
55             #pod
56             #pod The label for the button to log in using this OAuth2 provider. Defaults
57             #pod to C.
58             #pod
59             #pod =head2 Sessions
60             #pod
61             #pod This module uses L
62             #pod sessions|https://mojolicious.org/perldoc/Mojolicious/Controller#session>
63             #pod to store the login information in a secure, signed cookie.
64             #pod
65             #pod To configure the default expiration of a session, use
66             #pod L
67             #pod default_expiration|https://mojolicious.org/perldoc/Mojolicious/Sessions#default_expiration>.
68             #pod
69             #pod use Mojolicious::Lite;
70             #pod # Expire a session after 1 day of inactivity
71             #pod app->sessions->default_expiration( 24 * 60 * 60 );
72             #pod
73             #pod =head1 HELPERS
74             #pod
75             #pod This plugin has the following helpers.
76             #pod
77             #pod =head2 yancy.auth.current_user
78             #pod
79             #pod Get the current user from the session, if any. Returns C if no
80             #pod user was found in the session.
81             #pod
82             #pod my $user = $c->yancy->auth->current_user
83             #pod || return $c->render( status => 401, text => 'Unauthorized' );
84             #pod
85             #pod =head2 yancy.auth.require_user
86             #pod
87             #pod Validate there is a logged-in user and optionally that the user data has
88             #pod certain values. See L.
89             #pod
90             #pod # Display the user dashboard, but only to logged-in users
91             #pod my $auth_route = $app->routes->under( '/user', $app->yancy->auth->require_user );
92             #pod $auth_route->get( '' )->to( 'user#dashboard' );
93             #pod
94             #pod =head2 yancy.auth.login_form
95             #pod
96             #pod Returns the rendered login button.
97             #pod
98             #pod Login with OAuth2:
99             #pod %= $c->yancy->auth->login_form
100             #pod
101             #pod =head2 yancy.auth.logout
102             #pod
103             #pod Log out any current account from any auth plugin. Use this in your own
104             #pod route handlers to perform a logout.
105             #pod
106             #pod =head1 TEMPLATES
107             #pod
108             #pod To override these templates, add your own at the designated path inside
109             #pod your app's C directory.
110             #pod
111             #pod =head2 yancy/auth/oauth2/login_form.html.ep
112             #pod
113             #pod Display the button to log in using this OAuth2 provider.
114             #pod
115             #pod =head2 layouts/yancy/auth.html.ep
116             #pod
117             #pod The layout that Yancy uses when displaying the login form, the
118             #pod unauthorized error message, and other auth-related pages.
119             #pod
120             #pod =head1 SEE ALSO
121             #pod
122             #pod L
123             #pod
124             #pod =cut
125              
126 3     3   2357 use Mojo::Base 'Mojolicious::Plugin';
  3         6  
  3         23  
127 3     3   2300 use Role::Tiny::With;
  3         567  
  3         185  
128             with 'Yancy::Plugin::Auth::Role::RequireUser';
129 3     3   52 use Yancy::Util qw( currym match );
  3         10  
  3         188  
130 3     3   22 use Mojo::UserAgent;
  3         6  
  3         23  
131 3     3   93 use Mojo::URL;
  3         6  
  3         22  
132              
133             has ua => sub { Mojo::UserAgent->new };
134             has route =>;
135             has moniker => 'oauth2';
136             has client_id =>;
137             has client_secret =>;
138             has authorize_url =>;
139             has token_url =>;
140             has login_label => 'Login';
141             has logout_route =>;
142              
143             sub register {
144             my ( $self, $app, $config ) = @_;
145             $self->init( $app, $config );
146             $app->helper(
147             'yancy.auth.current_user' => currym( $self, 'current_user' ),
148             );
149             $app->helper(
150             'yancy.auth.logout' => currym( $self, 'logout' ),
151             );
152             $app->helper(
153             'yancy.auth.login_form' => currym( $self, 'login_form' ),
154             );
155             }
156              
157             sub init {
158 4     4 0 13 my ( $self, $app, $config ) = @_;
159 4         16 for my $attr ( qw( moniker ua client_id client_secret login_label ) ) {
160 20 100       112 next if !$config->{ $attr };
161 10         46 $self->$attr( $config->{ $attr } );
162             }
163 4         14 for my $url_attr ( qw( authorize_url token_url ) ) {
164 8 100       232 next if !$config->{ $url_attr };
165 6         36 $self->$url_attr( Mojo::URL->new( $config->{ $url_attr } ) );
166             }
167             $self->route(
168             $app->yancy->routify(
169             $config->{route},
170 4         204 '/yancy/auth/' . $self->moniker,
171             )
172             );
173 4         1805 $self->route->get( '' )->to( cb => currym( $self, '_handle_auth' ) );
174 4         118 $self->logout_route(
175             $self->route->get( '/logout' )->to( cb => currym( $self, '_handle_logout' ) )
176             ->name( 'yancy.auth.' . $self->moniker . '.logout' )
177             );
178             }
179              
180             #pod =method current_user
181             #pod
182             #pod Returns the access token of the currently-logged-in user.
183             #pod
184             #pod =cut
185              
186             sub current_user {
187 0     0 1 0 my ( $self, $c ) = @_;
188 0   0     0 return $c->session->{yancy}{ $self->moniker }{access_token} || undef;
189             }
190              
191             #pod =method logout
192             #pod
193             #pod Clear any currently-logged-in user.
194             #pod
195             #pod =cut
196              
197             sub logout {
198 9     9 1 52 my ( $self, $c ) = @_;
199 9         33 delete $c->session->{yancy}{ $self->moniker };
200 9         4793 return;
201             }
202              
203             #pod =method login_form
204             #pod
205             #pod Get a link to log in using this OAuth2 provider.
206             #pod
207             #pod =cut
208              
209             sub login_form {
210 1     1 1 3 my ( $self, $c ) = @_;
211 1         4 return $c->render_to_string(
212             'yancy/auth/oauth2/login_form',
213             label => $self->login_label,
214             url => $self->route->render,
215             );
216             }
217              
218             sub _handle_auth {
219 10     10   37 my ( $self, $c ) = @_;
220              
221             # This sub handles both steps of authentication. If we have a code,
222             # we can get a token.
223 10 100       39 if ( my $code = $c->param( 'code' ) ) {
224 4         506 my %client_info = (
225             client_id => $self->client_id,
226             client_secret => $self->client_secret,
227             code => $code,
228             );
229             $self->ua->post_p( $self->token_url, form => \%client_info )
230             ->then( sub {
231 4     4   35922 my ( $tx ) = @_;
232 4         19 my $token = $tx->res->body_params->param( 'access_token' );
233 4         730 $c->session->{yancy}{ $self->moniker }{ access_token } = $token;
234             $self->handle_token_p( $c, $token )
235             ->then( sub {
236 4         2618 my $return_to = $c->session->{ yancy }{ $self->moniker }{ return_to };
237 4         98 $c->redirect_to( $return_to );
238             } )
239             ->catch( sub {
240 0         0 my ( $err ) = @_;
241 0         0 $c->render( text => $err );
242 4         708 } );
243             } )
244             ->catch( sub {
245 0     0   0 my ( $err ) = @_;
246 0         0 $c->render(
247             text => $err,
248             );
249 4         59 } );
250 4         10090 return $c->render_later;
251             }
252              
253             # If we do not have a code, we need to get one
254 6         1246 my $to = $c->param( 'return_to' );
255              
256             # Do not allow return_to to redirect the user to another site.
257             # http://cwe.mitre.org/data/definitions/601.html
258 6 100 66     476 if ( $to && $to =~ m{^(?:\w+:|//)} ) {
    50 33        
    50          
259 2         16 return $c->reply->exception(
260             q{`return_to` can not contain URL scheme or host},
261             );
262             }
263             elsif ( !$to && $c->req->headers->referrer !~ m{^(?:\w+:|//)} ) {
264 0         0 $to = $c->req->headers->referrer;
265             }
266             elsif ( !$to ) {
267 0         0 $to = '/';
268             }
269              
270 4         22 $c->session->{yancy}{ $self->moniker }{ return_to } = $to;
271 4         1479 $c->redirect_to( $self->get_authorize_url( $c ) );
272             }
273              
274             #pod =method get_authorize_url
275             #pod
276             #pod my $url = $self->get_authorize_url( $c );
277             #pod
278             #pod Get a full authorization URL with query parameters. Override this in
279             #pod a subclass to customize the authorization parameters.
280             #pod
281             #pod =cut
282              
283             sub get_authorize_url {
284 1     1 1 4 my ( $self, $c ) = @_;
285 1         4 my %client_info = (
286             client_id => $self->client_id,
287             );
288 1         9 return $self->authorize_url->clone->query( \%client_info );
289             }
290              
291             #pod =method handle_token_p
292             #pod
293             #pod my $p = $self->handle_token_p( $c, $token );
294             #pod
295             #pod Handle the receipt of the token. Override this in a subclass to make any
296             #pod API requests to identify the user. Returns a L that will
297             #pod be fulfilled when the information is complete.
298             #pod
299             #pod =cut
300              
301             sub handle_token_p {
302 1     1 1 4 my ( $self, $c, $token ) = @_;
303 1         7 return Mojo::Promise->new->resolve;
304             }
305              
306             sub _handle_logout {
307 6     6   17 my ( $self, $c ) = @_;
308 6         30 $self->logout( $c );
309 6         22 $c->res->code( 303 );
310 6   100     109 my $redirect_to = $c->param( 'redirect_to' ) // $c->req->headers->referrer // '/';
      100        
311 6 50       1526 if ( $redirect_to eq $c->req->url->path ) {
312 0         0 $redirect_to = '/';
313             }
314 6         1226 return $c->redirect_to( $redirect_to );
315             }
316              
317             1;
318              
319             __END__