File Coverage

blib/lib/Google/SAML/Response.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             # Copyright (c) 2013 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              
7             package Google::SAML::Response;
8              
9             =head1 NAME
10              
11             Google::SAML::Response - Generate signed XML documents as SAML responses for
12             Google's SSO implementation
13              
14             =head1 VERSION
15              
16             You are currently reading the documentation for version 0.14
17              
18             =head1 DESCRIPTION
19              
20             Google::SAML::Response can be used to generate a signed XML document that is
21             needed for logging your users into Google using SSO.
22              
23             You have some sort of web application that can identify and authenticate users.
24             You want users to be able to use some sort of Google service such as Google mail.
25              
26             When using SSO with your Google partner account, your users will send a request
27             to a Google URL. If the user isn't already logged in to Google, Google will
28             redirect him to a URL that you can define. Behind this URL, you need to have
29             a script that authenticates users in your original framework and generates a
30             SAML response for Google that you send back to the user whose browser will then
31             submit it back to Google. If everything works, users will then be logged into
32             their Google account and they don't even have to know their usernames or
33             passwords.
34              
35             =head1 SYNOPSIS
36              
37             use Google::SAML::Response;
38             use CGI;
39              
40             # get SAMLRequest parameter:
41             my $req = CGI->new->param('SAMLRequest');
42              
43             # authenticate user
44             ...
45              
46             # find our user's login for Google
47             ...
48              
49             # Generate SAML response
50             my $saml = Google::SAML::Response->new( {
51             key => $key,
52             login => $login,
53             request => $req
54             } );
55             my $xml = $saml->get_response_xml;
56              
57             # Alternatively, send a HTML page to the client that will redirect
58             # her to Google. You have to extract the RelayState param from the cgi
59             # environment first.
60              
61             print $saml->get_google_form( $relayState );
62              
63             =head1 PREREQUISITES
64              
65             You will need the following modules installed:
66              
67             =over
68              
69             =item * L
70              
71             =item * L
72              
73             =item * L
74              
75             =item * L
76              
77             =item * L
78              
79             =item * L
80              
81             =back
82              
83             =head1 RESOURCES
84              
85             =over
86              
87             =item XML-Signature Syntax and Processing
88              
89             L
90              
91             =item Google-Documentation on SSO and SAML
92              
93             L
94              
95             =item XML Security Library
96              
97             L
98              
99             =back
100              
101             =head1 METHODS
102              
103             =cut
104              
105 5     5   649946 use strict;
  5         13  
  5         196  
106 5     5   28 use warnings;
  5         12  
  5         183  
107              
108 5     5   5769 use Crypt::OpenSSL::RSA;
  5         59646  
  5         223  
109 5     5   6549 use MIME::Base64;
  5         5149  
  5         432  
110 5     5   9485 use Digest::SHA qw/ sha1 /;
  5         22927  
  5         528  
111 5     5   4893 use Date::Format;
  5         68414  
  5         474  
112 5     5   6869 use Compress::Zlib;
  5         539505  
  5         2045  
113 5     5   3042 use Google::SAML::Request;
  0            
  0            
