File Coverage

blib/lib/Yancy/Plugin/Auth/Github.pm
Criterion Covered Total %
statement 58 68 85.2
branch 14 20 70.0
condition 3 7 42.8
subroutine 13 13 100.0
pod 4 5 80.0
total 92 113 81.4


line stmt bran cond sub pod time code
1             package Yancy::Plugin::Auth::Github;
2             our $VERSION = '1.088';
3             # ABSTRACT: Authenticate using Github's 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::Github' => {
12             #pod client_id => 'CLIENT_ID',
13             #pod client_secret => $ENV{ OAUTH_GITHUB_SECRET },
14             #pod schema => 'users',
15             #pod username_field => 'username',
16             #pod # TODO: Get other user information from Github, requesting
17             #pod # scopes if necessary
18             #pod } );
19             #pod
20             #pod =head1 DESCRIPTION
21             #pod
22             #pod This module allows authenticating using the Github OAuth2 API.
23             #pod
24             #pod This module extends the L module to add
25             #pod Github features.
26             #pod
27             #pod This module composes the L role
28             #pod to provide the
29             #pod L
30             #pod authorization method.
31             #pod
32             #pod =head1 CONFIGURATION
33             #pod
34             #pod This plugin has the following configuration options.
35             #pod
36             #pod =head2 client_id
37             #pod
38             #pod The client ID, provided by Github.
39             #pod
40             #pod =head2 client_secret
41             #pod
42             #pod The client secret, provided by Github.
43             #pod
44             #pod =head2 login_label
45             #pod
46             #pod The label for the button to log in using Github. Defaults
47             #pod to C.
48             #pod
49             #pod =head2 Sessions
50             #pod
51             #pod This module uses L
52             #pod sessions|https://mojolicious.org/perldoc/Mojolicious/Controller#session>
53             #pod to store the login information in a secure, signed cookie.
54             #pod
55             #pod To configure the default expiration of a session, use
56             #pod L
57             #pod default_expiration|https://mojolicious.org/perldoc/Mojolicious/Sessions#default_expiration>.
58             #pod
59             #pod use Mojolicious::Lite;
60             #pod # Expire a session after 1 day of inactivity
61             #pod app->sessions->default_expiration( 24 * 60 * 60 );
62             #pod
63             #pod =head1 HELPERS
64             #pod
65             #pod This plugin inherits all helpers from L.
66             #pod
67             #pod =head1 TEMPLATES
68             #pod
69             #pod To override these templates, add your own at the designated path inside
70             #pod your app's C directory.
71             #pod
72             #pod =head2 yancy/auth/github/login_form.html.ep
73             #pod
74             #pod Display the button to log in using Github.
75             #pod
76             #pod =head2 layouts/yancy/auth.html.ep
77             #pod
78             #pod The layout that Yancy uses when displaying the login form, the
79             #pod unauthorized error message, and other auth-related pages.
80             #pod
81             #pod =head1 SEE ALSO
82             #pod
83             #pod L, L
84             #pod
85             #pod =cut
86              
87 2     2   10746 use Mojo::Base 'Yancy::Plugin::Auth::OAuth2';
  2         5  
  2         11  
88 2     2   130 use Yancy::Util qw( currym match derp );
  2         6  
  2         98  
89 2     2   14 use Mojo::UserAgent;
  2         7  
  2         9  
90 2     2   66 use Mojo::URL;
  2         10  
  2         9  
