File Coverage

blib/lib/Google/SAML/Request.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             # Copyright (c) 2008-2009 Manni Heumann. All rights reserved.
2             #
3             # This program is free software; you can redistribute it and/or
4             # modify it under the same terms as Perl itself.
5             #
6             # Date: 2009-11-06
7             # Revision: 43
8             #
9              
10             package Google::SAML::Request;
11              
12             =head1 NAME
13              
14             Google::SAML::Request - Create or parse Google's SAML requests
15              
16             =head1 VERSION
17              
18             You are currently reading the documentation for version 0.05
19              
20             =head1 DESCRIPTION
21              
22             Google::SAML::Request will parse (and, for the sake of completeness, create)
23             SAML requests as used by Google. B that Google::SAML::Request is by
24             no means a full implementation of the SAML 2.0 standard. But if you want to
25             talk to Google to authenticate users, you should be fine.
26              
27             In fact, if you want to talk to Google about SSO, just use
28             L
29             and you should be fine.
30              
31             =head1 SYNOPSIS
32              
33             Create a new request object by taking the request ouf of the CGI environment:
34              
35             use Google::SAML::Request;
36             my $req = Google::SAML::Request->new_from_cgi();
37             if ( $req->ProviderName() eq 'google.com'
38             && $req->AssertionConsumerServiceURL() eq 'https://www.google.com/hosted/psosamldemo.net/acs' ) {
39              
40             processRequest();
41             }
42             else {
43             print "go talk to someone else\n";
44             }
45              
46             Or use a request string that you get from somewhere else (but make sure that it is no longer
47             URI-escaped):
48              
49             use Google::SAML::Request;
50             my $req = Google::SAML::Request->new_from_string( $request_string );
51             if ( $req->ProviderName() eq 'google.com'
52             && $req->AssertionConsumerServiceURL() eq 'https://www.google.com/hosted/psosamldemo.net/acs' ) {
53              
54             processRequest();
55             }
56             else {
57             print "go talk to someone else\n";
58             }
59              
60             Or, finally, create a request from scratch and send that to somebody else:
61              
62             use Google::SAML::Request;
63             my $req = Google::SAML::Request->new(
64             {
65             ProviderName => 'me.but.invalid',
66             AssertionConsumerServiceURL => 'http://send.your.users.here.invalid/script',
67             }
68             );
69              
70              
71              
72              
73             =head1 PREREQUISITES
74              
75             You will need the following modules installed:
76              
77             =over
78              
79             =item * L
80              
81             =item * L
82              
83             =item * L
84              
85             =item * L
86              
87             =item * L
88              
89             =item * L (if you are going to use the 'new_from_cgi' constructor)
90              
91             =back
92              
93             =head1 METHODS
94              
95             =cut
96              
97 3     3   104235 use strict;
  3         8  
  3         115  
98 3     3   16 use warnings;
  3         6  
  3         80  
99              
100 3     3   3035 use MIME::Base64;
  3         2783  
  3         224  
101 3     3   3589 use Compress::Zlib;
  3         351059  
  3         1024  
102 3     3   2126 use Date::Format;
  3         20158  
  3         215  
103 3     3   28 use Carp;
  3         5  
  3         168  
104 3     3   1749 use XML::Simple;
  0            
  0            
