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   681358 use Moose;
  2         5  
  2         20  
3 2     2   14145 use MooseX::Types::Common::String qw(NonEmptySimpleStr);
  2         220410  
  2         19  
4 2     2   5813 use LWP::UserAgent;
  2         6  
  2         59  
5 2     2   580 use HTTP::Request::Common;
  2         1607  
  2         165  
6 2     2   20 use JSON::Any;
  2         7  
  2         31  
7 2     2   376 use Moose::Util;
  2         6  
  2         24  
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 174292 my ( $class, $config, $app, $realm ) = @_;
34 4         37 Moose::Util::ensure_all_roles( $realm, 'CatalystX::OAuth2::ClientInjector' );
35 4         31222 Moose::Util::ensure_all_roles( $realm->store, 'CatalystX::OAuth2::ClientPersistor');
36 4         21355 return $config;
37             }
38              
39             sub authenticate {
40 4     4 0 116665 my ( $self, $ctx, $realm, $auth_info ) = @_;
41 4         32 my $callback_uri = $self->_build_callback_uri($ctx);
42              
43 4 100       104 unless ( defined( my $code = $ctx->request->params->{code} ) ) {
44 2         174 my $auth_url = $self->extend_permissions( $callback_uri, $auth_info );
45 2         92 $ctx->response->redirect($auth_url);
46              
47 2         1082 return;
48             } else {
49 2         223 my $token =
50             $self->request_access_token( $callback_uri, $code, $auth_info );
51 2 50       12 die 'Error validating verification code' unless $token;
52              
53 2         11 my %find_user_fields = (token => $token->{access_token});
54 2 50       96 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         18 return $realm->find_user( \%find_user_fields, $ctx );
58             }
59             }
60              
61             sub _build_callback_uri {
62 5     5   1738 my ( $self, $ctx ) = @_;
63 5         162 my $uri = $ctx->request->uri->clone;
64 5         324 $uri->query(undef);
65 5         124 return $uri;
66             }
67              
68             sub extend_permissions {
69 3     3 0 575 my ( $self, $callback_uri, $auth_info ) = @_;
70 3         135 my $uri = URI->new( $self->grant_uri );
71 3         428 my $query = {
72             response_type => 'code',
73             client_id => $self->client_id,
74             redirect_uri => $callback_uri,
75             };
76 3 50       16 $query->{state} = $auth_info->{state} if exists $auth_info->{state};
77 3 50       137 $query->{scope} = $self->scope if $self->has_scope;
78 3 50       15 $query->{scope} = $auth_info->{scope} if exists $auth_info->{scope};
79              
80 3         69 $uri->query_form($query);
81 3         661 return $uri;
82             }
83              
84             my $j = JSON::Any->new;
85              
86             sub request_access_token {
87 2     2 0 10 my ( $self, $callback_uri, $code, $auth_info ) = @_;
88 2         88 my $uri = URI->new( $self->token_uri );
89 2         377 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       52 push(@data, (state=>$auth_info->{state})) if exists $auth_info->{state};
95 2 100       109 push(@data, (client_secret=>$self->client_secret)) if $self->has_client_secret;
96              
97 2         8 my $req;
98 2 50       95 if($self->token_uri_method eq 'GET') {
    0          
99 2         31 $uri->query_form(+{@data});
100 2         607 $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         418 my $response = $self->ua->request($req);
114 2 50       7098 if($response->is_success) {
115 2         80 my $data = $j->jsonToObj( $response->decoded_content ); # Eval wrap
116 2         305 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.001006
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