File Coverage

blib/lib/Yancy/Plugin/Auth/Token.pm
Criterion Covered Total %
statement 59 70 84.2
branch 17 30 56.6
condition 4 10 40.0
subroutine 11 12 91.6
pod 2 8 25.0
total 93 130 71.5


line stmt bran cond sub pod time code
1             package Yancy::Plugin::Auth::Token;
2             our $VERSION = '1.087';
3             # ABSTRACT: A simple token-based auth
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod use Mojolicious::Lite;
8             #pod plugin Yancy => {
9             #pod backend => 'sqlite://myapp.db',
10             #pod schema => {
11             #pod tokens => {
12             #pod properties => {
13             #pod id => { type => 'integer', readOnly => 1 },
14             #pod username => { type => 'string' },
15             #pod token => { type => 'string' },
16             #pod },
17             #pod },
18             #pod },
19             #pod };
20             #pod app->yancy->plugin( 'Auth::Token' => {
21             #pod schema => 'tokens',
22             #pod username_field => 'username',
23             #pod token_field => 'token',
24             #pod token_digest => {
25             #pod type => 'SHA-1',
26             #pod },
27             #pod } );
28             #pod
29             #pod =head1 DESCRIPTION
30             #pod
31             #pod B This module is C and its API may change before
32             #pod Yancy v2.000 is released.
33             #pod
34             #pod This plugin provides a basic token-based authentication scheme for
35             #pod a site. Tokens are provided in the HTTP C header:
36             #pod
37             #pod Authorization: Token
38             #pod
39             #pod =head1 CONFIGURATION
40             #pod
41             #pod This plugin has the following configuration options.
42             #pod
43             #pod =head2 schema
44             #pod
45             #pod The name of the Yancy schema that holds tokens. Required.
46             #pod
47             #pod =head2 token_field
48             #pod
49             #pod The name of the field to use for the token. Defaults to C. The
50             #pod token itself is meaningless except to authenticate a user. It must be
51             #pod unique, and it should be treated like a password.
52             #pod
53             #pod =head2 token_digest
54             #pod
55             #pod This is the hashing mechanism that should be used for creating new
56             #pod tokens via the L helper. The default type is C.
57             #pod
58             #pod This value should be a hash of digest configuration. The one required
59             #pod field is C, and should be a type supported by the L module:
60             #pod
61             #pod =over
62             #pod
63             #pod =item * MD5 (part of core Perl)
64             #pod
65             #pod =item * SHA-1 (part of core Perl)
66             #pod
67             #pod =item * SHA-256 (part of core Perl)
68             #pod
69             #pod =item * SHA-512 (part of core Perl)
70             #pod
71             #pod =back
72             #pod
73             #pod Additional fields are given as configuration to the L module.
74             #pod Not all Digest types require additional configuration.
75             #pod
76             #pod =head2 username_field
77             #pod
78             #pod The name of the field in the schema which is the user's identifier.
79             #pod This can be a user name, ID, or e-mail address, and is used to keep track
80             #pod of who owns the token.
81             #pod
82             #pod This field is optional. If not specified, no user name will be stored.
83             #pod
84             #pod =head1 HELPERS
85             #pod
86             #pod This plugin has the following helpers.
87             #pod
88             #pod =head2 yancy.auth.current_user
89             #pod
90             #pod Get the current user from the session, if any. Returns C if no
91             #pod user was found in the session.
92             #pod
93             #pod my $user = $c->yancy->auth->current_user
94             #pod || return $c->render( status => 401, text => 'Unauthorized' );
95             #pod
96             #pod =head2 yancy.auth.require_user
97             #pod
98             #pod Validate there is a logged-in user and optionally that the user data has
99             #pod certain values. See L.
100             #pod
101             #pod # Display the user dashboard, but only to logged-in users
102             #pod my $auth_route = $app->routes->under( '/user', $app->yancy->auth->require_user );
103             #pod $auth_route->get( '' )->to( 'user#dashboard' );
104             #pod
105             #pod =head2 yancy.auth.add_token
106             #pod
107             #pod $ perl myapp.pl eval 'app->yancy->auth->add_token( "username" )'
108             #pod
109             #pod Generate a new token and add it to the database. C<"username"> is the
110             #pod username for the token. The token will be generated as a base-64 encoded
111             #pod hash of the following input:
112             #pod
113             #pod =over
114             #pod
115             #pod =item * The username
116             #pod
117             #pod =item * The site's L
118             #pod
119             #pod =item * The current L
120             #pod
121             #pod =item * A random number
122             #pod
123             #pod =back
124             #pod
125             #pod =head1 SEE ALSO
126             #pod
127             #pod L
128             #pod
129             #pod =cut
130              
131 2     2   1722 use Mojo::Base 'Mojolicious::Plugin';
  2         5  
  2         16  
132 2     2   432 use Yancy::Util qw( currym derp );
  2         7  
  2         121  
133 2     2   13 use Digest;
  2         6  
  2         2945  
