line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Authen::CAS::UserAgent; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
=head1 NAME |
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
Authen::CAS::UserAgent - CAS-aware LWP::UserAgent |
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
=head1 SYNOPSIS |
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
use Authen::CAS::UserAgent; |
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
my $ua = Authen::CAS::UserAgent->new( |
12
|
|
|
|
|
|
|
'cas_opts' => { |
13
|
|
|
|
|
|
|
'server' => 'https://cas.example.com/cas/', |
14
|
|
|
|
|
|
|
'username' => 'user', |
15
|
|
|
|
|
|
|
'password' => 'password', |
16
|
|
|
|
|
|
|
'restful' => 1, |
17
|
|
|
|
|
|
|
}, |
18
|
|
|
|
|
|
|
); |
19
|
|
|
|
|
|
|
$ua->get('https://www.example.com/casProtectedResource'); |
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
=head1 DESCRIPTION |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
This module attempts to add transparent CAS authentication support to |
24
|
|
|
|
|
|
|
LWP::UserAgent. It currently supports using proxy granting tickets, the RESTful |
25
|
|
|
|
|
|
|
API, screen scraping the login screen, or a custom login callback when CAS |
26
|
|
|
|
|
|
|
authentication is required. |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
=cut |
29
|
|
|
|
|
|
|
|
30
|
2
|
|
|
2
|
|
77054
|
use strict; |
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
81
|
|
31
|
2
|
|
|
2
|
|
2332
|
use utf8; |
|
2
|
|
|
|
|
22
|
|
|
2
|
|
|
|
|
11
|
|
32
|
2
|
|
|
2
|
|
90
|
use base qw{LWP::UserAgent Exporter}; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
2716
|
|
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
our $VERSION = '0.91'; |
35
|
|
|
|
|
|
|
|
36
|
2
|
|
|
2
|
|
181049
|
use constant CASHANDLERNAME => __PACKAGE__ . '.Handler'; |
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
709
|
|
37
|
2
|
|
|
2
|
|
23
|
use constant XMLNS_CAS => 'http://www.yale.edu/tp/cas'; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
89
|
|
38
|
|
|
|
|
|
|
|
39
|
2
|
|
|
2
|
|
10
|
use constant ERROR_PROXY_INVALIDRESPONSE => 1; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
78
|
|
40
|
2
|
|
|
2
|
|
9
|
use constant ERROR_PROXY_INVALIDTICKET => 2; |
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
76
|
|
41
|
2
|
|
|
2
|
|
11
|
use constant ERROR_PROXY_UNKNOWN => 3; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
185
|
|
42
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
our @EXPORT_OK = qw{ |
44
|
|
|
|
|
|
|
ERROR_PROXY_INVALIDRESPONSE |
45
|
|
|
|
|
|
|
ERROR_PROXY_INVALIDTICKET |
46
|
|
|
|
|
|
|
ERROR_PROXY_UNKNOWN |
47
|
|
|
|
|
|
|
}; |
48
|
|
|
|
|
|
|
our %EXPORT_TAGS = ( |
49
|
|
|
|
|
|
|
ERRORS => [qw{ |
50
|
|
|
|
|
|
|
ERROR_PROXY_INVALIDRESPONSE |
51
|
|
|
|
|
|
|
ERROR_PROXY_INVALIDTICKET |
52
|
|
|
|
|
|
|
ERROR_PROXY_UNKNOWN |
53
|
|
|
|
|
|
|
}], |
54
|
|
|
|
|
|
|
); |
55
|
|
|
|
|
|
|
|
56
|
2
|
|
|
2
|
|
10
|
use HTTP::Request; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
51
|
|
57
|
2
|
|
|
2
|
|
6697
|
use HTTP::Request::Common (); |
|
2
|
|
|
|
|
5757
|
|
|
2
|
|
|
|
|
53
|
|
58
|
2
|
|
|
2
|
|
14
|
use HTTP::Status (); |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
36
|
|
59
|
2
|
|
|
2
|
|
14
|
use URI; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
69
|
|
60
|
2
|
|
|
2
|
|
13
|
use URI::Escape qw{uri_escape}; |
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
162
|
|
61
|
2
|
|
|
2
|
|
2573
|
use URI::QueryParam; |
|
2
|
|
|
|
|
2080
|
|
|
2
|
|
|
|
|
97
|
|
62
|
2
|
|
|
2
|
|
2835
|
use XML::LibXML; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
use XML::LibXML::XPathContext; |
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
##LWP handlers |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
#cas login handler, detects a redirect to the cas login page, logs the user in and updates the initial redirect |
68
|
|
|
|
|
|
|
my $casLoginHandler = sub { |
69
|
|
|
|
|
|
|
my ($response, $ua, $h) = @_; |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
#prevent potential recursion caused by attempting to log the user in |
72
|
|
|
|
|
|
|
return if($h->{'running'} > 0); |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
#check to see if this is a redirection to the login page |
75
|
|
|
|
|
|
|
my $uri = URI->new_abs($response->header('Location'), $response->request->uri)->canonical; |
76
|
|
|
|
|
|
|
my $loginUri = URI->new_abs('login', $h->{'casServer'})->canonical; |
77
|
|
|
|
|
|
|
if( |
78
|
|
|
|
|
|
|
$uri->scheme eq $loginUri->scheme && |
79
|
|
|
|
|
|
|
$uri->authority eq $loginUri->authority && |
80
|
|
|
|
|
|
|
$uri->path eq $loginUri->path |
81
|
|
|
|
|
|
|
) { |
82
|
|
|
|
|
|
|
#short-circuit if a service isn't specified |
83
|
|
|
|
|
|
|
my $service = URI->new(scalar $uri->query_param('service')); |
84
|
|
|
|
|
|
|
return if($service eq ''); |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
#short-circuit if in strict mode and the service is different than the original uri |
87
|
|
|
|
|
|
|
return if($h->{'strict'} && $response->request->uri ne $service); |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
#get a ticket for the specified service |
90
|
|
|
|
|
|
|
my $ticket = $ua->get_cas_ticket($service, $h); |
91
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
#short-circuit if a ticket wasn't found |
93
|
|
|
|
|
|
|
return if(!defined $ticket); |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
#update the Location header |
96
|
|
|
|
|
|
|
$response->header('Location', $service . ($service =~ /\?/ ? '&' : '?') . 'ticket=' . uri_escape($ticket)); |
97
|
|
|
|
|
|
|
|
98
|
|
|
|
|
|
|
#attach a local response_redirect handler that will issue the redirect if necessary |
99
|
|
|
|
|
|
|
push(@{$response->{'handlers'}->{'response_redirect'}}, |
100
|
|
|
|
|
|
|
{ |
101
|
|
|
|
|
|
|
%$h, |
102
|
|
|
|
|
|
|
'callback' => sub { |
103
|
|
|
|
|
|
|
my ($response, $ua, $h) = @_; |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
#delete this response_redirect handler from the response object |
106
|
|
|
|
|
|
|
delete $response->{'handlers'}->{'response_redirect'}; |
107
|
|
|
|
|
|
|
delete $response->{'handlers'} unless(%{$response->{'handlers'}}); |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
#determine the new uri |
110
|
|
|
|
|
|
|
my $uri = $response->request->uri; |
111
|
|
|
|
|
|
|
my $newUri = URI->new_abs(scalar $response->header('Location'), $uri); |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
#check to see if the target uri is the same as the original uri (ignoring the ticket) |
114
|
|
|
|
|
|
|
my $targetUri = $newUri->clone; |
115
|
|
|
|
|
|
|
if($targetUri =~ s/[\&\?]ticket=[^\&\?]*$//sog) { |
116
|
|
|
|
|
|
|
if($targetUri eq $uri) { |
117
|
|
|
|
|
|
|
#clone the original request, update the request uri, and return the new request |
118
|
|
|
|
|
|
|
my $request = $response->request->clone; |
119
|
|
|
|
|
|
|
$request->uri($newUri); |
120
|
|
|
|
|
|
|
return $request |
121
|
|
|
|
|
|
|
} |
122
|
|
|
|
|
|
|
} |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
return; |
125
|
|
|
|
|
|
|
}, |
126
|
|
|
|
|
|
|
}, |
127
|
|
|
|
|
|
|
); |
128
|
|
|
|
|
|
|
} |
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
return; |
131
|
|
|
|
|
|
|
}; |
132
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
# default heuristic for finding login parameters |
134
|
|
|
|
|
|
|
my $defaultLoginParamsHeuristic = sub { |
135
|
|
|
|
|
|
|
my ($service, $response, $ua, $h, @params) = @_; |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
# find all input controls on the submit form |
138
|
|
|
|
|
|
|
my $content = $response->decoded_content; |
139
|
|
|
|
|
|
|
while($content =~ /(\)/igs) { |
140
|
|
|
|
|
|
|
my $input = $1; |
141
|
|
|
|
|
|
|
my $name = $input =~ /name=\"(.*?)\"/si ? $1 : undef; |
142
|
|
|
|
|
|
|
my $value = $input =~ /value=\"(.*?)\"/si ? $1 : undef; |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
# we only care about the lt, execution, and _eventId parameters |
145
|
|
|
|
|
|
|
if($name eq 'lt' || $name eq 'execution' || $name eq '_eventId') { |
146
|
|
|
|
|
|
|
push @params, $name, $value; |
147
|
|
|
|
|
|
|
} |
148
|
|
|
|
|
|
|
} |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
# return the updated params |
151
|
|
|
|
|
|
|
return @params; |
152
|
|
|
|
|
|
|
}; |
153
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
#default heuristic for detecting the service and ticket in the login response |
155
|
|
|
|
|
|
|
my $defaultTicketHeuristic = sub { |
156
|
|
|
|
|
|
|
my ($response, $service) = @_; |
157
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
#attempt using the Location header on a redirect response |
159
|
|
|
|
|
|
|
if($response->is_redirect) { |
160
|
|
|
|
|
|
|
my $uri = $response->header('Location'); |
161
|
|
|
|
|
|
|
if($uri =~ /[?&]ticket=([^&]*)$/) { |
162
|
|
|
|
|
|
|
return $1; |
163
|
|
|
|
|
|
|
} |
164
|
|
|
|
|
|
|
} |
165
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
#check for a javascript window.location.href redirect |
167
|
|
|
|
|
|
|
if($response->decoded_content =~ /window\.location\.href="[^"]*ticket=([^&"]*?)"/sg) { |
168
|
|
|
|
|
|
|
return $1; |
169
|
|
|
|
|
|
|
} |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
return; |
172
|
|
|
|
|
|
|
}; |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
#default callback to log the user into CAS and return a ticket for the specified service |
175
|
|
|
|
|
|
|
my $defaultLoginCallback = sub { |
176
|
|
|
|
|
|
|
my ($service, $ua, $h) = @_; |
177
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
# generate the params for this login request |
179
|
|
|
|
|
|
|
my $loginUri = URI->new_abs('login', $h->{'casServer'}); |
180
|
|
|
|
|
|
|
my @params = ( |
181
|
|
|
|
|
|
|
'service' => $service, |
182
|
|
|
|
|
|
|
'username' => $h->{'username'}, |
183
|
|
|
|
|
|
|
'password' => $h->{'password'}, |
184
|
|
|
|
|
|
|
); |
185
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
# find any additional required login params (i.e. lt, execution, and _eventId) |
187
|
|
|
|
|
|
|
if(@{$h->{'config'}->{'param_heuristics'}}) { |
188
|
|
|
|
|
|
|
# retrieve the login form that will be parsed by configured param_heuristics |
189
|
|
|
|
|
|
|
my $formUri = $loginUri->clone(); |
190
|
|
|
|
|
|
|
$formUri->query_param('service', $service); |
191
|
|
|
|
|
|
|
my $response = $ua->simple_request(HTTP::Request::Common::GET($formUri)); |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
# process all configured param heuristics |
194
|
|
|
|
|
|
|
foreach (@{$h->{'config'}->{'param_heuristics'}}) { |
195
|
|
|
|
|
|
|
# skip invalid heuristics |
196
|
|
|
|
|
|
|
next if(ref($_) ne 'CODE'); |
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
# process this heuristic |
199
|
|
|
|
|
|
|
@params = $_->($service, $response, $ua, $h, @params); |
200
|
|
|
|
|
|
|
} |
201
|
|
|
|
|
|
|
} |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
# issue the login request |
204
|
|
|
|
|
|
|
my $response = $ua->simple_request(HTTP::Request::Common::POST($loginUri, \@params)); |
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
#short-circuit if there is no response from CAS for some reason |
207
|
|
|
|
|
|
|
return if(!$response); |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
#process all the ticket heuristics until a ticket is found |
210
|
|
|
|
|
|
|
foreach (@{$h->{'config'}->{'ticket_heuristics'}}) { |
211
|
|
|
|
|
|
|
#skip invalid heuristics |
212
|
|
|
|
|
|
|
next if(ref($_) ne 'CODE'); |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
#process the current heuristic |
215
|
|
|
|
|
|
|
my $ticket = eval {$_->($response, $service)}; |
216
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
#quit processing if a ticket is found |
218
|
|
|
|
|
|
|
return $ticket if(defined $ticket); |
219
|
|
|
|
|
|
|
} |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
#return undefined if no ticket was found |
222
|
|
|
|
|
|
|
return; |
223
|
|
|
|
|
|
|
}; |
224
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
# Login callback when the specified server is in proxy mode |
226
|
|
|
|
|
|
|
my $proxyLoginCallback = sub { |
227
|
|
|
|
|
|
|
my ($service, $ua, $h) = @_; |
228
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
#clear any previous error |
230
|
|
|
|
|
|
|
delete $h->{'error'}; |
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
#create the request uri |
233
|
|
|
|
|
|
|
my $ptUri = URI->new_abs('proxy', $h->{'casServer'}); |
234
|
|
|
|
|
|
|
$ptUri->query_form( |
235
|
|
|
|
|
|
|
'pgt' => $h->{'pgt'}, |
236
|
|
|
|
|
|
|
'targetService' => $service, |
237
|
|
|
|
|
|
|
); |
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
# fetch proxy ticket and parse response xml |
240
|
|
|
|
|
|
|
my $response = $ua->simple_request(HTTP::Request::Common::GET($ptUri)); |
241
|
|
|
|
|
|
|
my $doc = eval {XML::LibXML->new()->parse_string($response->decoded_content('charset' => 'none'))}; |
242
|
|
|
|
|
|
|
if($@ || !$doc) { |
243
|
|
|
|
|
|
|
$h->{'error'} = ERROR_PROXY_INVALIDRESPONSE; |
244
|
|
|
|
|
|
|
push @{$h->{'errors'}}, $h->{'error'}; |
245
|
|
|
|
|
|
|
return; |
246
|
|
|
|
|
|
|
} |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
# process the response to extract the proxy ticket or any errors |
249
|
|
|
|
|
|
|
my $xpc = XML::LibXML::XPathContext->new(); |
250
|
|
|
|
|
|
|
$xpc->registerNs('cas', XMLNS_CAS); |
251
|
|
|
|
|
|
|
if($xpc->exists('/cas:serviceResponse/cas:proxyFailure', $doc)) { |
252
|
|
|
|
|
|
|
my $code = $xpc->findvalue('/cas:serviceResponse/cas:proxyFailure[position()=1]/@code', $doc); |
253
|
|
|
|
|
|
|
if($code eq 'INVALID_TICKET') { |
254
|
|
|
|
|
|
|
$h->{'error'} = ERROR_PROXY_INVALIDTICKET; |
255
|
|
|
|
|
|
|
push @{$h->{'errors'}}, $h->{'error'}; |
256
|
|
|
|
|
|
|
} |
257
|
|
|
|
|
|
|
else { |
258
|
|
|
|
|
|
|
$h->{'error'} = ERROR_PROXY_UNKNOWN; |
259
|
|
|
|
|
|
|
push @{$h->{'errors'}}, $h->{'error'}; |
260
|
|
|
|
|
|
|
} |
261
|
|
|
|
|
|
|
} |
262
|
|
|
|
|
|
|
elsif($xpc->exists('/cas:serviceResponse/cas:proxySuccess', $doc)) { |
263
|
|
|
|
|
|
|
return $xpc->findvalue('/cas:serviceResponse/cas:proxySuccess[position()=1]/cas:proxyTicket[position()=1]', $doc); |
264
|
|
|
|
|
|
|
} |
265
|
|
|
|
|
|
|
else { |
266
|
|
|
|
|
|
|
$h->{'error'} = ERROR_PROXY_INVALIDRESPONSE; |
267
|
|
|
|
|
|
|
push @{$h->{'errors'}}, $h->{'error'}; |
268
|
|
|
|
|
|
|
} |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
# default to no ticket being returned |
271
|
|
|
|
|
|
|
return; |
272
|
|
|
|
|
|
|
}; |
273
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
#Login callback for CAS servers that implement the RESTful API |
275
|
|
|
|
|
|
|
#TODO: cache the TGT |
276
|
|
|
|
|
|
|
my $restLoginCallback = sub { |
277
|
|
|
|
|
|
|
my ($service, $ua, $h) = @_; |
278
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
#retrieve the tgt |
280
|
|
|
|
|
|
|
my $loginUri = URI->new_abs('v1/tickets', $h->{'casServer'}); |
281
|
|
|
|
|
|
|
my $tgtResponse = $ua->simple_request(HTTP::Request::Common::POST($loginUri, [ |
282
|
|
|
|
|
|
|
'username' => $h->{'username'}, |
283
|
|
|
|
|
|
|
'password' => $h->{'password'}, |
284
|
|
|
|
|
|
|
])); |
285
|
|
|
|
|
|
|
return if($tgtResponse->code != 201); |
286
|
|
|
|
|
|
|
my $tgtUri = $tgtResponse->header('Location'); |
287
|
|
|
|
|
|
|
|
288
|
|
|
|
|
|
|
#retrieve a ticket for the requested service |
289
|
|
|
|
|
|
|
my $ticketResponse = $ua->simple_request(HTTP::Request::Common::POST($tgtUri, [ |
290
|
|
|
|
|
|
|
'service' => $service, |
291
|
|
|
|
|
|
|
])); |
292
|
|
|
|
|
|
|
return if($ticketResponse->code != 200); |
293
|
|
|
|
|
|
|
return $ticketResponse->decoded_content; |
294
|
|
|
|
|
|
|
}; |
295
|
|
|
|
|
|
|
|
296
|
|
|
|
|
|
|
##Static Methods |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
#return the default user agent for this class |
299
|
|
|
|
|
|
|
sub _agent($) { |
300
|
|
|
|
|
|
|
return |
301
|
|
|
|
|
|
|
$_[0]->SUPER::_agent . ' ' . |
302
|
|
|
|
|
|
|
'CAS-UserAgent/' . $VERSION; |
303
|
|
|
|
|
|
|
} |
304
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
#Constructor |
306
|
|
|
|
|
|
|
sub new($%) { |
307
|
|
|
|
|
|
|
my $self = shift; |
308
|
|
|
|
|
|
|
my (%opt) = @_; |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
# remove any cas options before creating base object |
311
|
|
|
|
|
|
|
my $cas_opts = delete $opt{'cas_opts'}; |
312
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
#setup the base object |
314
|
|
|
|
|
|
|
$self = $self->SUPER::new(%opt); |
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
#attach a cas login handler if options were specified |
317
|
|
|
|
|
|
|
$self->attach_cas_handler(%$cas_opts) if(ref($cas_opts) eq 'HASH'); |
318
|
|
|
|
|
|
|
|
319
|
|
|
|
|
|
|
#return this object |
320
|
|
|
|
|
|
|
return $self; |
321
|
|
|
|
|
|
|
} |
322
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
=head1 METHODS |
324
|
|
|
|
|
|
|
|
325
|
|
|
|
|
|
|
The following methods are available: |
326
|
|
|
|
|
|
|
|
327
|
|
|
|
|
|
|
=over 4 |
328
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
=item $ua->attach_cas_handler( %options ) |
330
|
|
|
|
|
|
|
|
331
|
|
|
|
|
|
|
This method attaches a CAS handler to the current C |
332
|
|
|
|
|
|
|
object. |
333
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
The following options are supported: |
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
=over |
337
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
=item C => $url |
339
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
This option defines the base CAS URL to use for this handler. The base url is |
341
|
|
|
|
|
|
|
used to detect redirects to CAS for authentication and to issue any requests to |
342
|
|
|
|
|
|
|
CAS when authenticating. |
343
|
|
|
|
|
|
|
|
344
|
|
|
|
|
|
|
This option is required. |
345
|
|
|
|
|
|
|
|
346
|
|
|
|
|
|
|
=item C => $string |
347
|
|
|
|
|
|
|
|
348
|
|
|
|
|
|
|
This option defines the username to use for authenticating with the CAS server. |
349
|
|
|
|
|
|
|
|
350
|
|
|
|
|
|
|
This option is required unless using proxy mode. |
351
|
|
|
|
|
|
|
|
352
|
|
|
|
|
|
|
=item C => $string |
353
|
|
|
|
|
|
|
|
354
|
|
|
|
|
|
|
This option defines the password to use for authenticating with the CAS server. |
355
|
|
|
|
|
|
|
|
356
|
|
|
|
|
|
|
This option is required unless using proxy mode. |
357
|
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
=item C => $bool |
359
|
|
|
|
|
|
|
|
360
|
|
|
|
|
|
|
When this option is TRUE, C will use the RESTful API to |
361
|
|
|
|
|
|
|
authenticate with the CAS server. |
362
|
|
|
|
|
|
|
|
363
|
|
|
|
|
|
|
This option defaults to FALSE. |
364
|
|
|
|
|
|
|
|
365
|
|
|
|
|
|
|
=item C => $bool |
366
|
|
|
|
|
|
|
|
367
|
|
|
|
|
|
|
When this option is TRUE, C using a proxy granting |
368
|
|
|
|
|
|
|
ticket to authenticate with the CAS server. |
369
|
|
|
|
|
|
|
|
370
|
|
|
|
|
|
|
This option defaults to FALSE. |
371
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
=item C => $string |
373
|
|
|
|
|
|
|
|
374
|
|
|
|
|
|
|
This option specifies the proxy granting ticket to use when proxy mode is active. |
375
|
|
|
|
|
|
|
|
376
|
|
|
|
|
|
|
This option is required when using proxy mode. |
377
|
|
|
|
|
|
|
|
378
|
|
|
|
|
|
|
=item C => $bool |
379
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
When this option is TRUE, C will only allow |
381
|
|
|
|
|
|
|
authentication for the URL of the request requiring authentication. |
382
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
This option defaults to FALSE. |
384
|
|
|
|
|
|
|
|
385
|
|
|
|
|
|
|
=item C => $cb |
386
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
This option can be used to specify a custom callback to use when authenticating |
388
|
|
|
|
|
|
|
with CAS. The callback is called as follows: $cb->($service, $ua, $handler) and |
389
|
|
|
|
|
|
|
is expected to return a $ticket for the specified service on successful |
390
|
|
|
|
|
|
|
authentication. |
391
|
|
|
|
|
|
|
|
392
|
|
|
|
|
|
|
=back |
393
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
=back |
395
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
=cut |
397
|
|
|
|
|
|
|
|
398
|
|
|
|
|
|
|
#method that will attach the cas server login handler |
399
|
|
|
|
|
|
|
# server => the base CAS server uri to add a login handler for |
400
|
|
|
|
|
|
|
# username => the username to use to login to the specified CAS server |
401
|
|
|
|
|
|
|
# password => the password to use to login to the specified CAS server |
402
|
|
|
|
|
|
|
# pgt => the pgt for a proxy login handler |
403
|
|
|
|
|
|
|
# proxy => a boolean indicating this handler is a proxy login handler |
404
|
|
|
|
|
|
|
# restful => a boolean indicating if the CAS server supports the RESTful API |
405
|
|
|
|
|
|
|
# callback => a login callback to use for logging into CAS, it should return a ticket for the specified service |
406
|
|
|
|
|
|
|
# ticket_heuristics => an array of heuristic callbacks that are performed when searching for the service and ticket in a CAS response |
407
|
|
|
|
|
|
|
# strict => only allow CAS login when the service is the same as the original url |
408
|
|
|
|
|
|
|
sub attach_cas_handler($%) { |
409
|
|
|
|
|
|
|
my $self = shift; |
410
|
|
|
|
|
|
|
my (%opt) = @_; |
411
|
|
|
|
|
|
|
|
412
|
|
|
|
|
|
|
#short-circuit if required options aren't specified |
413
|
|
|
|
|
|
|
return if(!exists $opt{'server'}); |
414
|
|
|
|
|
|
|
return if(!$opt{'proxy'} && !(exists $opt{'username'} && exists $opt{'password'})); |
415
|
|
|
|
|
|
|
return if($opt{'proxy'} && !$opt{'pgt'}); |
416
|
|
|
|
|
|
|
|
417
|
|
|
|
|
|
|
#sanitize options |
418
|
|
|
|
|
|
|
$opt{'server'} = URI->new($opt{'server'} . ($opt{'server'} =~ /\/$/o ? '' : '/'))->canonical; |
419
|
|
|
|
|
|
|
my $callback = |
420
|
|
|
|
|
|
|
ref($opt{'callback'}) eq 'CODE' ? $opt{'callback'} : |
421
|
|
|
|
|
|
|
$opt{'proxy'} ? $proxyLoginCallback : |
422
|
|
|
|
|
|
|
$opt{'restful'} ? $restLoginCallback : |
423
|
|
|
|
|
|
|
$defaultLoginCallback; |
424
|
|
|
|
|
|
|
|
425
|
|
|
|
|
|
|
# process any default config values for bundled callbacks/heuristics, we do this here |
426
|
|
|
|
|
|
|
# instead of in the callbacks to make default values available to custom |
427
|
|
|
|
|
|
|
# callbacks |
428
|
|
|
|
|
|
|
$opt{'ticket_heuristics'} = [$opt{'ticket_heuristics'}] if(ref($opt{'ticket_heuristics'}) ne 'ARRAY'); |
429
|
|
|
|
|
|
|
push @{$opt{'ticket_heuristics'}}, $defaultTicketHeuristic; |
430
|
|
|
|
|
|
|
@{$opt{'ticket_heuristics'}} = grep {ref($_) eq 'CODE'} @{$opt{'ticket_heuristics'}}; |
431
|
|
|
|
|
|
|
|
432
|
|
|
|
|
|
|
$opt{'param_heuristics'} = [$opt{'param_heuristics'}] if(ref($opt{'param_heuristics'}) ne 'ARRAY'); |
433
|
|
|
|
|
|
|
push @{$opt{'param_heuristics'}}, $defaultLoginParamsHeuristic; |
434
|
|
|
|
|
|
|
@{$opt{'param_heuristics'}} = grep {ref($_) eq 'CODE'} @{$opt{'param_heuristics'}}; |
435
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
#remove any pre-existing login handler for the current CAS server |
437
|
|
|
|
|
|
|
$self->remove_cas_handlers($opt{'server'}); |
438
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
#attach a new CAS login handler |
440
|
|
|
|
|
|
|
$self->set_my_handler('response_done', $casLoginHandler, |
441
|
|
|
|
|
|
|
'owner' => CASHANDLERNAME, |
442
|
|
|
|
|
|
|
'casServer' => $opt{'server'}, |
443
|
|
|
|
|
|
|
'strict' => $opt{'strict'}, |
444
|
|
|
|
|
|
|
'loginCb' => $callback, |
445
|
|
|
|
|
|
|
'username' => $opt{'username'}, |
446
|
|
|
|
|
|
|
'password' => $opt{'password'}, |
447
|
|
|
|
|
|
|
'pgt' => $opt{'pgt'}, |
448
|
|
|
|
|
|
|
'config' => \%opt, |
449
|
|
|
|
|
|
|
'errors' => [], |
450
|
|
|
|
|
|
|
'running' => 0, |
451
|
|
|
|
|
|
|
'm_code' => [ |
452
|
|
|
|
|
|
|
HTTP::Status::HTTP_MOVED_PERMANENTLY, |
453
|
|
|
|
|
|
|
HTTP::Status::HTTP_FOUND, |
454
|
|
|
|
|
|
|
HTTP::Status::HTTP_SEE_OTHER, |
455
|
|
|
|
|
|
|
HTTP::Status::HTTP_TEMPORARY_REDIRECT, |
456
|
|
|
|
|
|
|
], |
457
|
|
|
|
|
|
|
); |
458
|
|
|
|
|
|
|
|
459
|
|
|
|
|
|
|
return 1; |
460
|
|
|
|
|
|
|
} |
461
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
sub get_cas_handlers($;$) { |
463
|
|
|
|
|
|
|
my $self = shift; |
464
|
|
|
|
|
|
|
my ($server) = @_; |
465
|
|
|
|
|
|
|
|
466
|
|
|
|
|
|
|
$server = URI->new($server . ($server =~ /\/$/o ? '' : '/'))->canonical if(defined $server); |
467
|
|
|
|
|
|
|
return $self->get_my_handler('response_done', |
468
|
|
|
|
|
|
|
'owner' => CASHANDLERNAME, |
469
|
|
|
|
|
|
|
(defined $server ? ('casServer' => $server) : ()), |
470
|
|
|
|
|
|
|
); |
471
|
|
|
|
|
|
|
} |
472
|
|
|
|
|
|
|
|
473
|
|
|
|
|
|
|
# method that will retrieve a ticket for the specified service |
474
|
|
|
|
|
|
|
sub get_cas_ticket($$;$) { |
475
|
|
|
|
|
|
|
my $self = shift; |
476
|
|
|
|
|
|
|
my ($service, $server) = @_; |
477
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
# resolve which handler to use |
479
|
|
|
|
|
|
|
my $h; |
480
|
|
|
|
|
|
|
if(ref($server) eq 'HASH' && defined $server->{'casServer'}) { |
481
|
|
|
|
|
|
|
$h = $server; |
482
|
|
|
|
|
|
|
} |
483
|
|
|
|
|
|
|
else { |
484
|
|
|
|
|
|
|
my @handlers = $self->get_cas_handlers($server); |
485
|
|
|
|
|
|
|
die 'too many CAS servers found, try specifying a specific CAS server' if(@handlers > 1); |
486
|
|
|
|
|
|
|
$h = $handlers[0]; |
487
|
|
|
|
|
|
|
} |
488
|
|
|
|
|
|
|
die 'cannot find a CAS server to fetch the ST from' if(!$h); |
489
|
|
|
|
|
|
|
|
490
|
|
|
|
|
|
|
# get a ticket from the handler |
491
|
|
|
|
|
|
|
$h->{'running'}++; |
492
|
|
|
|
|
|
|
my $ticket = eval {$h->{'loginCb'}->($service, LWP::UserAgent->new('cookie_jar' => {}), $h)}; |
493
|
|
|
|
|
|
|
$h->{'running'}--; |
494
|
|
|
|
|
|
|
|
495
|
|
|
|
|
|
|
# return the found ticket |
496
|
|
|
|
|
|
|
return $ticket; |
497
|
|
|
|
|
|
|
} |
498
|
|
|
|
|
|
|
|
499
|
|
|
|
|
|
|
#method that will remove the cas login handlers for the specified cas servers or all if a specified server is not provided |
500
|
|
|
|
|
|
|
sub remove_cas_handlers($@) { |
501
|
|
|
|
|
|
|
my $self = shift; |
502
|
|
|
|
|
|
|
|
503
|
|
|
|
|
|
|
#remove cas login handlers for any specified cas servers |
504
|
|
|
|
|
|
|
$self->remove_handler('response_done', |
505
|
|
|
|
|
|
|
'owner' => CASHANDLERNAME, |
506
|
|
|
|
|
|
|
'casServer' => $_, |
507
|
|
|
|
|
|
|
) foreach(map {URI->new($_ . ($_ =~ /\/$/o ? '' : '/'))->canonical} @_); |
508
|
|
|
|
|
|
|
|
509
|
|
|
|
|
|
|
#remove all cas login handlers if no servers were specified |
510
|
|
|
|
|
|
|
$self->remove_handler('response_done', |
511
|
|
|
|
|
|
|
'owner' => CASHANDLERNAME, |
512
|
|
|
|
|
|
|
) if(!@_); |
513
|
|
|
|
|
|
|
|
514
|
|
|
|
|
|
|
return; |
515
|
|
|
|
|
|
|
} |
516
|
|
|
|
|
|
|
|
517
|
|
|
|
|
|
|
1; |
518
|
|
|
|
|
|
|
|
519
|
|
|
|
|
|
|
__END__ |