File Coverage

blib/lib/Catalyst/Authentication/Credential/OAuth2.pm
Criterion Covered Total %
statement 58 68 85.2
branch 12 26 46.1
condition n/a
subroutine 11 11 100.0
pod 1 4 25.0
total 82 109 75.2


line stmt bran cond sub pod time code
1             package Catalyst::Authentication::Credential::OAuth2;
2 2     2   744282 use Moose;
  2         8  
  2         19  
3 2     2   20147 use MooseX::Types::Common::String qw(NonEmptySimpleStr);
  2         249766  
  2         22  
4 2     2   9087 use LWP::UserAgent;
  2         6  
  2         68  
5 2     2   889 use HTTP::Request::Common;
  2         2786  
  2         214  
6 2     2   21 use JSON::Any;
  2         7  
  2         25  
7 2     2   379 use Moose::Util;
  2         7  
  2         25  
8              
9             # ABSTRACT: Authenticate against OAuth2 servers
10              
11              
12             has [qw(grant_uri token_uri client_id)] => (
13             is => 'ro',
14             isa => NonEmptySimpleStr,
15             required => 1,
16             );
17              
18             has token_uri_method => (is=>'ro', required=>1, default=>'GET');
19             has token_uri_post_content_type => (is=>'ro', required=>1, default=>'application/x-www-form-urlencoded');
20             has extra_find_user_token_fields => (is=>'ro', required=>0, predicate=>'has_extra_find_user_token_fields');
21             has scope => (is=>'ro', required=>0, predicate=>'has_scope');
22              
23             has client_secret => (
24             is => 'ro',
25             isa => NonEmptySimpleStr,
26             required => 0,
27             predicate => 'has_client_secret'
28             );
29              
30             has ua => ( is => 'ro', default => sub { LWP::UserAgent->new } );
31              
32             sub BUILDARGS {
33 4     4 1 179121 my ( $class, $config, $app, $realm ) = @_;
34 4         32 Moose::Util::ensure_all_roles( $realm, 'CatalystX::OAuth2::ClientInjector' );
35 4         32103 Moose::Util::ensure_all_roles( $realm->store, 'CatalystX::OAuth2::ClientPersistor');
36 4         19090 return $config;
37             }
38              
39             sub authenticate {
40 4     4 0 130992 my ( $self, $ctx, $realm, $auth_info ) = @_;
41 4         27 my $callback_uri = $self->_build_callback_uri($ctx);
42              
43 4 100       99 unless ( defined( my $code = $ctx->request->params->{code} ) ) {
44 2         191 my $auth_url = $self->extend_permissions( $callback_uri, $auth_info );
45 2         111 $ctx->response->redirect($auth_url);
46              
47 2         1050 return;
48             } else {
49 2         129 my $token =
50             $self->request_access_token( $callback_uri, $code, $auth_info );
51 2 50       9 die 'Error validating verification code' unless $token;
52              
53 2         11 my %find_user_fields = (token => $token->{access_token});
54 2 50       90 if($self->has_extra_find_user_token_fields) {
55 0         0 $find_user_fields{$_} = $token->{$_} for @{$self->extra_find_user_token_fields};
  0         0  
56             }
57 2         15 return $realm->find_user( \%find_user_fields, $ctx );
58             }
59             }
60              
61             sub _build_callback_uri {
62 5     5   1365 my ( $self, $ctx ) = @_;
63 5         144 my $uri = $ctx->request->uri->clone;
64 5         269 $uri->query(undef);
65 5         111 return $uri;
66             }
67              
68             sub extend_permissions {
69 3     3 0 540 my ( $self, $callback_uri, $auth_info ) = @_;
70 3         126 my $uri = URI->new( $self->grant_uri );
71 3         452 my $query = {
72             response_type => 'code',
73             client_id => $self->client_id,
74             redirect_uri => $callback_uri,
75             };
76 3 50       18 $query->{state} = $auth_info->{state} if exists $auth_info->{state};
77 3 50       126 $query->{scope} = $self->scope if $self->has_scope;
78 3 50       16 $query->{scope} = $auth_info->{scope} if exists $auth_info->{scope};
79              
80 3         68 $uri->query_form($query);
81 3         652 return $uri;
82             }
83              
84             my $j = JSON::Any->new;
85              
86             sub request_access_token {
87 2     2 0 8 my ( $self, $callback_uri, $code, $auth_info ) = @_;
88 2         64 my $uri = URI->new( $self->token_uri );
89 2         299 my @data = (
90             client_id => $self->client_id,
91             redirect_uri => "$callback_uri", #stringify for JSON
92             code => $code,
93             grant_type => 'authorization_code');
94 2 50       25 push(@data, (state=>$auth_info->{state})) if exists $auth_info->{state};
95 2 100       76 push(@data, (client_secret=>$self->client_secret)) if $self->has_client_secret;
96              
97 2         6 my $req;
98 2 50       62 if($self->token_uri_method eq 'GET') {
    0          
99 2         19 $uri->query_form(+{@data});
100 2         635 $req = GET $uri;
101             } elsif($self->token_uri_method eq 'POST') {
102 0 0       0 if($self->token_uri_post_content_type eq 'application/json') {
    0          
103 0         0 $req = POST $uri, 'Content_Type' => 'application/json', Content => $j->to_json(+{@data});
104             } elsif($self->token_uri_post_content_type eq 'application/x-www-form-urlencoded') {
105 0         0 $req = POST $uri, 'Content_Type' => 'application/x-www-form-urlencoded', Content => \@data;
106             } else {
107 0         0 die "Unrecognized 'token_uri_post_content_type' of '${\$self->token_uri_post_content_type}'";
  0         0  
108             }
109             } else {
110 0         0 die "Unrecognized 'token_uri_method' of '${\$self->token_uri_method}'";
  0         0  
111             }
112              
113 2         274 my $response = $self->ua->request($req);
114 2 50       5797 if($response->is_success) {
115 2         114 my $data = $j->jsonToObj( $response->decoded_content ); # Eval wrap
116 2         265 return $data;
117             } else {
118 0           return;
119             }
120             }
121              
122             1;
123              
124             __END__
125              
126             =pod
127              
128             =head1 NAME
129              
130             Catalyst::Authentication::Credential::OAuth2 - Authenticate against OAuth2 servers
131              
132             =head1 VERSION
133              
134             version 0.001007
135              
136             =head1 SYNOPSIS
137              
138             __PACKAGE__->config(
139             'Plugin::Authentication' => {
140             default => {
141             credential => {
142             class => 'OAuth2',
143             grant_uri => 'http://authserver/request',
144             token_uri => 'http://authserver/token',
145             client_id => 'dead69beef'
146             },
147             store => { class => 'Null' }
148             }
149             }
150             );
151              
152             =head1 DESCRIPTION
153              
154             This module implements authentication via OAuth2 credentials, giving you a
155             user object which stores tokens for accessing protected resources.
156              
157             =head1 ATTRIBUTES
158              
159             =head2 grant_uri
160              
161             =head2 token_uri
162              
163             =head2 client_id
164              
165             Required attributes that you get from your Oauth2 provider
166              
167             =head2 client_secret
168              
169             optional secret code from your Oauth2 provider (you need to review the docs from
170             your provider).
171              
172             =head2 scope
173              
174             Value of 'scope' field submitted to the grant_uri
175              
176             =head2 token_uri_method
177              
178             Default is GET; some providers require POST
179              
180             =head2 token_uri_post_content_type
181              
182             Default is 'application/x-www-form-urlencoded', some providers support 'application/json'.
183              
184             =head2 has_extra_find_user_token_fields
185              
186             By default we call ->find_user on the store with a hashref that contains key 'token' and the
187             value of the access_token (which we get from calling the 'token_uri'). The results of calling
188             the token_uri is usually a JSON named array structure which can contain other fields such as
189             id_token (typically a JWT). You can set this to an arrayref of extra fields you want to pass.
190              
191             =head1 AUTHOR
192              
193             Eden Cardim <edencardim@gmail.com>
194              
195             =head1 COPYRIGHT AND LICENSE
196              
197             This software is copyright (c) 2017 by Suretec Systems Ltd.
198              
199             This is free software; you can redistribute it and/or modify it under
200             the same terms as the Perl 5 programming language system itself.
201              
202             =cut