File Coverage

blib/lib/Win32/IntAuth.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Win32::IntAuth;
2            
3 1     1   56998 use strict;
  1         3  
  1         40  
4 1     1   5 use warnings;
  1         2  
  1         65  
5            
6             require Exporter;
7            
8             our $VERSION = '0.20';
9             our @ISA = qw(Exporter);
10            
11 1     1   581 use Win32;
  0            
  0            
12             use Win32::API;
13            
14             =head1 NAME
15            
16             Win32::IntAuth - Perl extension for implementing
17             basic Windows Integrated Authentication
18            
19             =head1 SYNOPSIS
20            
21             # at client:
22             use Win32::IntAuth;
23             my $auth = Win32::IntAuth->new();
24            
25             # create a user token intended for the user the server process is running as
26             my $token = $auth->create_token('my_service_user@my_domain.org')
27             or die "couldn't create auth token, ", $auth->last_err_txt();
28             # now transfer the token to the server process
29            
30            
31             # at server:
32             # receive the token from client, then:
33            
34             use Win32::IntAuth;
35             my $auth = Win32::IntAuth->new();
36            
37             # the service user will need the user rights
38             # SeAssignPrimaryTokenPrivilege and SeImpersonatePrivilege
39             # and needs to be trusted for delegation in ActiveDirectory
40            
41             # impersonate the user that created the token
42             $auth->impersonate($token)
43             or die "couldn't impersonate user, ", $auth->last_err_txt();
44            
45             print 'Hooray user ', $auth->get_username(), " authenticated!\n";
46            
47             # now do something as the impersonated user
48            
49             # revert back to standard server context
50             $auth->revert()
51            
52             =head1 DESCRIPTION
53            
54             This module encapsulates (with Win32::API) the SSPI-API functions that are necessary
55             to authenticate and impersonate remote users from an already existing
56             session without additional specification of username and password.
57            
58             The module does not handle transport of the created user token to the
59             server process or service nor does it provise routines for further
60             evaluation of user rights or group memberships.
61            
62             The outline provided in the synopsis should be enough to get you
63             started. For details please look at the SSPI docs.
64            
65             L (as of 5/2008)
66            
67             =head2 EXPORT
68            
69             None by default. Only for calling the SSPI functions directly
70             via C<_sspi_call()> the constants can be imported with:
71            
72             use Win32::IntAuth qw/:constants/;
73            
74             But to do that you will have to look at the implementation.
75             May the source be with you :-).
76            
77             =cut
78            
79             # constants
80             my %err_txt;
81             my %constant_hash;
82            
83             BEGIN {
84            
85             %constant_hash = (
86             SECBUFFER_EMPTY => 0x00000000,
87             SECBUFFER_DATA => 0x00000001,
88             SECBUFFER_TOKEN => 0x00000002,
89             SECBUFFER_PKG_PARAMS => 0x00000003,
90             SECBUFFER_MISSING => 0x00000004,
91             SECBUFFER_EXTRA => 0x00000005,
92             SECBUFFER_STREAM_TRAILER => 0x00000006,
93             SECBUFFER_STREAM_HEADER => 0x00000007,
94             SECBUFFER_NEGOTIATION_INFO => 0x00000008,
95            
96             SECURITY_NATIVE_DREP => 0x00000010,
97             SECURITY_NETWORK_DREP => 0x00000000,
98            
99             SECPKG_CRED_INBOUND => 0x00000001,
100             SECPKG_CRED_OUTBOUND => 0x00000002,
101             SECPKG_CRED_BOTH => 0x00000003,
102            
103             ISC_REQ_DELEGATE => 0x00000001,
104             ISC_REQ_MUTUAL_AUTH => 0x00000002,
105             ISC_REQ_REPLAY_DETECT => 0x00000004,
106             ISC_REQ_SEQUENCE_DETECT => 0x00000008,
107             ISC_REQ_CONFIDENTIALITY => 0x00000010,
108             ISC_REQ_USE_SESSION_KEY => 0x00000020,
109             ISC_REQ_PROMPT_FOR_CREDS => 0x00000040,
110             ISC_REQ_USE_SUPPLIED_CREDS => 0x00000080,
111             ISC_REQ_ALLOCATE_MEMORY => 0x00000100,
112             ISC_REQ_USE_DCE_STYLE => 0x00000200,
113             ISC_REQ_DATAGRAM => 0x00000400,
114             ISC_REQ_CONNECTION => 0x00000800,
115             ISC_REQ_CALL_LEVEL => 0x00001000,
116             ISC_REQ_EXTENDED_ERROR => 0x00004000,
117             ISC_REQ_STREAM => 0x00008000,
118             ISC_REQ_INTEGRITY => 0x00010000,
119             ISC_REQ_IDENTIFY => 0x00020000,
120             ISC_REQ_NULL_SESSION => 0x00040000,
121            
122             ASC_REQ_DELEGATE => 0x00000001,
123             ASC_REQ_MUTUAL_AUTH => 0x00000002,
124             ASC_REQ_REPLAY_DETECT => 0x00000004,
125             ASC_REQ_SEQUENCE_DETECT => 0x00000008,
126             ASC_REQ_CONFIDENTIALITY => 0x00000010,
127             ASC_REQ_USE_SESSION_KEY => 0x00000020,
128             ASC_REQ_ALLOCATE_MEMORY => 0x00000100,
129             ASC_REQ_USE_DCE_STYLE => 0x00000200,
130             ASC_REQ_DATAGRAM => 0x00000400,
131             ASC_REQ_CONNECTION => 0x00000800,
132             ASC_REQ_CALL_LEVEL => 0x00001000,
133             ASC_REQ_EXTENDED_ERROR => 0x00008000,
134             ASC_REQ_STREAM => 0x00010000,
135             ASC_REQ_INTEGRITY => 0x00020000,
136             ASC_REQ_LICENSING => 0x00040000,
137             ASC_REQ_IDENTIFY => 0x00080000,
138             ASC_REQ_ALLOW_NULL_SESSION => 0x00100000,
139            
140             SECPKG_ATTR_SIZES => 0x00000000,
141             SECPKG_ATTR_NAMES => 0x00000001,
142             SECPKG_ATTR_LIFESPAN => 0x00000002,
143             SECPKG_ATTR_DCE_INFO => 0x00000003,
144             SECPKG_ATTR_STREAM_SIZES => 0x00000004,
145             SECPKG_ATTR_KEY_INFO => 0x00000005,
146             SECPKG_ATTR_AUTHORITY => 0x00000006,
147             SECPKG_ATTR_PROTO_INFO => 0x00000007,
148             SECPKG_ATTR_PASSWORD_EXPIRY => 0x00000008,
149             SECPKG_ATTR_SESSION_KEY => 0x00000009,
150             SECPKG_ATTR_PACKAGE_INFO => 0x0000000A,
151             SECPKG_ATTR_NATIVE_NAMES => 0x0000000D,
152            
153             SEC_E_OK => 0x00000000,
154             SEC_E_INSUFFICIENT_MEMORY => 0x80090300,
155             SEC_E_INVALID_HANDLE => 0x80090301,
156             SEC_E_UNSUPPORTED_FUNCTION => 0x80090302,
157             SEC_E_TARGET_UNKNOWN => 0x80090303,
158             SEC_E_INTERNAL_ERROR => 0x80090304,
159             SEC_E_SECPKG_NOT_FOUND => 0x80090305,
160             SEC_E_NOT_OWNER => 0x80090306,
161             SEC_E_CANNOT_INSTALL => 0x80090307,
162             SEC_E_INVALID_TOKEN => 0x80090308,
163             SEC_E_CANNOT_PACK => 0x80090309,
164             SEC_E_QOP_NOT_SUPPORTED => 0x8009030A,
165             SEC_E_NO_IMPERSONATION => 0x8009030B,
166             SEC_E_LOGON_DENIED => 0x8009030C,
167             SEC_E_UNKNOWN_CREDENTIALS => 0x8009030D,
168             SEC_E_NO_CREDENTIALS => 0x8009030E,
169             SEC_E_MESSAGE_ALTERED => 0x8009030F,
170             SEC_E_OUT_OF_SEQUENCE => 0x80090310,
171             SEC_E_NO_AUTHENTICATING_AUTHORITY => 0x80090311,
172             SEC_I_CONTINUE_NEEDED => 0x00090312,
173             SEC_I_COMPLETE_NEEDED => 0x00090313,
174             SEC_I_COMPLETE_AND_CONTINUE => 0x00090314,
175             SEC_I_LOCAL_LOGON => 0x00090315,
176             SEC_E_BAD_PKGID => 0x80090316,
177             SEC_E_CONTEXT_EXPIRED => 0x80090317,
178             SEC_E_INCOMPLETE_MESSAGE => 0x80090318,
179             SEC_E_INCOMPLETE_CREDENTIALS => 0x80090320,
180             SEC_E_BUFFER_TOO_SMALL => 0x80090321,
181             SEC_I_INCOMPLETE_CREDENTIALS => 0x00090320,
182             SEC_I_RENEGOTIATE => 0x00090321,
183             SEC_E_WRONG_PRINCIPAL => 0x80090322,
184            
185             ERROR_NO_SUCH_DOMAIN => 0x0000054B,
186             ERROR_MORE_DATA => 0x000000EA,
187             ERROR_NONE_MAPPED => 0x00000534,
188             );
189            
190             # create lookup hash for error names
191             %err_txt = map {
192             sprintf('0x%08x', $constant_hash{$_}) => $_
193             } grep {
194             /^SEC_[EI]/
195             } keys %constant_hash;
196            
197             }
198            
199             use constant \%constant_hash;
200            
201             our @EXPORT_OK = keys %constant_hash;
202             our %EXPORT_TAGS = (
203             constants => [keys %constant_hash],
204             );
205            
206             =head1 CONSTRUCTOR
207            
208             =head2 new
209            
210             my $auth = Win32::IntAuth->new([debug => 1]);
211            
212             Creates a new Win32::IntAuth object. By setting the C
213             parameter, you'll get a bit of debugging information on STDERR.
214            
215             =cut
216             sub new {
217             my($class, %args) = @_;
218            
219             my $self = bless({}, $class);
220             $self->_init(%args);
221            
222             return($self);
223             }
224            
225            
226             sub _init {
227             my($self, %args) = @_;
228            
229             $self->{$_} = $args{$_} for keys %args;
230            
231             warn "\n" if $self->{debug};
232            
233             return(1);
234             }
235            
236             =head1 METHODS
237            
238             All methods return undef on error. Call C or C
239             to get the error code respectively a short description.
240            
241             =head2 last_err
242            
243             Returns the last error code from a method call.
244            
245             =cut
246             sub last_err {
247             return($_[0]->{last_err} || '0E0');
248             }
249            
250             =head2 last_err_txt
251            
252             Returns the last error text from a method call.
253            
254             =cut
255             sub last_err_txt {
256             return($_[0]->{last_err_txt} || 'UNKNOWN ERRCODE');
257             }
258            
259            
260             my %sspi = (
261             AcquireCredentialsHandle => new Win32::API(
262             "Secur32.dll",
263             "AcquireCredentialsHandle",
264             [qw/P P N P P P P P P/],
265             'I',
266             ),
267             InitializeSecurityContext => new Win32::API(
268             "Secur32.dll",
269             "InitializeSecurityContext",
270             [qw/P P P N N N P N P P P P/],
271             'I',
272             ),
273             AcceptSecurityContext => new Win32::API(
274             "Secur32.dll",
275             "AcceptSecurityContext",
276             [qw/P P P N N P P P P/],
277             'I',
278             ),
279             CompleteAuthToken => new Win32::API(
280             "Secur32.dll",
281             "CompleteAuthToken",
282             [qw/P P/],
283             'I',
284             ),
285             ImpersonateSecurityContext => new Win32::API(
286             "Secur32.dll",
287             "ImpersonateSecurityContext",
288             [qw/P/],
289             'I',
290             ),
291             RevertSecurityContext => new Win32::API(
292             "Secur32.dll",
293             "RevertSecurityContext",
294             [qw/P/],
295             'I',
296             ),
297             GetUserNameEx => new Win32::API(
298             "Secur32.dll",
299             "GetUserNameEx",
300             [qw/N P P/],
301             'I',
302             ),
303             FreeContextBuffer => new Win32::API(
304             "Secur32.dll",
305             "FreeContextBuffer",
306             [qw/P/],
307             'I',
308             ),
309             FreeCredentialsHandle => new Win32::API(
310             "Secur32.dll",
311             "FreeCredentialsHandle",
312             [qw/P/],
313             'I',
314             ),
315             );
316            
317            
318             sub _sspi_call {
319             my $self = shift;
320             my $fname = shift;
321            
322             warn "calling $fname with "
323             . scalar(@_)
324             . " parameters:\n"
325             . join("\n", @_)
326             if $self->{debug};
327            
328             {
329             no warnings;
330             $self->{last_err} = $sspi{$fname}->Call(@_);
331             }
332            
333             my $rc_hex = sprintf('0x%08x', $self->{last_err});
334            
335             if ( $fname eq 'GetUserNameEx' ) {
336             return($self->{last_err}) if $self->{last_err};
337             $self->{last_err} = Win32::GetLastError();
338             $rc_hex = sprintf('0x%08x', $self->{last_err});
339             $self->{last_err_txt} = $err_txt{$rc_hex};
340             return;
341             }
342            
343             $self->{last_err_txt} = $err_txt{$rc_hex};
344            
345             warn "$fname -> ", $self->{last_err_txt}, "\n" if $self->{debug};
346            
347             return if $self->{last_err} < 0;
348            
349             return($self->{last_err} || '0E0');
350             }
351            
352            
353             =head2 create_token($spn [, $mechanism [, $token]])
354            
355             Create and returns a token for the current process user ready to be
356             sent to the server service that should authenticate/impersonate the
357             client.
358            
359             The mechanism defaults to "Negotiate".
360            
361             C<$spn> has to be the UPN (User Principal Name) of the user the service
362             is running as (or a dedicated Service Principal Name SPN).
363            
364             C<$token> is only used in a second call to create_token in case of a
365             continue request. It must contain the token sent back by the server.
366            
367             =cut
368             sub create_token {
369             my($self, $spn, $mechanism, $token) = @_;
370            
371             $mechanism ||= 'Negotiate';
372            
373             # if we didn't receive a token, then acquire a new credentials handle
374             unless ( $token ) {
375             my $Package = $mechanism . "\x00";
376             my $pExpiry = pack('LL', 0, 0);
377             $self->{hCred} = pack('LL', 0, 0);
378             my $Principal = undef;
379            
380             $self->_sspi_call(
381             'AcquireCredentialsHandle',
382             $Principal,
383             $Package,
384             SECPKG_CRED_OUTBOUND,
385             0,
386             0,
387             0,
388             0,
389             $self->{hCred},
390             $pExpiry,
391             ) or return;
392             }
393            
394             $self->{Context} = pack('L L', 0, 0)
395             unless $self->{Context} and $self->{Context} =~ /[^\0]/;
396            
397             my $pContextAttr = pack('L', 0);
398            
399             my $in_buf_size = length($token);
400             my $sec_inbuf = pack("L L P$in_buf_size", $in_buf_size, SECBUFFER_TOKEN, $token);
401             my $pInput = pack('L L P', 0, 1, $sec_inbuf);
402            
403             my $out_buf_size = 4096;
404             my $out_buf = "\x00" x $out_buf_size;
405             my $sec_outbuf = pack("L L P$out_buf_size", $out_buf_size, SECBUFFER_TOKEN, $out_buf);
406             my $pOutput = pack('L L P', 0, 1, $sec_outbuf);
407            
408             my $pExpiry = pack('LL', 0, 0);
409            
410             $self->_sspi_call(
411             'InitializeSecurityContext',
412             $self->{hCred},
413             $token ? $self->{Context} : 0,
414             $spn,
415             0,
416             0,
417             SECURITY_NATIVE_DREP,
418             $token ? $pInput : 0,
419             0,
420             $self->{Context},
421             $pOutput,
422             $pContextAttr,
423             $pExpiry,
424             ) or return;
425            
426             $self->{continue}
427             = $self->{last_err} == SEC_I_CONTINUE_NEEDED
428             ? 1
429             : 0;
430            
431             # retrieve new output buffer size and trim the buffer to that size
432             $out_buf_size = unpack('L', $sec_outbuf);
433             $out_buf = substr($out_buf, 0, $out_buf_size);
434            
435             return($out_buf);
436             }
437            
438            
439             =head2 get_token_upn($token [, $spn])
440            
441             Combines C, C and
442             C for simple authentication without acting on behalf of the
443             user.
444            
445             Returns the fully qualified user name (UPN) of the token user.
446            
447             =cut
448             sub get_token_upn {
449             my($self, $token) = @_;
450            
451             $self->impersonate($token) or return;
452            
453             my $upn = $self->get_username() or return;
454            
455             $self->revert() or return;
456            
457             return($upn);
458             }
459            
460            
461             =head2 impersonate($token [, $spn])
462            
463             Impersonates the user that has created the token in the client
464             session.
465            
466             The client user has to have the appropriate rights. (At least network
467             logon rights on the server the service is running at).
468            
469             The service user has to have at least the user rights
470             SeAssignPrimaryTokenPrivilege and SeImpersonatePrivilege and needs to
471             be trusted for delegation in ActiveDirectory.
472            
473             If the client creates the token for an ServicePrincipalName the server
474             must call impersonate with the same SPN in C<$spn>. Otherwise the UPN
475             of the user the service is running as has to be used.
476            
477             You will have to check continue_needed() after a call to
478             impersonate(). If it is needed, impersonate will have returned a token
479             to be sent back to the client. The client then has to make a second
480             call to create_token with the server token as second parameter.
481            
482             Proceed with the second client token as before.
483            
484             =cut
485             sub impersonate {
486             my($self, $token, $spn) = @_;
487            
488             my $Package = "Negotiate" . "\x00";
489             my $pExpiry = pack('L L', 0, 0);
490             $self->{hCred} = pack('L L', 0, 0);
491            
492             $self->_sspi_call(
493             'AcquireCredentialsHandle',
494             $spn ? $spn . "\x00" : 0,
495             $Package,
496             SECPKG_CRED_INBOUND,
497             0,
498             0,
499             0,
500             0,
501             $self->{hCred},
502             $pExpiry,
503             ) or return;
504            
505             $self->{Context} = pack('L L', 0, 0)
506             unless $self->{Context} and $self->{Context} =~ /[^\0]/;
507            
508             my $pContextAttr = pack('L', 0);
509             my $buf_size = 4096;
510             my $sec_inbuf = pack("L L P$buf_size", $buf_size, SECBUFFER_TOKEN, $token);
511             my $pInput = pack('L L P', 0, 1, $sec_inbuf);
512             my $out_buf = ' ' x $buf_size;
513             my $sec_outbuf = pack("L L P$buf_size", $buf_size, SECBUFFER_TOKEN, $out_buf);
514             my $pOutput = pack('L L P', 0, 1, $sec_outbuf);
515             $pExpiry = pack('L L', 0, 0);
516            
517             $self->_sspi_call(
518             'AcceptSecurityContext',
519             $self->{hCred},
520             $self->{Context} =~ /[^\0]/ ? $self->{Context} : 0,
521             $pInput,
522             0,
523             SECURITY_NATIVE_DREP,
524             $self->{Context},
525             $pOutput,
526             $pContextAttr,
527             $pExpiry,
528             ) or return;
529            
530             $self->{continue} = $self->{last_err} == SEC_I_CONTINUE_NEEDED;
531            
532             return($out_buf) if $self->{continue};
533            
534             $self->_sspi_call(
535             'CompleteAuthToken',
536             $self->{Context},
537             $pOutput,
538             ) if $self->{last_err} == SEC_I_COMPLETE_NEEDED;
539            
540             $self->_sspi_call(
541             'ImpersonateSecurityContext',
542             $self->{Context},
543             ) or return;
544            
545             return('0E0');
546             }
547            
548            
549             =head2 continue_needed()
550            
551             Will return 1 if the last call to C returned a request to
552             ask the client for a second token.
553            
554             =cut
555             sub continue_needed {
556             return($_[0]->{continue} || 0);
557             }
558            
559            
560             =head2 revert()
561            
562             Ends impersonation and reverts back to the original server context.
563            
564             =cut
565             sub revert {
566             my($self) = @_;
567            
568             $self->_sspi_call(
569             'RevertSecurityContext',
570             $self->{Context},
571             ) or return;
572            
573             return('0E0');
574             }
575            
576             =head2 get_username()
577            
578             Returns the fully qualified user name (UPN) of the current user. If
579             called after C it will return the impersonated user's
580             UPN.
581            
582             =cut
583             sub get_username {
584             my($self) = @_;
585            
586             my $siz = 256;
587             my $name = ' ' x $siz;
588             my $lsiz = pack('L', $siz);
589             my $rc = $self->_sspi_call(
590             'GetUserNameEx',
591             8,
592             $name,
593             $lsiz,
594             ) or return;
595            
596             $name =~ s/\0.*$//;
597            
598             return($name);
599             }
600            
601            
602             1;
603            
604            
605             =head1 AUTHOR
606            
607             Thomas Kratz Etomk@cpan.orgE
608            
609             =head1 COPYRIGHT AND LICENSE
610            
611             Copyright (C) 2011 by Thomas Kratz
612            
613             This library is free software; you can redistribute it and/or modify
614             it under the same terms as Perl itself, either Perl version 5.8.8 or,
615             at your option, any later version of Perl 5 you may have available.
616            
617             =cut