File Coverage

blib/lib/Authen/SCRAM/Server.pm
Criterion Covered Total %
statement 80 85 94.1
branch 21 26 80.7
condition 2 3 66.6
subroutine 15 15 100.0
pod 3 3 100.0
total 121 132 91.6


line stmt bran cond sub pod time code
1 3     3   10566 use 5.008;
  3         9  
2 3     3   16 use strict;
  3         6  
  3         73  
3 3     3   18 use warnings;
  3         6  
  3         134  
4              
5             package Authen::SCRAM::Server;
6             # ABSTRACT: RFC 5802 SCRAM Server
7              
8             our $VERSION = '0.010';
9              
10 3     3   460 use Moo 1.001000;
  3         9243  
  3         25  
11              
12 3     3   2320 use Authen::SASL::SASLprep qw/saslprep/;
  3         63614  
  3         145  
13 3     3   18 use Carp qw/croak/;
  3         6  
  3         120  
14 3     3   427 use Crypt::URandom qw/urandom/;
  3         3283  
  3         142  
15 3     3   20 use Encode qw/encode_utf8/;
  3         6  
  3         125  
16 3     3   14 use MIME::Base64 qw/decode_base64/;
  3         8  
  3         139  
17 3     3   16 use PBKDF2::Tiny 0.003 qw/derive digest_fcn hmac/;
  3         58  
  3         139  
18 3     3   495 use Types::Standard qw/Str Num CodeRef Bool/;
  3         61777  
  3         39  
19              
20 3     3   3044 use namespace::clean;
  3         7982  
  3         22  
