1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
#----------------------------------------------------------------------
# Fripost admin panel - ephemeral sessions
# Copyright © 2018 Fripost
# Copyright © 2018 Guilhem Moulin <guilhem@fripost.org>
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
package Fripost::Session v0.0.1;
use warnings;
use strict;
use Authen::SASL ();
use Net::LDAP::Constant qw/LDAP_SUCCESS LDAP_ALREADY_EXISTS/;
use Net::LDAP::Extension::Refresh ();
use Net::LDAP::Util "escape_dn_value";
use Crypt::URandom "urandom";
use Fripost ();
# new(Fripost object)
# Create a new ephemeral session from a Fripost object, and return
# suitable credentials for later SASL proxy authorization.
sub new($$) {
my ($class, $fp) = @_;
# don't base64-encode but hex-encode as the commonName is case-insensitive
my $id = unpack("H*", urandom(16));
my $dn = sprintf($fp->{_config}->{ldap}->{"session-authcDN"},
escape_dn_value($id));
# hex-encode the password too since we can't have NUL bytes in the SASL packet
my $password = unpack("H*", urandom(16));
my $authzid = $fp->whoami() // die;
die "Invalid identity: $authzid\n" unless $authzid =~ /\Adn:/;
my @attrs = (objectClass => [ qw/organizationalRole simpleSecurityObject dynamicObject/ ]);
# libsasl2 requires {CLEARTEXT} passwords, even for PLAIN, cf.
# https://openldap.org/lists/openldap-technical/201310/msg00007.html
# (not a big deal here though since our shared secrets are internal
# and ephemeral)
push @attrs, userPassword => ( "{CLEARTEXT}" . $password );
my $r = $fp->{_ldap}->add($dn, attrs => \@attrs);
if ($r->code == LDAP_ALREADY_EXISTS) {
# try to delete the entry (we're not allowed to modify existing entries)
my $r2 = $fp->{_ldap}->delete($dn);
$r = $fp->{_ldap}->add($dn, attrs => \@attrs)
if $r2->code == LDAP_SUCCESS;
}
$fp->croak("LDAP error code %i: %s\n", $r->code, $r->error)
unless $r->code == LDAP_SUCCESS;
my %creds = (authcid => $id, password => $password, authzid => $authzid);
bless \%creds, $class;
}
# authenticate(OPTION => VALUE, ..)
# Create a new Fripost object and return it after authentication
# (using SASL proxy authorization with the ephemeral credentials).
# If the "refresh" is set (the default), then TTL value of the entry
# on the backup is refreshed.
sub authenticate($%) {
my $self = shift;
my %conf = @_;
my $refresh = delete $conf{refresh} // 1;
my $authcid = sprintf($conf{ldap}->{"session-authcID"} // "%s",
$self->{authcid});
my $sasl = Authen::SASL::->new( mechanism => "PLAIN", callback => {
user => $authcid
, pass => $self->{password}
, authname => $self->{authzid}
}) or die "Creation of Authen::SASL object failed";
my $fp = Fripost::->new(%conf);
my $r = $fp->{_ldap}->bind(undef, sasl => $sasl);
$fp->croak("LDAP error code %i: %s\n", $r->code, $r->error)
unless $r->code == LDAP_SUCCESS;
if ($refresh) {
my $dn = sprintf($conf{ldap}->{"session-authcDN"} // "%s",
escape_dn_value($self->{authcid}));
my $ttl = $conf{www}->{"cache-expires"};
$r = $fp->{_ldap}->refresh(entryName => $dn, requestTtl => $ttl);
$fp->croak("LDAP error code %i: %s\n", $r->code, $r->error)
unless $r->code == LDAP_SUCCESS;
}
return $fp;
}
# destroy(OPTION => VALUE, ..)
# Create a new Fripost object, authenticate (using SASL proxy
# authorization), and delete the entry on the LDAP backend.
# The object shouldn't be used after using this method.
sub destroy($%) {
my $self = shift;
my %conf = @_;
my $dn = sprintf($conf{ldap}->{"session-authcDN"} // "%s",
escape_dn_value($self->{authcid}));
my $fp = authenticate($self, %conf, refresh => 0);
my $r = $fp->{_ldap}->delete($dn);
$fp->croak("LDAP error code %i: %s\n", $r->code, $r->error)
unless $r->code == LDAP_SUCCESS;
# forget credentials in the object (now a blessed empty hash reference)
undef %$self;
}
1;
|