114             use Carp;
115             use HTML::Entities;
116              
117              
118             our $VERSION = '0.14';
119              
120             =head2 new
121              
122             Creates a new object and needs to have all parameters needed to generate
123             the signed xml later on. Parameters are passed in as a hash-reference.
124              
125             =head3 Required parameters
126              
127             =over
128              
129             =item * request
130              
131             The SAML request, base64-encoded and all, just as retrieved from the GET
132             request your user contacted you with (make sure that it's not url-encoded, though)
133              
134             =item * key
135              
136             The path to your private key that will be used to sign the response. Currently,
137             only RSA and DSA keys without pass phrases are supported. B: To handle DSA keys,
138             the module L needs to be installed. However,
139             it is not listed as a requirement in the Makefile for Google::SAML::Response, so make
140             sure it really is installed before using DSA keys.
141              
142             =item * login
143              
144             Your user's login name with Google
145              
146             =back
147              
148             =head3 Optional parameters
149              
150             =over
151              
152             =item * ttl
153              
154             Time to live: Number of seconds your response should be valid. Default is two minutes.
155              
156             =item * canonicalizer
157              
158             The name of the module that will be used to canonicalize parts of our xml. Currently,
159             L and L are
160             supported. L is the default.
161              
162             =back
163              
164             =cut
165              
166              
167             sub new {
168             my $class = shift;
169              
170             my $params = shift;
171             my $self = {};
172              
173             foreach my $required ( qw/ request key login / ) {
174             if ( exists $params->{ $required } ) {
175             $self->{ $required } = $params->{ $required };
176             }
177             else {
178             confess "You need to provide the $required parameter!";
179             }
180             }
181              
182             bless $self, $class;
183              
184             my $request = Google::SAML::Request->new_from_string( $self->{ request } );
185              
186             if ( $request && $self->_load_key ) {
187             $self->{ service_url } = $request->AssertionConsumerServiceURL;
188             $self->{ request_id } = $request->ID;
189             $self->{ ttl } = ( exists $params->{ ttl } ) ? $params->{ ttl } : 60*2;
190             $self->{ canonicalizer } = exists $params->{ canonicalizer }
191             ? $params->{ canonicalizer }
192             : 'XML::CanonicalizeXML';
193              
194             return $self;
195             }
196             else {
197             return;
198             }
199              
200             }
201              
202              
203             sub _load_dsa_key {
204             my $self = shift;
205             my $key_text = shift;
206              
207             eval {
208             require Crypt::OpenSSL::DSA;
209             };
210              
211             confess "Crypt::OpenSSL::DSA needs to be installed so that we can handle DSA keys." if $@;
212              
213             my $dsa_key = Crypt::OpenSSL::DSA->read_priv_key_str( $key_text );
214              
215             if ( $dsa_key ) {
216             $self->{ key_obj } = $dsa_key;
217             my $g = encode_base64( $dsa_key->get_g, '' );
218             my $p = encode_base64( $dsa_key->get_p, '' );
219             my $q = encode_base64( $dsa_key->get_q, '' );
220             my $y = encode_base64( $dsa_key->get_pub_key, '' );
221              
222             $self->{ KeyInfo } = "

$p

$q$g$y
";
223             $self->{ key_type } = 'dsa';
224             }
225             else {
226             confess "did not get a new Crypt::OpenSSL::RSA object";
227             }
228             }
229              
230              
231             sub _load_rsa_key {
232             my $self = shift;
233             my $key_text = shift;
234              
235             my $rsaKey = Crypt::OpenSSL::RSA->new_private_key( $key_text );
236              
237             if ( $rsaKey ) {
238             $rsaKey->use_pkcs1_padding;
239             $self->{ key_obj } = $rsaKey;
240              
241             my $bigNum = ( $rsaKey->get_key_parameters )[ 1 ];
242             my $bin = $bigNum->to_bin;
243             my $exp = encode_base64( $bin, '' );
244              
245             $bigNum = ( $rsaKey->get_key_parameters )[ 0 ];
246             $bin = $bigNum->to_bin;
247             my $mod = encode_base64( $bin, '' );
248             $self->{ KeyInfo } = "$mod$exp";
249             $self->{ key_type } = 'rsa';
250             }
251             else {
252             confess "did not get a new Crypt::OpenSSL::RSA object";
253             }
254             }
255              
256              
257             sub _load_key {
258             my $self = shift;
259             my $file = $self->{ key };
260              
261             if ( open my $KEY, '<', $file ) {
262             my $text = '';
263             local $/ = undef;
264             $text = <$KEY>;
265             close $KEY;
266              
267             if ( $text =~ m/BEGIN ([DR]SA) PRIVATE KEY/ ) {
268             my $key_used = $1;
269              
270             if ( $key_used eq 'RSA' ) {
271             $self->_load_rsa_key( $text );
272             }
273             else {
274             $self->_load_dsa_key( $text );
275             }
276              
277             return 1;
278             }
279             else {
280             confess "Could not detect type of key $file.";
281             }
282             }
283             else {
284             confess "Could not load key $file: $!";
285             }
286              
287             return;
288             }
289              
290              
291             =head2 get_response_xml
292              
293             Generate the signed response xml and return it as a string
294              
295             The method does what the w3c tells us to do (L):
296              
297             =over
298              
299             3.1.1 Reference Generation
300              
301             For each data object being signed:
302              
303             1. Apply the Transforms, as determined by the application, to the data object.
304              
305             2. Calculate the digest value over the resulting data object.
306              
307             3. Create a Reference element, including the (optional) identification of the data object, any (optional) transform elements, the digest algorithm and the DigestValue. (Note, it is the canonical form of these references that are signed in 3.1.2 and validated in 3.2.1 .)
308              
309             3.1.2 Signature Generation
310              
311             1. Create SignedInfo element with SignatureMethod, CanonicalizationMethod and Reference(s).
312              
313             2. Canonicalize and then calculate the SignatureValue over SignedInfo based on algorithms specified in SignedInfo.
314              
315             3. Construct the Signature element that includes SignedInfo, Object(s) (if desired, encoding may be different than that used for signing), KeyInfo (if required), and SignatureValue.
316              
317             =back
318              
319             =cut
320              
321             sub get_response_xml {
322             my $self = shift;
323              
324             # This is the xml response without any signatures or digests:
325             my $xml = $self->_response_xml;
326              
327             # We now calculate the SHA1 digest of the canoncial response xml
328             my $canonical = $self->_canonicalize_xml( $xml );
329              
330             my $bin_digest = sha1( $canonical );
331             my $digest = encode_base64( $bin_digest, '' );
332              
333             # Create a xml fragment containing the digest:
334             my $digest_xml = $self->_reference_xml( $digest );
335              
336             # create a xml fragment consisting of the SignedInfo element
337             my $signed_info = $self->_signedinfo_xml( $digest_xml );
338              
339             # We now calculate a signature over the canonical SignedInfo element
340              
341             $canonical = $self->_canonicalize_xml( $signed_info );
342             my $signature;
343              
344             if ( $self->{ key_type } eq 'dsa' ) {
345             my $sig = $self->{ key_obj }->do_sign( sha1( $canonical ) );
346             $signature = encode_base64( $sig->get_r . $sig->get_s );
347             }
348             else {
349             my $bin_signature = $self->{ key_obj }->sign( $canonical );
350             $signature = encode_base64( $bin_signature, "\n" );
351             }
352              
353             # With the signature value and the signedinfo element, we create
354             # a Signature element:
355             my $signature_xml = $self->_signature_xml( $signed_info, $signature );
356              
357             # Now insert the signature xml into our response xml
358             $xml =~ s//$signature_xml/;
359              
360             return "\n" . $xml;
361             }
362              
363              
364             sub _signature_xml {
365             my $self = shift;
366             my $signed_info = shift;
367             my $signature_value = shift;
368              
369             return qq{
370             $signed_info
371             $signature_value
372             $self->{ KeyInfo }
373             };
374             }
375              
376              
377             sub _signedinfo_xml {
378             my $self = shift;
379             my $digest_xml = shift;
380              
381             return qq{
382            
383            
384             $digest_xml
385             };
386             }
387              
388              
389             sub _reference_xml {
390             my $self = shift;
391             my $digest = shift;
392              
393             return qq{
394            
395            
396            
397            
398             $digest
399             };
400             }
401              
402              
403             sub _canonicalize_xml {
404             my $self = shift;
405             my $xml = shift;
406              
407             if ( $self->{ canonicalizer } eq 'XML::Canonical' ) {
408             require XML::Canonical;
409             my $xmlcanon = XML::Canonical->new( comments => 1 );
410             return $xmlcanon->canonicalize_string( $xml );
411             }
412             elsif ( $self->{ canonicalizer } eq 'XML::CanonicalizeXML' ) {
413             require XML::CanonicalizeXML;
414             my $xpath = '(//. | //@* | //namespace::*)';
415             return XML::CanonicalizeXML::canonicalize( $xml, $xpath, [], 0, 0 );
416             }
417             else {
418             confess "Unknown XML canonicalizer module.";
419             }
420             }
421              
422              
423             sub _response_xml {
424             my $self = shift;
425              
426             # A 160-bit string containing a set of randomly generated characters.
427             # The ID MUST start with a character
428             my $response_id = sprintf 'GOSAML0d%04d', time, rand(10000);
429              
430             # A timestamp indicating the date and time that the SAML response was generated
431             # Bsp: 2006-08-17T10:05:29Z
432             # All SAML time values have the type xs:dateTime, which is built in to the W3C XML Schema Datatypes
433             # specification [Schema2], and MUST be expressed in UTC form, with no time zone component.
434             my $issue_instant = time2str( "%Y-%m-%dT%XZ", time, 'UTC' );
435              
436             # A 160-bit string containing a set of randomly generated characters.
437             my $assertion_id = sprintf 'GOSAML%010d%04d', time, rand(10000);
438              
439             # The acs url
440             my $assertion_url = $self->{ service_url };
441              
442             # The username for the authenticated user.
443             my $username = $self->{ login };
444              
445             # A timestamp identifying the date and time after which the SAML response is deemed invalid.
446             my $best_before = time2str( "%Y-%m-%dT%XZ", time + $self->{ ttl }, 'UTC' );
447              
448             # A timestamp indicating the date and time that you authenticated the user.
449             my $authn_instant = $issue_instant;
450              
451             my $request_id = $self->{ request_id };
452              
453             return
454             qq{
455            
456            
457            
458            
459             https://www.opensaml.org/IDP
460            
461             $username
462            
463            
464             Recipient="$assertion_url"
465             NotOnOrAfter="$best_before"
466             InResponseTo="$request_id"
467             />
468            
469            
470            
471            
472             $assertion_url
473            
474            
475            
476            
477             urn:oasis:names:tc:SAML:2.0:ac:classes:Password
478            
479            
480            
481            
482             };
483             }
484              
485              
486             =head2 get_google_form
487              
488             This function will give you a complete HTML page that you can send to clients
489             to have them redirected to Google. Note that former versions of this module
490             also included a Content-Type HTTP header. Fortunately, this is no longer the
491             case and you will have to send a "Content-Type: text/html" yourself using
492             whatever method your framework provides.
493              
494             After all the hi-tec stuff Google wants us to do to parse their request and
495             generate a response, this is where it gets low-tec and messy. We are supposed
496             to give clients a html page that contains a hidden form that uses Javascript
497             to post that form to Google. Ugly, but it works. The form will contain a textarea
498             containing the response xml and a textarea containing the relay state.
499              
500             Hence the only required argument: the RelayState parameter from the user's GET request
501              
502             =cut
503              
504             sub get_google_form {
505             my $self = shift;
506             my $rs = shift;
507              
508             my $url = $self->{ service_url };
509             my $output = "\n";
510             $output .= "\n";
511              
512             my $xml = $self->get_response_xml;
513             my $encoded_rs = encode_entities( $rs );
514              
515             $output .= qq|
516            
517            
518            
519            
520            
521            
522            
523             |;
524              
525             $output .= "\n";
526              
527             return $output;
528             }
529              
530              
531              
532             =head1 REMARKS
533              
534             Coming up with a valid response for a SAML-request is quite tricky. The simplest
535             way to go is to use the xmlsec1 program distributed with the XML Security Library.
536             Google seems to use that program itself. However, I wanted to have a perlish way
537             of creating the response. Testing your computed response is best done
538             against xmlsec1: If your response is stored in the file test.xml, you can simply do:
539              
540             xmlsec1 --verify --store-references --store-signatures test.xml > debug.txt
541              
542             This will give you a file debug.txt with lots of information, most importantly it
543             will give you the canonical xml versions of your response and the 'References'
544             element. If your canonical xml of these two elements isn't exactly like the one
545             in debug.txt, your response will not be valid.
546              
547             This brings us to another issue: XML-canonicalization. There are currently two
548             modules on CPAN that promise to do the work for you:
549             L and L.
550             Both can be used with Google::SAML::Response, however the default is to use the former
551             because it is much easier to install. However, the latter's interface is much
552             cleaner and Perl-like than the interface of the former.
553              
554             L uses L which has a
555             Makefile.PL that begs to be hacked because it insists on using the version
556             of gdome that was available when Makefile.PL was written (2003) and then it still doesn't
557             install without force. L is much easier
558             to install, you just have to have the libxml development files installed so it will
559             compile.
560              
561             =head1 TODO
562              
563             =over
564              
565             =item * Add support for encrypted keys
566              
567             =back
568              
569             =head1 SOURCE CODE
570              
571             This module has a github repository:
572              
573             https://github.com/mannih/Google-SAML-Response/
574              
575             =head1 AUTHOR
576              
577             Manni Heumann (saml at lxxi dot org)
578              
579             with the help of Jeremy Smith and Thiago Damasceno. Thank you!
580              
581             =head1 LICENSE
582              
583             Copyright (c) 2008-2013 Manni Heumann. All rights reserved.
584              
585             This program is free software; you can redistribute it and/or
586             modify it under the same terms as Perl itself.
587              
588             =cut
589              
590              
591             1;