91              
92             has moniker => 'github';
93             has authorize_url => sub { Mojo::URL->new( 'https://github.com/login/oauth/authorize' ) };
94             has token_url => sub { Mojo::URL->new( 'https://github.com/login/oauth/access_token' ) };
95             has api_url => sub { Mojo::URL->new( 'https://api.github.com/' ) };
96             has schema =>;
97             has username_field => 'username';
98             has plugin_field =>;
99             has allow_register => 0;
100             has login_label => 'Login with Github';
101              
102             sub init {
103 3     3 0 12 my ( $self, $app, $config ) = @_;
104 3 50       25 if ( $config->{collection} ) {
105 0         0 $self->schema( $config->{collection} );
106 0         0 derp "'collection' configuration in Auth::Github is now 'schema'. Please fix your configuration.\n";
107             }
108 3         16 for my $attr ( qw( schema username_field plugin_field allow_register ) ) {
109 12 100       68 next if !$config->{ $attr };
110 8         37 $self->$attr( $config->{ $attr } );
111             }
112 3         13 for my $url_attr ( qw( api_url ) ) {
113 3 100       14 next if !$config->{ $url_attr };
114 2         14 $self->$url_attr( Mojo::URL->new( $config->{ $url_attr } ) );
115             }
116 3         171 return $self->SUPER::init( $app, $config );
117             }
118              
119             #pod =method current_user
120             #pod
121             #pod Returns the user row of the currently-logged-in user.
122             #pod
123             #pod =cut
124              
125             sub current_user {
126 5     5 1 21 my ( $self, $c ) = @_;
127 5   100     18 my $username = $c->session->{yancy}{ $self->moniker }{ github_login } || return undef;
128 2         1387 return $self->_get_user( $c, $username );
129             }
130              
131             #pod =method login_form
132             #pod
133             #pod Get a link to log in using Github.
134             #pod
135             #pod =cut
136              
137             sub login_form {
138 3     3 1 22148 my ( $self, $c ) = @_;
139 3         16 return $c->render_to_string(
140             'yancy/auth/github/login_form',
141             label => $self->login_label,
142             url => $self->route->render,
143             );
144             }
145              
146             sub _get_user {
147 5     5   13 my ( $self, $c, $username ) = @_;
148 5         19 my $schema_name = $self->schema;
149 5         40 my $schema = $c->yancy->schema( $schema_name );
150 5         32 my $username_field = $self->username_field;
151 5         35 my %search;
152 5 50       23 if ( my $field = $self->plugin_field ) {
153 0         0 $search{ $field } = $self->moniker;
154             }
155 5 50 33     48 if ( $username_field && $username_field ne $schema->{'x-id-field'} ) {
156 0         0 $search{ $username_field } = $username;
157 0         0 my ( $user ) = @{ $c->yancy->backend->list( $schema_name, \%search, { limit => 1 } )->{items} };
  0         0  
158 0         0 return $user;
159             }
160 5         21 return $c->yancy->backend->get( $schema_name, $username );
161             }
162              
163             sub _handle_auth {
164 6     6   18 my ( $self, $c ) = @_;
165              
166             # Verify the CSRF from Github
167 6 100       25 if ( my $code = $c->param( 'code' ) ) {
168 3 50       1107 if ( $c->param( 'state' ) ne $c->csrf_token ) {
169 0         0 $c->render( status => 400, text => 'CSRF token failure' );
170             }
171             }
172              
173 6         3342 return $self->SUPER::_handle_auth( $c );
174             }
175              
176             sub get_authorize_url {
177 3     3 1 12 my ( $self, $c ) = @_;
178 3         12 my %client_info = (
179             client_id => $self->client_id,
180             state => $c->csrf_token,
181             );
182 3         309 return $self->authorize_url->clone->query( \%client_info );
183             }
184              
185             sub handle_token_p {
186 3     3 1 10 my ( $self, $c, $token ) = @_;
187 3         13 my $api_url = $self->api_url->clone;
188 3         142 $api_url->path->trailing_slash( '1' )->merge( 'user' );
189             return $self->ua->get_p(
190             $api_url,
191             { Authorization => join( ' ', 'token', $token ) },
192             )
193             ->then( sub {
194 3     3   22086 my ( $tx ) = @_;
195 3         14 my $login = $tx->res->json( '/login' );
196 3 50       617 if ( !$login ) {
197 0         0 $c->render( text => 'Error getting Github login: ' . $tx->res->body );
198             }
199 3         15 $c->session->{yancy}{ $self->moniker }{ github_login } = $login;
200 3 50       87 if ( !$self->_get_user( $c, $login ) ) {
201 3 100       15 if ( !$self->allow_register ) {
202 1         10 $c->app->log->error( 'Registration not allowed (set allow_register)' );
203 1         20 $c->stash( status => 403 );
204 1         25 die 'Registration of new users is not allowed';
205 0         0 return;
206             }
207 2         20 my $schema = $c->yancy->schema( $self->schema );
208             $c->yancy->create(
209             $self->schema,
210 2   0     11 { $self->username_field || $schema->{'x-id-field'} || 'id' => $login },
211             );
212             }
213             } )
214             ->catch( sub {
215 1     1   142 my ( $err ) = @_;
216 1         6 $c->render( text => $err );
217 3         507 } );
218             }
219              
220             1;
221              
222             __END__