134              
135             has schema =>;
136             has username_field =>;
137             has token_field =>;
138             has token_digest =>;
139             has plugin_field => undef;
140             has moniker => 'token';
141              
142             sub register {
143 2     2 1 48 my ( $self, $app, $config ) = @_;
144 2         13 $self->init( $app, $config );
145 2         62 $app->helper(
146             'yancy.auth.current_user' => currym( $self, 'current_user' ),
147             );
148 2         5467 $app->helper(
149             'yancy.auth.add_token' => currym( $self, 'add_token' ),
150             );
151 2         5520 $app->helper(
152             'yancy.auth.require_user' => currym( $self, 'require_user' ),
153             );
154             }
155              
156             sub init {
157 3     3 0 12 my ( $self, $app, $config ) = @_;
158             my $schema = $config->{schema} || $config->{collection}
159 3   0     16 || die "Error configuring Auth::Token plugin: No schema defined\n";
160             derp "'collection' configuration in Auth::Token is now 'schema'. Please fix your configuration.\n"
161 3 50       13 if $config->{collection};
162 3 50       114 die sprintf(
163             q{Error configuring Auth::Token plugin: Schema "%s" not found}."\n",
164             $schema,
165             ) unless $app->yancy->schema( $schema );
166              
167 3         44 $self->schema( $schema );
168 3         41 $self->username_field( $config->{username_field} );
169             $self->token_field(
170 3   50     32 $config->{token_field} || $config->{password_field} || 'token'
171             );
172              
173 3         20 my $digest_type = delete $config->{token_digest}{type};
174 3 100       10 if ( $digest_type ) {
175 2         5 my $digest = eval {
176 2         4 Digest->new( $digest_type, %{ $config->{token_digest} } )
  2         19  
177             };
178 2 50       130 if ( my $error = $@ ) {
179 0 0       0 if ( $error =~ m{Can't locate Digest/${digest_type}\.pm in \@INC} ) {
180 0         0 die sprintf(
181             q{Error configuring Auth::Token plugin: Token digest type "%s" not found}."\n",
182             $digest_type,
183             );
184             }
185 0         0 die "Error configuring Auth::Token plugin: Error loading Digest module: $@\n";
186             }
187 2         10 $self->token_digest( $digest );
188             }
189              
190             my $route = $app->yancy->routify(
191             $config->{route},
192 3         23 '/yancy/auth/' . $self->moniker,
193             );
194 3         1249 $route->to( cb => currym( $self, 'check_token' ) );
195             }
196              
197             sub current_user {
198 8     8 0 27 my ( $self, $c ) = @_;
199 8 100       27 return undef unless my $auth = $c->req->headers->authorization;
200 4 100       143 return undef unless my ( $token ) = $auth =~ /^Token\ (\S+)$/;
201 3         16 my $schema = $self->schema;
202 3         15 my %search;
203 3         16 $search{ $self->token_field } = $token;
204 3 100       60 if ( my $field = $self->plugin_field ) {
205 1         7 $search{ $field } = $self->moniker;
206             }
207 3         27 my @users = $c->yancy->list( $schema, \%search );
208 3 50       14 if ( @users > 1 ) {
209 0         0 die "Refusing to auth: Multiple users with the same token found";
210 0         0 return undef;
211             }
212 3         32 return $users[0];
213             }
214              
215             sub check_token {
216 0     0 0 0 my ( $self, $c ) = @_;
217 0         0 my $field = $self->username_field;
218 0 0       0 if ( my $user = $self->current_user( $c ) ) {
219             return $c->render(
220 0 0       0 text => $field ? $user->{ $field } : 'Ok',
221             );
222             }
223 0         0 return $c->render(
224             status => 401,
225             text => 'Unauthorized',
226             );
227             }
228              
229             sub login_form {
230             # There is no login form for a token
231 2     2 0 8372 return undef;
232             }
233              
234             sub logout {
235             # There is no way to log out a token
236 3     3 0 58 return;
237             }
238              
239             sub add_token {
240 1     1 0 7 my ( $self, $c, $username, %user ) = @_;
241 1         7 my @parts = ( $username, $c->app->secrets->[0], $$, scalar time, int rand 1_000_000 );
242 1         20 my $token = $self->token_digest->clone->add( join "", @parts )->b64digest;
243 1         39 my $username_field = $self->username_field;
244 1 50       9 $c->yancy->create( $self->schema, {
    50          
245             ( $username_field ? ( $username_field => $username ) : () ),
246             $self->token_field => $token,
247             ( $self->plugin_field ? ( $self->plugin_field => $self->moniker ) : () ),
248             %user,
249             } );
250 1         11 return $token;
251             }
252              
253             #pod =method require_user
254             #pod
255             #pod my $subref = $c->yancy->auth->require_user( \%match );
256             #pod
257             #pod Build a callback to validate there is a logged-in user, and optionally
258             #pod that the current user has certain fields set. C<\%match> is optional and
259             #pod is a L matched
260             #pod with L.
261             #pod
262             #pod # Ensure the user is logged-in
263             #pod my $user_cb = $app->yancy->auth->require_user;
264             #pod my $user_only = $app->routes->under( $user_cb );
265             #pod
266             #pod # Ensure the user's "is_admin" field is set to 1
267             #pod my $admin_cb = $app->yancy->auth->require_user( { is_admin => 1 } );
268             #pod my $admin_only = $app->routes->under( $admin_cb );
269             #pod
270             #pod =cut
271              
272             sub require_user {
273 1     1 1 5 my ( $self, $c, $where ) = @_;
274             return sub {
275 3     3   38552 my ( $c ) = @_;
276             #; say "Are you authorized? " . $c->yancy->auth->current_user;
277 3         12 my $user = $c->yancy->auth->current_user;
278 3 100 66     72 if ( !$where && $user ) {
279 1         5 return 1;
280             }
281 2 50 33     8 if ( $where && match( $where, $user ) ) {
282 0         0 return 1;
283             }
284             $c->stash(
285 2         8 template => 'yancy/auth/unauthorized',
286             status => 401,
287             );
288 2         56 $c->respond_to(
289             json => {},
290             html => {},
291             );
292 2         12382 return undef;
293 1         7 };
294             }
295              
296             1;
297              
298             __END__