21              
22             with 'Authen::SCRAM::Role::Common';
23              
24             #--------------------------------------------------------------------------#
25             # public attributes
26             #--------------------------------------------------------------------------#
27              
28             #pod =attr credential_cb (required)
29             #pod
30             #pod This attribute must contain a code reference that takes a username (as a
31             #pod character string normalized by SASLprep) and returns the four user-credential
32             #pod parameters required by SCRAM: C, C, C, and
33             #pod C. The C, C and C must be
34             #pod provided as octets (i.e. B base64 encoded).
35             #pod
36             #pod If the username is unknown, it should return an empty list.
37             #pod
38             #pod ($salt, $stored_key, $server_key, $iterations) =
39             #pod $server->credential_cb->( $username );
40             #pod
41             #pod See L
42             #pod for details.
43             #pod
44             #pod =cut
45              
46             has credential_cb => (
47             is => 'ro',
48             isa => CodeRef,
49             required => 1,
50             );
51              
52             #pod =attr auth_proxy_cb
53             #pod
54             #pod If provided, this attribute must contain a code reference that takes an
55             #pod B username and a B username (both as character
56             #pod strings), and return a true value if the authentication username is permitted
57             #pod to act as the authorization username:
58             #pod
59             #pod $bool = $server->auth_proxy_cb->(
60             #pod $authentication_user, $authorization_user
61             #pod );
62             #pod
63             #pod It will only be all called if the authentication username has successfully
64             #pod authenticated. Both usernames will have been normalized via C with
65             #pod any transport encoding removed before being passed to this function.
66             #pod
67             #pod =cut
68              
69             has auth_proxy_cb => (
70             is => 'ro',
71             isa => CodeRef,
72             default => sub {
73             return sub { 1 }
74             },
75             );
76              
77             #--------------------------------------------------------------------------#
78             # provided by Authen::SCRAM::Role::Common
79             #--------------------------------------------------------------------------#
80              
81             with 'Authen::SCRAM::Role::Common';
82              
83             #pod =attr digest
84             #pod
85             #pod Name of a digest function available via L. Valid values are
86             #pod SHA-1, SHA-224, SHA-256, SHA-384, or SHA-512. Defaults to SHA-1.
87             #pod
88             #pod =attr nonce_size
89             #pod
90             #pod Size of the client-generated nonce, in bits. Defaults to 192.
91             #pod The server-nonce will be appended, so the final nonce size will
92             #pod be substantially larger.
93             #pod
94             #pod =attr skip_saslprep
95             #pod
96             #pod A boolean that defaults to false. If set to true, usernames and passwords will
97             #pod not be normalized through SASLprep. This is a deviation from the RFC5802 spec
98             #pod and is not recommended.
99             #pod
100             #pod =cut
101              
102             #--------------------------------------------------------------------------#
103             # private attributes
104             #--------------------------------------------------------------------------#
105              
106             has _proof_ok => (
107             is => 'ro',
108             isa => Bool,
109             writer => '_set_proof_ok',
110             );
111              
112             #--------------------------------------------------------------------------#
113             # public methods
114             #--------------------------------------------------------------------------#
115              
116             #pod =method first_msg
117             #pod
118             #pod $server_first_msg = $server->first_msg( $client_first_msg );
119             #pod
120             #pod This takes the C received from the client and returns the
121             #pod C string to be sent to the client to continue a SCRAM
122             #pod session. Calling this again will reset the internal state and initiate a new
123             #pod session. This will throw an exception should an error occur.
124             #pod
125             #pod =cut
126              
127             sub first_msg {
128 17     17 1 29894 my ( $self, $msg ) = @_;
129 17         373 $self->_clear_session;
130              
131 17         105 my ( $cbind, $authz, $c_1_bare, $mext, @params ) = $msg =~ $self->_client_first_re;
132              
133 17 100       54 if ( !defined $cbind ) {
134 9         710 croak "SCRAM client-first-message could not be parsed";
135             }
136 8 50       23 if ( $cbind eq 'p' ) {
137 0         0 croak
138             "SCRAM client-first-message required channel binding, but we do not support it";
139             }
140 8 50       15 if ( defined $mext ) {
141 0         0 croak
142             "SCRAM client-first-message required mandatory extension '$mext', but we do not support it";
143             }
144              
145 8 100       18 push @params, $authz if defined $authz;
146 8         28 $self->_parse_to_session(@params);
147 8         24 $self->_extend_nonce;
148              
149 8         95 my $name = $self->_get_session('n');
150 8         72 my ( $salt, $stored_key, $server_key, $iters ) = $self->credential_cb->($name);
151              
152 8 100       88 if ( !defined $salt ) {
153 1         185 croak "SCRAM client-first-message had unknown user '$name'";
154             }
155              
156             $self->_set_session(
157 7         21 s => $self->_base64($salt),
158             i => $iters,
159             _c1b => $c_1_bare,
160             _stored_key => $stored_key,
161             _server_key => $server_key
162             );
163              
164 7         22 my $reply = $self->_join_reply(qw/r s i/);
165 7         23 $self->_set_session( _s1 => $reply );
166              
167 7         53 return $reply;
168             }
169              
170             #pod =method final_msg
171             #pod
172             #pod $server_final_msg = $server->final_msg( $client_final_msg );
173             #pod
174             #pod This takes the C received from the client and returns the
175             #pod C string containing the verification signature to be sent
176             #pod to the client.
177             #pod
178             #pod If an authorization identity was provided by the client, it will confirm that
179             #pod the authenticating username is authorized to act as the authorization id using
180             #pod the L attribute.
181             #pod
182             #pod If the client credentials do not match or the authentication name is not
183             #pod authorized to act as the authorization name, then an exception will be thrown.
184             #pod
185             #pod =cut
186              
187             sub final_msg {
188 7     7 1 4023 my ( $self, $msg ) = @_;
189              
190 7         30 my ( $c2wop, @params ) = $msg =~ $self->_client_final_re;
191 7         31 $self->_set_session( _c2wop => $c2wop );
192              
193 7 50       19 if ( !defined $c2wop ) {
194 0         0 croak "SCRAM client-first-message could not be parsed";
195             }
196              
197             # confirm nonce
198 7         27 my $original_nonce = $self->_get_session("r");
199 7         66 $self->_parse_to_session(@params);
200 7         20 my $joint_nonce = $self->_get_session("r");
201 7 50       57 unless ( $joint_nonce eq $original_nonce ) {
202 0         0 croak "SCRAM client-final-message nonce invalid";
203             }
204              
205             # confirm channel bindings
206 7         20 my $cbind =
207             $self->_base64( encode_utf8( $self->_construct_gs2( $self->_get_session("a") ) ) );
208 7 50       21 if ( $cbind ne $self->_get_session("c") ) {
209 0         0 croak "SCRAM client-final-message channel binding didn't match";
210             }
211              
212             # confirm proof
213              
214 7         73 my $client_sig = $self->_client_sig;
215 7         114 my $proof = decode_base64( $self->_get_session("p") );
216 7         67 my $client_key = $proof ^ $client_sig;
217 7         97 my $computed_key = $self->_digest_fcn->($client_key);
218 7         149 my $name = $self->_get_session("n");
219              
220 7 100       59 if ( !$self->_const_eq_fcn->( $computed_key, $self->_get_session("_stored_key") ) ) {
221 1         168 croak "SCRAM authentication for user '$name' failed";
222             }
223              
224 6 100       16 if ( my $authz = $self->_get_session("a") ) {
225 3 100       54 $self->auth_proxy_cb->( $name, $authz )
226             or croak("SCRAM authentication failed; '$name' not authorized to act as '$authz'");
227             }
228              
229 5         51 $self->_set_session( _proof_ok => 1 );
230              
231 5         11 my $server_sig =
232             $self->_hmac_fcn->( $self->_get_session('_server_key'), $self->_auth_msg );
233              
234 5         80 $self->_set_session( v => $self->_base64($server_sig) );
235              
236 5         15 $self->_join_reply('v');
237             }
238              
239             #pod =method authorization_id
240             #pod
241             #pod $username = $client->authorization_id();
242             #pod
243             #pod This takes no arguments and returns the authorization identity resulting from
244             #pod the SCRAM exchange. This is the client-supplied authorization identity (if one
245             #pod was provided and validated) or else the successfully authenticated identity.
246             #pod
247             #pod =cut
248              
249             sub authorization_id {
250 8     8 1 4251 my ($self) = @_;
251 8 100       28 return '' unless $self->_get_session("_proof_ok");
252 5         51 my $authz = $self->_get_session("a");
253 5 100 66     62 return ( defined($authz) && length($authz) ) ? $authz : $self->_get_session("n");
254             }
255              
256             1;
257              
258              
259             # vim: ts=4 sts=4 sw=4 et:
260              
261             __END__