| 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 |