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   11402 use 5.008;
  3         13  
2 3     3   17 use strict;
  3         7  
  3         70  
3 3     3   17 use warnings;
  3         7  
  3         177  
4              
5             package Authen::SCRAM::Server;
6             # ABSTRACT: RFC 5802 SCRAM Server
7              
8             our $VERSION = '0.009';
9              
10 3     3   669 use Moo 1.001000;
  3         13996  
  3         24  
11              
12 3     3   3234 use Authen::SASL::SASLprep qw/saslprep/;
  3         99569  
  3         213  
13 3     3   23 use Carp qw/croak/;
  3         8  
  3         145  
14 3     3   725 use Crypt::URandom qw/urandom/;
  3         5163  
  3         160  
15 3     3   24 use Encode qw/encode_utf8/;
  3         7  
  3         132  
16 3     3   21 use MIME::Base64 qw/decode_base64/;
  3         6  
  3         157  
17 3     3   22 use PBKDF2::Tiny 0.003 qw/derive digest_fcn hmac/;
  3         67  
  3         184  
18 3     3   731 use Types::Standard qw/Str Num CodeRef Bool/;
  3         101192  
  3         64  
19              
20 3     3   4047 use namespace::clean;
  3         12634  
  3         26  
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 40249 my ( $self, $msg ) = @_;
129 17         389 $self->_clear_session;
130              
131 17         116 my ( $cbind, $authz, $c_1_bare, $mext, @params ) = $msg =~ $self->_client_first_re;
132              
133 17 100       55 if ( !defined $cbind ) {
134 9         738 croak "SCRAM client-first-message could not be parsed";
135             }
136 8 50       31 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       22 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       23 push @params, $authz if defined $authz;
146 8         34 $self->_parse_to_session(@params);
147 8         31 $self->_extend_nonce;
148              
149 8         131 my $name = $self->_get_session('n');
150 8         114 my ( $salt, $stored_key, $server_key, $iters ) = $self->credential_cb->($name);
151              
152 8 100       103 if ( !defined $salt ) {
153 1         175 croak "SCRAM client-first-message had unknown user '$name'";
154             }
155              
156             $self->_set_session(
157 7         27 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         28 my $reply = $self->_join_reply(qw/r s i/);
165 7         27 $self->_set_session( _s1 => $reply );
166              
167 7         31 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 6680 my ( $self, $msg ) = @_;
189              
190 7         38 my ( $c2wop, @params ) = $msg =~ $self->_client_final_re;
191 7         34 $self->_set_session( _c2wop => $c2wop );
192              
193 7 50       27 if ( !defined $c2wop ) {
194 0         0 croak "SCRAM client-first-message could not be parsed";
195             }
196              
197             # confirm nonce
198 7         33 my $original_nonce = $self->_get_session("r");
199 7         74 $self->_parse_to_session(@params);
200 7         24 my $joint_nonce = $self->_get_session("r");
201 7 50       73 unless ( $joint_nonce eq $original_nonce ) {
202 0         0 croak "SCRAM client-final-message nonce invalid";
203             }
204              
205             # confirm channel bindings
206 7         28 my $cbind =
207             $self->_base64( encode_utf8( $self->_construct_gs2( $self->_get_session("a") ) ) );
208 7 50       29 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         99 my $client_sig = $self->_client_sig;
215 7         138 my $proof = decode_base64( $self->_get_session("p") );
216 7         78 my $client_key = $proof ^ $client_sig;
217 7         125 my $computed_key = $self->_digest_fcn->($client_key);
218 7         194 my $name = $self->_get_session("n");
219              
220 7 100       67 if ( !$self->_const_eq_fcn->( $computed_key, $self->_get_session("_stored_key") ) ) {
221 1         176 croak "SCRAM authentication for user '$name' failed";
222             }
223              
224 6 100       199 if ( my $authz = $self->_get_session("a") ) {
225 3 100       41 $self->auth_proxy_cb->( $name, $authz )
226             or croak("SCRAM authentication failed; '$name' not authorized to act as '$authz'");
227             }
228              
229 5         64 $self->_set_session( _proof_ok => 1 );
230              
231 5         17 my $server_sig =
232             $self->_hmac_fcn->( $self->_get_session('_server_key'), $self->_auth_msg );
233              
234 5         85 $self->_set_session( v => $self->_base64($server_sig) );
235              
236 5         19 $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 6822 my ($self) = @_;
251 8 100       37 return '' unless $self->_get_session("_proof_ok");
252 5         70 my $authz = $self->_get_session("a");
253 5 100 66     69 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__