105             use URI::Escape;
106              
107              
108             our $VERSION = '0.05';
109              
110              
111             =head2 new
112              
113             Create a new Google::SAML::Request object from scratch.
114              
115             You have to provide the needed parameters here. Some parameters
116             are optional and defaults are used if they are not supplied.
117              
118             The parameters need to be passed in in a hash reference as
119             key value pairs.
120              
121             =head3 Required parameters
122              
123             =over
124              
125             =item * ProviderName
126              
127             Your name, e.g. 'google.com'
128              
129             =item * AssertionConsumerServiceURL
130              
131             The URL the user used to contact you. E.g. 'https://www.google.com/hosted/psosamldemo.net/acs'
132              
133             =back
134              
135             =head3 Optional parameters
136              
137             =over
138              
139             =item * IssueInstant
140              
141             The time stamp for the Request. Default is I.
142              
143             =item * ID
144              
145             If you need to create the ID yourself, use this option. Otherwise the ID is
146             generated from the current time and a pseudo-random number.
147              
148             =back
149              
150             =cut
151              
152             sub new {
153             my $class = shift;
154             my $args = shift;
155              
156             my $self = {
157             ProviderName
158             => '',
159             AssertionConsumerServiceURL
160             => '',
161             IssueInstant
162             => time2str( "%Y-%m-%dT%XZ", time, 'UTC' ),
163             ID
164             => undef,
165             };
166              
167             bless $self, $class;
168              
169             foreach my $required ( qw/ ProviderName AssertionConsumerServiceURL / ) {
170             confess "You need to provide the $required parameter to Googe::SAML::Request::new()"
171             unless exists $args->{ $required };
172             $self->{ $required } = $args->{ $required };
173             }
174              
175             foreach my $optional ( qw/ IssueInstant ID / ) {
176             $self->{ $optional } = $args->{ $optional }
177             if exists $args->{ $optional };
178             }
179              
180             unless ( defined $self->{ ID } ) {
181             $self->{ ID } = $self->_generate_id();
182             }
183              
184             $self->{request} = {
185             'ID' => $self->{ID},
186             'Version' => '2.0',
187             'xmlns:samlp' => 'urn:oasis:names:tc:SAML:2.0:protocol',
188             'IssueInstant' => $self->{IssueInstant},
189             'ProviderName' => $self->{ProviderName},
190             'ProtocolBinding' => 'urn:oasis:names.tc:SAML:2.0:bindings:HTTP-Redirect',
191             'AssertionConsumerServiceURL' => $self->{AssertionConsumerServiceURL},
192             };
193              
194             return $self;
195             }
196              
197              
198              
199              
200             =head2 new_from_cgi
201              
202             Create a new Google::SAML::Request object by fishing it out of the CGI
203             environment.
204              
205             If you provide a hash-ref with the key 'param_name' you can determine
206             which cgi parameter to use. The default is 'SAMLRequest'.
207              
208             =cut
209              
210             sub new_from_cgi {
211             my $class = shift;
212             my $args = shift;
213              
214             my $self ={};
215             bless $self, $class;
216              
217             require CGI;
218             my $cgi = CGI->new();
219              
220             my $param = ( exists $args->{param_name} ) ? $args->{param_name} : 'SAMLRequest';
221             my $request = $cgi->param( $param );
222              
223             if ( ! $request ) {
224             warn "could not get request from cgi environment through parameter '$param'.";
225             }
226             elsif ( $self->_decode_saml_msg( $request ) ) {
227             return $self;
228             }
229              
230             return;
231             }
232              
233              
234              
235              
236              
237              
238             =head2 new_from_string
239              
240             Pass in a (uri_unescaped!) string that contains the request string. The string
241             will be base64-unencoded, inflated and parsed. You'll get back a fresh
242             Google::SAML::Response object if the string can be parsed.
243              
244             =cut
245              
246             sub new_from_string {
247             my $class = shift;
248             my $string = shift;
249              
250             my $self = {};
251             bless $self, $class;
252              
253             if ( $self->_decode_saml_msg( $string ) ) {
254             return $self;
255             }
256             else {
257             return;
258             }
259             }
260              
261              
262              
263             =head2 get_xml
264              
265             Returns the XML representation of the request.
266              
267             =cut
268              
269             sub get_xml {
270             my $self = shift;
271              
272             if ( exists $self->{request} ) {
273             return
274             XMLout( $self->{request},
275             KeyAttr => [ keys %{$self->{request}} ],
276             RootName => 'samlp:AuthnRequest',
277             XMLDecl => 1
278             );
279             }
280             else {
281             confess "The request object hasn't even been made yet";
282             }
283             }
284              
285              
286             =head2 get_get_param
287              
288             No, that's not a typo. This method will return the request in a form
289             suitable to be used as a GET parameter. In other words, this method
290             will take the XML representation, compress it, base64-encode the result
291             and, finally, URI-escape that.
292              
293             =cut
294              
295             sub get_get_param {
296             my $self = shift;
297              
298             my $xml = $self->get_xml();
299              
300             my ( $d, $status ) = deflateInit( -WindowBits => -&MAX_WBITS() );
301              
302             if ( $status == Z_OK && $d ) {
303             my ( $compressed, $status ) = $d->deflate( $xml );
304             $compressed .= $d->flush();
305              
306             if ( $status == Z_OK && length( $compressed ) ) {
307             my $encoded = encode_base64( $compressed, '' );
308             my $escaped = uri_escape( $encoded );
309              
310             return $escaped;
311             }
312             else {
313             warn "Could not compress xml";
314             }
315             }
316             else {
317             warn "Could not initialise deflation stream.";
318             }
319              
320             return;
321             }
322              
323              
324              
325              
326              
327             sub _generate_id {
328             my $self = shift;
329              
330             my $id = '';
331              
332             my $time = time;
333             foreach ( split //, $time ) {
334             $id .= chr( $_ + 97 );
335             }
336              
337             foreach ( 1 .. 30 ) {
338             $id .= chr( int(rand( 26 )) + 97 );
339             }
340              
341             return $id;
342             }
343              
344              
345              
346              
347             sub _decode_saml_msg {
348             my $self = shift;
349             my $msg = shift;
350              
351             my $decoded = decode_base64( $msg );
352             my $inflated = undef;
353              
354             foreach my $wbits ( -&MAX_WBITS(), &MAX_WBITS() ) {
355             $inflated = $self->_inflate( $decoded, $wbits );
356             last if defined $inflated;
357             }
358              
359             if ( defined $inflated ) {
360             $self->{request} = XMLin( $inflated, ForceArray => 0 );
361             foreach ( qw/ ProviderName AssertionConsumerServiceURL ID IssueInstant / ) {
362             $self->{ $_ } = $self->{request}->{ $_ };
363             }
364              
365             return 1;
366             }
367             else {
368             warn "Could not inflate base64-decoded string.";
369             }
370              
371             return;
372             }
373              
374              
375              
376              
377             sub _inflate {
378             my $self = shift;
379             my $string = shift;
380             my $windowBits = shift;
381              
382             my ( $i, $status ) = inflateInit( -WindowBits => $windowBits );
383              
384             if ( $status == Z_OK ) {
385             my $inflated;
386             ($inflated, $status) = $i->inflate( $string );
387              
388             if ( $status == Z_OK || $status == Z_STREAM_END ) {
389             return $inflated;
390             }
391             }
392             else {
393             warn "No inflater!";
394             }
395              
396             return;
397             }
398              
399             =head3 Accessor methods (read-only)
400              
401             All of the following accessor methods return the value of the
402             attribute with the same name
403              
404             =head2 AssertionConsumerServiceURL
405              
406             =head2 ID
407              
408             =head2 IssueInstant
409              
410             =head2 ProtocolBinding
411              
412             =head2 ProviderName
413              
414             =head2 Version
415              
416             =cut
417              
418             sub AssertionConsumerServiceURL { return shift->{AssertionConsumerServiceURL}; }
419             sub ID { return shift->{ID}; }
420             sub IssueInstant { return shift->{IssueInstant}; }
421             sub ProviderName { return shift->{ProviderName}; }
422              
423             =head1 SOURCE CODE
424              
425             This module has a repository on github. Pull requests are welcome.
426              
427             https://github.com/mannih/Google-SAML-Request/
428              
429             =head1 AUTHOR
430              
431             Manni Heumann (saml at lxxi dot org)
432              
433              
434             =head1 LICENSE
435              
436             Copyright (c) 2008 Manni Heumann. All rights reserved.
437              
438             This program is free software; you can redistribute it and/or
439             modify it under the same terms as Perl itself.
440              
441             =cut
442              
443              
444             1;