blib/lib/WebService/Windows/LiveID/Auth.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 95 | 98 | 96.9 |
branch | 18 | 26 | 69.2 |
condition | 14 | 26 | 53.8 |
subroutine | 21 | 23 | 91.3 |
pod | 8 | 8 | 100.0 |
total | 156 | 181 | 86.1 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package WebService::Windows::LiveID::Auth; | ||||||
2 | |||||||
3 | 6 | 6 | 76376 | use strict; | |||
6 | 19 | ||||||
6 | 521 | ||||||
4 | 6 | 6 | 33 | use warnings; | |||
6 | 14 | ||||||
6 | 190 | ||||||
5 | |||||||
6 | 6 | 6 | 33 | use base qw(Class::Accessor::Fast); | |||
6 | 12 | ||||||
6 | 18411 | ||||||
7 | |||||||
8 | 6 | 6 | 30052 | use Carp::Clan qw(croak); | |||
6 | 21270 | ||||||
6 | 255 | ||||||
9 | 6 | 6 | 26674 | use CGI; | |||
6 | 101995 | ||||||
6 | 1293 | ||||||
10 | 6 | 6 | 10275 | use Crypt::Rijndael; | |||
6 | 5367 | ||||||
6 | 189 | ||||||
11 | 6 | 6 | 7328 | use Digest::SHA (); | |||
6 | 27431 | ||||||
6 | 172 | ||||||
12 | 6 | 6 | 5760 | use MIME::Base64 (); | |||
6 | 5019 | ||||||
6 | 172 | ||||||
13 | 6 | 6 | 2229 | use URI; | |||
6 | 11177 | ||||||
6 | 149 | ||||||
14 | 6 | 6 | 3075 | use URI::QueryParam; | |||
6 | 1452 | ||||||
6 | 155 | ||||||
15 | 6 | 6 | 36 | use URI::Escape (); | |||
6 | 18 | ||||||
6 | 139 | ||||||
16 | |||||||
17 | 6 | 6 | 4016 | use WebService::Windows::LiveID::Auth::User; | |||
6 | 22 | ||||||
6 | 95 | ||||||
18 | |||||||
19 | __PACKAGE__->mk_accessors(qw/ | ||||||
20 | appid | ||||||
21 | algorithm | ||||||
22 | _secret_key | ||||||
23 | _crypt_key | ||||||
24 | _sign_key | ||||||
25 | /); | ||||||
26 | |||||||
27 | my $control_url = 'http://login.live.com/controls/WebAuth.htm'; | ||||||
28 | my $sign_in_url = 'http://login.live.com/wlogin.srf'; | ||||||
29 | my $sign_out_url = 'http://login.live.com/logout.srf'; | ||||||
30 | |||||||
31 | =head1 NAME | ||||||
32 | |||||||
33 | WebService::Windows::LiveID::Auth - Perl implementation of Windows Live ID Web Authentication 1.0 | ||||||
34 | |||||||
35 | =head1 VERSION | ||||||
36 | |||||||
37 | version 0.01 | ||||||
38 | |||||||
39 | =cut | ||||||
40 | |||||||
41 | our $VERSION = '0.01'; | ||||||
42 | |||||||
43 | =head1 SYNOPSIS | ||||||
44 | |||||||
45 | use WebService::Windows::LiveID::Auth; | ||||||
46 | |||||||
47 | my $appid = '00163FFF80003203'; | ||||||
48 | my $secret_key = 'ApplicationKey123'; | ||||||
49 | my $appctx = 'zigorou'; | ||||||
50 | |||||||
51 | my $auth = WebService::Windows::LiveID::Auth->new({ | ||||||
52 | appid => $appid, | ||||||
53 | secret_key => $secret_key | ||||||
54 | }); | ||||||
55 | |||||||
56 | local $\ = "\n"; | ||||||
57 | |||||||
58 | print $auth->control_url; ### SignIn, SignOut links page by LiveID. Set this page url to iframe's src attribute. | ||||||
59 | print $auth->sign_in_url; ### SignIn page | ||||||
60 | print $auth->sign_out_url; ### SignOut page | ||||||
61 | |||||||
62 | In the request to "ReturnURL", | ||||||
63 | |||||||
64 | use CGI; | ||||||
65 | use WebService::Windows::LiveID::Auth; | ||||||
66 | |||||||
67 | my $q = CGI->new; | ||||||
68 | |||||||
69 | my $appid = '00163FFF80003203'; | ||||||
70 | my $secret_key = 'ApplicationKey123'; | ||||||
71 | my $appctx = 'zigorou'; | ||||||
72 | |||||||
73 | my $auth = WebService::Windows::LiveID::Auth->new({ | ||||||
74 | appid => $appid, | ||||||
75 | secret_key => $secret_key | ||||||
76 | }); | ||||||
77 | |||||||
78 | my $user = eval { $auth->process_token($q->param("stoken"), $appctx); }; | ||||||
79 | print $q->header; | ||||||
80 | |||||||
81 | unless ($@) { | ||||||
82 | print " Login sucsess. \n"; |
||||||
83 | print " uid: " . $user->uid . " "; |
||||||
84 | } | ||||||
85 | else { | ||||||
86 | print " Login failed. "; |
||||||
87 | } | ||||||
88 | |||||||
89 | =head1 METHODS | ||||||
90 | |||||||
91 | =head2 new($arguments) | ||||||
92 | |||||||
93 | Constructor. | ||||||
94 | $arguments must be HASH reference. | ||||||
95 | |||||||
96 | ## Constructor parameter sample. | ||||||
97 | $arguments = { | ||||||
98 | appid => '00163FFF80003203', ## required | ||||||
99 | secret_key => 'ApplicationKey123', ## required | ||||||
100 | algorithm => 'wsignin1.0' ## optional | ||||||
101 | }; | ||||||
102 | |||||||
103 | =cut | ||||||
104 | |||||||
105 | sub new { | ||||||
106 | 6 | 6 | 1 | 26518 | my ($class, $arguments) = @_; | ||
107 | |||||||
108 | 6 | 50 | 58 | $arguments->{algorithm} ||= 'wsignin1.0'; | |||
109 | |||||||
110 | 6 | 13 | my $args = {}; | ||||
111 | |||||||
112 | 6 | 20 | for my $prop (qw/appid secret_key algorithm/) { | ||||
113 | 16 | 100 | 66 | 92 | if (exists $arguments->{$prop} && $arguments->{$prop}) { | ||
114 | 15 | 42 | $args->{$prop} = $arguments->{$prop}; | ||||
115 | } | ||||||
116 | else { | ||||||
117 | 1 | 8 | croak(qq|$prop is required parameter|); | ||||
118 | } | ||||||
119 | } | ||||||
120 | |||||||
121 | 5 | 74 | my $self = $class->SUPER::new($args); | ||||
122 | 5 | 89 | $self->secret_key($args->{secret_key}); | ||||
123 | |||||||
124 | 5 | 82 | return $self; | ||||
125 | } | ||||||
126 | |||||||
127 | =head2 process_token($stoken, $appctx) | ||||||
128 | |||||||
129 | Process and validate stoken value. | ||||||
130 | If the authentication is sucsess, then this method will return L |
||||||
131 | On fail, return undef value. | ||||||
132 | |||||||
133 | =cut | ||||||
134 | |||||||
135 | sub process_token { | ||||||
136 | 2 | 2 | 1 | 49 | my ($self, $stoken, $appctx) = @_; | ||
137 | |||||||
138 | 2 | 50 | 11 | croak('stoken parameter is required') unless ($stoken); | |||
139 | |||||||
140 | 2 | 6 | $stoken = $self->_uud64($stoken); | ||||
141 | |||||||
142 | 2 | 100 | 33 | 71 | croak('Invalid stoken value') if (!$stoken || (length $stoken) <= 16 || (length $stoken) % 16 != 0); | ||
66 | |||||||
143 | |||||||
144 | 1 | 2 | my $iv = substr($stoken, 0, 16); | ||||
145 | 1 | 2 | my $crypted = substr($stoken, 16); | ||||
146 | |||||||
147 | 1 | 50 | 33 | 6 | croak('Invalid iv or crypted value') unless ($iv && $crypted); | ||
148 | |||||||
149 | 1 | 4 | my $cipher = Crypt::Rijndael->new($self->_crypt_key, Crypt::Rijndael::MODE_CBC); | ||||
150 | 1 | 20 | $cipher->set_iv($iv); | ||||
151 | |||||||
152 | 1 | 9 | my $token = $cipher->decrypt($crypted); | ||||
153 | 1 | 5 | my ($body, $sig) = split(/&sig=/, $token); | ||||
154 | |||||||
155 | 1 | 50 | 33 | 8 | croak('Failed to decrypt token') unless ($body && $sig); | ||
156 | 1 | 50 | 4 | croak('Invalid signature') if (Digest::SHA::hmac_sha256($body, $self->_sign_key) ne $self->_uud64($sig)); | |||
157 | |||||||
158 | 1 | 20 | my $query = CGI->new($token); | ||||
159 | |||||||
160 | 1 | 3643 | return WebService::Windows::LiveID::Auth::User->new({$query->Vars}); | ||||
161 | } | ||||||
162 | |||||||
163 | =head2 control_url([$query]) | ||||||
164 | |||||||
165 | Return control url as L |
||||||
166 | $query parameter is optional, It must be HASH reference. | ||||||
167 | |||||||
168 | ## query parameter sample | ||||||
169 | $query = { | ||||||
170 | appctx => "zigorou", | ||||||
171 | style => "font-family: Times Roman;" | ||||||
172 | }; | ||||||
173 | |||||||
174 | Or | ||||||
175 | |||||||
176 | $query = { | ||||||
177 | appctx => "zigorou", | ||||||
178 | style => { | ||||||
179 | "font-family" => "Verdana", | ||||||
180 | "color" => "Grey" | ||||||
181 | } | ||||||
182 | } | ||||||
183 | |||||||
184 | The "style" property allows SCALAR and HASH reference. | ||||||
185 | |||||||
186 | =cut | ||||||
187 | |||||||
188 | sub control_url { | ||||||
189 | 4 | 4 | 1 | 2013 | my ($self, $query) = @_; | ||
190 | 4 | 16 | my $control_url = URI->new($control_url); | ||||
191 | |||||||
192 | 4 | 211 | $control_url->query_param('appid', $self->appid); | ||||
193 | 4 | 503 | $control_url->query_param('alg', $self->algorithm); | ||||
194 | |||||||
195 | 4 | 100 | 66 | 451 | if ($query && ref $query eq 'HASH') { | ||
196 | 3 | 50 | 17 | $control_url->query_param('appctx', $query->{appctx}) if ($query->{appctx}); | |||
197 | 3 | 100 | 413 | if ($query->{style}) { | |||
198 | 2 | 100 | 13 | $query->{style} = $self->_style_to_string($query->{style}) if (ref $query->{style} eq "HASH"); | |||
199 | 2 | 8 | $control_url->query_param('style', $query->{style}); | ||||
200 | } | ||||||
201 | } | ||||||
202 | |||||||
203 | 4 | 552 | return $control_url; | ||||
204 | } | ||||||
205 | |||||||
206 | =head2 sign_in_url([$query]) | ||||||
207 | |||||||
208 | Return sign-in url as L |
||||||
209 | $query parameter is optional, It must be HASH reference. | ||||||
210 | |||||||
211 | ## query parameter sample | ||||||
212 | $query = { | ||||||
213 | appctx => "zigorou" | ||||||
214 | }; | ||||||
215 | |||||||
216 | =cut | ||||||
217 | |||||||
218 | sub sign_in_url { | ||||||
219 | 2 | 2 | 1 | 895 | my ($self, $query) = @_; | ||
220 | 2 | 10 | my $sign_in_url = URI->new($sign_in_url); | ||||
221 | |||||||
222 | 2 | 150 | $sign_in_url->query_param('appid', $self->appid); | ||||
223 | 2 | 218 | $sign_in_url->query_param('alg', $self->algorithm); | ||||
224 | 2 | 50 | 66 | 287 | $sign_in_url->query_param('appctx', $query->{appctx}) if ($query && ref $query eq 'HASH' && $query->{appctx}); | ||
66 | |||||||
225 | |||||||
226 | 2 | 180 | return $sign_in_url; | ||||
227 | } | ||||||
228 | |||||||
229 | =head2 sign_out_url() | ||||||
230 | |||||||
231 | Return sign-out url as L |
||||||
232 | |||||||
233 | =cut | ||||||
234 | |||||||
235 | sub sign_out_url { | ||||||
236 | 1 | 1 | 1 | 329 | my $self = shift; | ||
237 | |||||||
238 | 1 | 6 | my $sign_out_url = URI->new($sign_out_url); | ||||
239 | 1 | 64 | $sign_out_url->query_param('appid', $self->appid); | ||||
240 | |||||||
241 | 1 | 93 | return $sign_out_url; | ||||
242 | } | ||||||
243 | |||||||
244 | =head2 appid([$appid]) | ||||||
245 | |||||||
246 | Application ID | ||||||
247 | |||||||
248 | =head2 algorithm([$algorithm]) | ||||||
249 | |||||||
250 | Algorithm name | ||||||
251 | |||||||
252 | =head2 secret_key([$secret_key]) | ||||||
253 | |||||||
254 | Secret key | ||||||
255 | |||||||
256 | =cut | ||||||
257 | |||||||
258 | sub secret_key { | ||||||
259 | 5 | 5 | 1 | 13 | my ($self, $secret_key) = @_; | ||
260 | |||||||
261 | 5 | 50 | 23 | if ($secret_key) { | |||
262 | 5 | 36 | $self->_secret_key($secret_key); | ||||
263 | 5 | 92 | $self->_sign_key($self->_derive_key("SIGNATURE")); | ||||
264 | 5 | 221 | $self->_crypt_key($self->_derive_key("ENCRYPTION")); | ||||
265 | } | ||||||
266 | else { | ||||||
267 | 0 | 0 | return $self->_secret_key; | ||||
268 | } | ||||||
269 | } | ||||||
270 | |||||||
271 | =head2 sign_key() | ||||||
272 | |||||||
273 | Signature key. | ||||||
274 | |||||||
275 | =cut | ||||||
276 | |||||||
277 | 0 | 0 | 1 | 0 | sub sign_key { shift->_sign_key; } | ||
278 | |||||||
279 | =head2 crypt_key() | ||||||
280 | |||||||
281 | Encryption key | ||||||
282 | |||||||
283 | =cut | ||||||
284 | |||||||
285 | 0 | 0 | 1 | 0 | sub crypt_key { shift->_crypt_key; } | ||
286 | |||||||
287 | ### | ||||||
288 | ### private methods | ||||||
289 | ### | ||||||
290 | |||||||
291 | sub _derive_key { | ||||||
292 | 10 | 10 | 18 | my ($self, $prefix) = @_; | |||
293 | 10 | 32 | return substr(Digest::SHA::sha256($prefix . $self->_secret_key), 0, 16); | ||||
294 | } | ||||||
295 | |||||||
296 | sub _style_to_string { | ||||||
297 | 1 | 1 | 2 | my ($self, $props) = @_; | |||
298 | 1 | 3 | my @allow_props = qw(font-family font-weight font-style font-size color background); | ||||
299 | |||||||
300 | 6 | 15 | return join(" ", | ||||
301 | 6 | 50 | 25 | map { join(": ", $_, $props->{$_}) . ";" } | |||
302 | 1 | 3 | grep { exists $props->{$_} && $props->{$_} } | ||||
303 | @allow_props | ||||||
304 | ); | ||||||
305 | } | ||||||
306 | |||||||
307 | sub _uud64 { | ||||||
308 | 3 | 3 | 18 | my ($self, $strings) = @_; | |||
309 | 3 | 9 | return MIME::Base64::decode_base64(URI::Escape::uri_unescape($strings)); | ||||
310 | } | ||||||
311 | |||||||
312 | =head1 SEE ALSO | ||||||
313 | |||||||
314 | =over 4 | ||||||
315 | |||||||
316 | =item http://go.microsoft.com/fwlink/?linkid=92886 | ||||||
317 | |||||||
318 | =item http://msdn2.microsoft.com/en-us/library/bb676626.aspx | ||||||
319 | |||||||
320 | =item http://dev.live.com/blogs/liveid/archive/2006/05/18/8.aspx | ||||||
321 | |||||||
322 | =item http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=646&SiteID=1 | ||||||
323 | |||||||
324 | =item http://www.microsoft.com/downloads/details.aspx?FamilyId=8BA187E5-3630-437D-AFDF-59AB699A483D&displaylang=en | ||||||
325 | |||||||
326 | =item http://msdn2.microsoft.com/en-us/library/bb288408.aspx | ||||||
327 | |||||||
328 | =item L |
||||||
329 | |||||||
330 | =item L |
||||||
331 | |||||||
332 | =item L |
||||||
333 | |||||||
334 | =item L |
||||||
335 | |||||||
336 | =back | ||||||
337 | |||||||
338 | =head1 AUTHOR | ||||||
339 | |||||||
340 | Toru Yamaguchi, C<< |
||||||
341 | |||||||
342 | =head1 BUGS | ||||||
343 | |||||||
344 | Please report any bugs or feature requests to | ||||||
345 | C |
||||||
346 | L |
||||||
347 | notified of progress on your bug as I make changes. | ||||||
348 | |||||||
349 | =head1 COPYRIGHT & LICENSE | ||||||
350 | |||||||
351 | Copyright 2007 Toru Yamaguchi, All Rights Reserved. | ||||||
352 | |||||||
353 | This program is free software; you can redistribute it and/or modify it | ||||||
354 | under the same terms as Perl itself. | ||||||
355 | |||||||
356 | =cut | ||||||
357 | |||||||
358 | 1; # End of WebService::Windows::LiveID::Auth |