Merge remote-tracking branch 'origin/v2.0'

This commit is contained in:
Yadd 2021-03-30 21:41:31 +02:00
commit ed84fdd771
11 changed files with 207 additions and 142 deletions

View File

@ -113,22 +113,19 @@ Register LL::NG to an OpenID Connect Provider
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To register LL::NG, you will need to give some information like
application name or logo. One of mandatory information is the redirect
URL (one or many).
application name or logo.
To know this information, just take the portal URL and the Callback GET
parameter, for example:
You will be asked to provide a *Redirect URI* for LemonLDAP::NG, which is constructed by appending the ``openidcallback=1`` parameter to the Portal URL.
- http://auth.example.com/?openidcallback=1
- http://auth.example.com/index.pl?openidcallback=1
- http://auth.example.com/?lmAuth=oidc&openidcallback=1
For example:
- https://auth.example.com/?openidcallback=1
.. attention::
If you use the :doc:`choice backend<authchoice>`, you
need to add the choice parameter in redirect URL or
set SameSite cookie value to "Lax" or "None".
If you use the :doc:`choice backend<authchoice>`,
you need to set SameSite cookie value to "Lax" or "None".
See :doc:`SSO cookie parameters<ssocookie>`
After registration, the OP must give you a client ID and a client

View File

@ -45,6 +45,7 @@ Invocation
Options:
- ``-c``: job configuration file (mandatory)
- ``-r oldkey=newkey``: rename session keys during conversion (optional, can be given multiple times)
- ``-i``: ignore errors. By default errors will stop the script
execution
- ``-d``: print debugging output

View File

@ -20,7 +20,7 @@ Activation
To activate notifications system:
Go to Manager ``General Parameters`` » ``Advanced Parameters`` » ``Notifications`` » ``Activation``
Go to Manager ``General Parameters`` » ``Plugins`` » ``Notifications`` » ``Activation``
or in ``lemonldap-ng.ini`` [portal] section:
@ -34,7 +34,7 @@ Explorer
Notifications explorer allows users to see and display theirs accepted
notifications. Disable by default, you just have to activate it in the
Manager (``General Parameters`` » ``Advanced Parameters`` » ``Notifications`` »
Manager (``General Parameters`` » ``Plugins`` » ``Notifications`` »
``Explorer``)
or in ``lemonldap-ng.ini`` [portal] section:
@ -91,7 +91,7 @@ configuration:
You can change default parameters using the "notificationStorage" and
"notificationStorageOptions" parameters with the same syntax as
configuration storage parameters. To do this in Manager, go in General
Parameters > Advanced Parameters > Notifications.
Parameters > Plugins > Notifications.
File
^^^^
@ -200,7 +200,7 @@ The notifications module uses a wildcard to manage notifications for all
users. The default value of this wildcard is ``allusers``, but you can
change it if ``allusers`` is a known identifier in your system.
To change it, go in General Parameters > Advanced Parameters >
To change it, go in General Parameters > Plugins >
Notifications > Wildcard for all users, and set for example
``alluserscustom``.

View File

@ -22,7 +22,7 @@ our $specialNodeHash = {
};
our $doubleHashKeys = 'issuerDBGetParameters';
our $simpleHashKeys = '(?:(?:c(?:as(?:StorageOption|Attribute)|ustom(?:Plugins|Add)Param|heckUserHiddenHeader|ombModule)|l(?:o(?:calSessionStorageOption|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|f(?:indUser(?:Exclud|Search)ingAttribute|acebookExportedVar)|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|p(?:ersistentStorageOption|ortalSkinRule)|(?:(?:d(?:emo|bi)|webID)E|e)xportedVar|macro)s|o(?:idcS(?:ervice(?:DynamicRegistrationEx(?:portedVar|traClaim)s|MetaDataAuthnContext)|torageOptions)|penIdExportedVars)|a(?:(?:daptativeAuthenticationLevelR|ut(?:hChoiceMod|oSigninR))ules|pplicationList)|s(?:(?:amlStorageOption|laveExportedVar)s|essionDataToRemember|fExtra)|S(?:MTPTLSOpts|SLVarIf))';
our $simpleHashKeys = '(?:(?:c(?:as(?:StorageOption|Attribute)|ustom(?:Plugins|Add)Param|heckUserHiddenHeader|ombModule)|l(?:o(?:calSessionStorageOption|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|f(?:indUser(?:Exclud|Search)ingAttribute|acebookExportedVar)|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|p(?:ersistentStorageOption|ortalSkinRule)|(?:(?:d(?:emo|bi)|webID)E|e)xportedVar|macro)s|o(?:idc(?:S(?:ervice(?:DynamicRegistrationEx(?:portedVar|traClaim)s|MetaDataAuthnContext)|torageOptions)|OPMetaDataJ(?:SON|WKS))|penIdExportedVars)|a(?:(?:daptativeAuthenticationLevelR|ut(?:hChoiceMod|oSigninR))ules|pplicationList)|s(?:(?:amlStorageOption|laveExportedVar)s|essionDataToRemember|fExtra)|S(?:MTPTLSOpts|SLVarIf))';
our $specialNodeKeys = '(?:(?:(?:saml(?:ID|S)|oidc[OR])P|cas(?:App|Srv))MetaDataNode|virtualHost)s';
our $casAppMetaDataNodeKeys = 'casAppMetaData(?:Options(?:(?:UserAttribut|Servic|Rul)e|AuthnLevel)|(?:ExportedVar|Macro)s)';
our $casSrvMetaDataNodeKeys = 'casSrvMetaData(?:Options(?:ProxiedServices|DisplayName|SortNumber|Gateway|Renew|Icon|Url)|ExportedVars)';

View File

@ -13,62 +13,45 @@ use Lemonldap::NG::Common::Apache::Session;
use Lemonldap::NG::Common::Session;
use Config::IniFiles;
use strict;
use Getopt::Std;
$Getopt::Std::STANDARD_HELP_VERSION = 1;
use Getopt::Long;
use Pod::Usage;
our $VERSION = "2.0.6";
# Options
# -d: debug mode
# -c: configuration file
# -r: configuration file
# -i: ignore errors
my $opts = {};
getopts( 'dic:', $opts );
my $debug = $opts->{d};
my $config_file = $opts->{c};
my $ignore_errors = $opts->{i};
my $nb_converted = 0;
my $nb_error = 0;
my $debug;
my $config_file;
my $ignore_errors;
my %rename;
my $help;
my $nb_converted = 0;
my $nb_error = 0;
sub HELP_MESSAGE {
my $OUT = shift;
print $OUT <<END_MESSAGE;
$0 [-di] -c config_file.ini
-d Debug mode
-i Ignore errors
This script converts sessions in between the two backends specified in the configuration file
The configuration file must contain the following (adjust to your environment):
[sessions_from]
storageModule = Apache::Session::File
storageModuleOptions = { \\
'Directory' => '/var/lib/lemonldap-ng/sessions', \\
'LockDirectory' => '/var/lib/lemonldap-ng/sessions/lock', \\
}
# Only convert some session types
# sessionKind = Persistent, SSO
[sessions_to]
storageModule = Apache::Session::Browseable::Postgres
storageModuleOptions = { \\
'DataSource' => 'DBI:Pg:database=lemonldapdb;host=pg.example.com', \\
'UserName' => 'lemonldaplogin', \\
'Password' => 'lemonldappw', \\
'Commit' => 1, \\
'Index' => 'ipAddr _whatToTrace user', \\
'TableName' => 'sessions', \\
}
END_MESSAGE
}
GetOptions(
'help|?' => \$help,
'debug|d' => \$debug,
'config|c=s' => \$config_file,
'ignore-errors|i' => \$ignore_errors,
'rename|r=s' => \%rename,
) or pod2usage(2);
pod2usage(
-exitval => 1,
-verbose => 99,
-sections => "SYNOPSIS|OPTIONS|CONFIGURATION FILE FORMAT"
) if $help;
unless ($config_file) {
HELP_MESSAGE( \*STDERR );
die "You must provide the -c option";
pod2usage(
-exitval => 2,
-verbose => 99,
-message => "You must provide the -c option\n",
-sections => "SYNOPSIS|OPTIONS|CONFIGURATION FILE FORMAT"
);
}
my $inicfg =
@ -128,6 +111,28 @@ Lemonldap::NG::Common::Apache::Session->get_key_from_all_sessions(
my $entry = shift;
my $id = shift;
# If filtering sessionKind
if (@sessionKindOnly) {
unless ( grep { $_ eq $entry->{_session_kind} } @sessionKindOnly ) {
print "Ignoring session $id with type "
. $entry->{_session_kind} . "\n"
if $debug;
return undef;
}
}
if (%rename) {
for my $oldkey ( keys %rename ) {
my $newkey = $rename{$oldkey};
if ( $newkey and $entry->{$oldkey} ) {
print "Renaming $oldkey to $newkey in session $id\n"
if $debug;
$entry->{$newkey} = delete $entry->{$oldkey};
}
}
}
print "Processing session $id\n" if $debug;
my $s = Lemonldap::NG::Common::Session->new( {
storageModule => $backendTo->{backend},
@ -138,14 +143,6 @@ Lemonldap::NG::Common::Apache::Session->get_key_from_all_sessions(
}
);
# If filtering sessionKind
if (@sessionKindOnly) {
unless ( grep { $_ eq $entry->{_session_kind} } @sessionKindOnly ) {
return undef;
}
}
if ( $s->error ) {
die "Error encountered on session $id" unless $ignore_errors;
$nb_error += 1;
@ -175,7 +172,7 @@ convertSessions - A tool to convert Lemonldap::NG sessions between storage backe
=head1 SYNOPSIS
convertSession [-di] -c parameters.ini
convertSession [-di] [-r oldkey=newkey ] -c parameters.ini
=head1 DESCRIPTION
@ -191,6 +188,29 @@ destination backend will be kept, unless they have the same session ID as a
session in the source backend. In that case, the source will overwrite the
destination.
=head1 OPTIONS
=over
=item B<--config>,B<-c>
Specify configuration file
=item B<--debug>,B<-d>
Turns on debugging information
=item B<--ignore-errors>,B<-i>
Skip to the next session if converting a session fails
=item B<--rename oldkey=newkey>,B<-r oldkey=newkey>
Rename key names when migrating from one backend to the next.
This option can be specified multiple times
=back
=head1 CONFIGURATION FILE FORMAT

View File

@ -2152,9 +2152,15 @@ m[^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
'type' => 'keyTextContainer'
},
'oidcOPMetaDataJSON' => {
'keyTest' => sub {
1;
},
'type' => 'file'
},
'oidcOPMetaDataJWKS' => {
'keyTest' => sub {
1;
},
'type' => 'file'
},
'oidcOPMetaDataNodes' => {
@ -2447,7 +2453,7 @@ m[^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
'default' => {},
'test' => {
'keyMsgFail' => '__badMacroName__',
'keyTest' => qr/^[_a-zA-Z][a-zA-Z0-9_]*$/,
'keyTest' => qr/^[\x21\x23-\x5B\x5D-\x7E]+$/,
'test' => sub {
return perlExpr(@_);
}

View File

@ -694,9 +694,18 @@ sub scanTree {
}
}
}
if ($prefix) {
push @cnodesKeys, $leaf;
}
# issue 2439
# FIXME: in future versions, oidcOPMetaDataJSON and samlIDPMetaDataXML shoud
# behave the same
if ( $leaf =~ /^oidcOPMetaData(?:JSON|JWKS)$/ ) {
push @simpleHashKeys, $leaf;
}
if ( $attr->{type} =~ /^(?:catAndAppList|\w+Container)$/ ) {
$jleaf->{cnodes} = $prefix . $leaf;
unless ( $prefix or $leaf =~ $reIgnoreKeys ) {

View File

@ -4162,8 +4162,8 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
oidcRPMetaDataOptions => { type => 'subContainer', },
# OpenID Connect providers
oidcOPMetaDataJSON => { type => 'file', },
oidcOPMetaDataJWKS => { type => 'file', },
oidcOPMetaDataJSON => { type => 'file', keyTest => sub { 1 } },
oidcOPMetaDataJWKS => { type => 'file', keyTest => sub { 1 } },
oidcOPMetaDataExportedVars => {
type => 'keyTextContainer',
default => {
@ -4348,7 +4348,8 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
type => 'keyTextContainer',
help => 'idpopenidconnect.html#scope-rules',
test => {
keyTest => qr/^[_a-zA-Z][a-zA-Z0-9_]*$/,
# RFC6749
keyTest => qr/^[\x21\x23-\x5B\x5D-\x7E]+$/,
keyMsgFail => '__badMacroName__',
test => sub { return perlExpr(@_) },
},

View File

@ -1719,7 +1719,7 @@ sub userInfo {
unless ($accessTokenSession) {
$self->userLogger->error(
"Unable to get access token session for id $access_token");
"Unable to validate access token $access_token");
return $self->returnBearerError( 'invalid_request',
'Invalid request', 401 );
}

View File

@ -722,93 +722,100 @@ sub getAuthorizationCode {
sub newAccessToken {
my ( $self, $req, $rp, $scope, $sessionInfo, $info ) = @_;
my $at_info = {
scope => $scope,
rp => $rp,
%{$info},
};
my $session = $self->getOpenIDConnectSession(
undef,
"access_token",
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsAccessTokenExpiration}
|| $self->conf->{oidcServiceAccessTokenExpiration},
{
scope => $scope,
rp => $rp,
%{$info},
}
$at_info,
);
if ($session) {
return $self->maybeJWT( $req, $rp, $scope, $session->id, $sessionInfo );
if ( $self->_wantJWT($rp) ) {
my $at_jwt =
$self->makeJWT( $req, $rp, $scope, $session->id, $sessionInfo );
$at_info->{sha256_hash} = $self->createHash( $at_jwt, 256 );
$self->updateToken( $session->id, $at_info );
return $at_jwt;
}
else {
return $session->id;
}
}
else {
return undef;
}
}
sub maybeJWT {
sub _wantJWT {
my ( $self, $rp ) = @_;
return $self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsAccessTokenJWT};
}
sub makeJWT {
my ( $self, $req, $rp, $scope, $id, $sessionInfo ) = @_;
if ( $self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsAccessTokenJWT} )
{
my $exp =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsAccessTokenExpiration}
|| $self->conf->{oidcServiceAccessTokenExpiration};
$exp += time;
my $client_id = $self->oidcRPList->{$rp}->{oidcRPMetaDataOptionsClientID};
my $exp =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsAccessTokenExpiration}
|| $self->conf->{oidcServiceAccessTokenExpiration};
$exp += time;
my $client_id =
$self->oidcRPList->{$rp}->{oidcRPMetaDataOptionsClientID};
my $access_token_payload = {
iss => $self->iss, # Issuer Identifier
exp => $exp, # expiration
aud => $self->getAudiences($rp), # Audience
client_id => $client_id, # Client ID
iat => time, # Issued time
jti => $id, # Access Token session ID
scope => $scope, # Scope
};
my $access_token_payload = {
iss => $self->iss, # Issuer Identifier
exp => $exp, # expiration
aud => $self->getAudiences($rp), # Audience
client_id => $client_id, # Client ID
iat => time, # Issued time
jti => $id, # Access Token session ID
scope => $scope, # Scope
};
my $claims;
if ( ref($sessionInfo) eq "HASH" ) {
$claims = $self->buildUserInfoResponseFromData( $req, $scope,
$rp, $sessionInfo );
}
else {
$claims = $self->buildUserInfoResponseFromId( $req, $scope,
$rp, $sessionInfo );
}
# Release claims, or only sub
if ( $self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsAccessTokenClaims} )
{
foreach ( keys %$claims ) {
$access_token_payload->{$_} = $claims->{$_};
}
}
else {
$access_token_payload->{sub} = $claims->{sub};
}
# Call hook to let the user modify payload
my $h = $self->p->processHook( $req, 'oidcGenerateAccessToken',
$access_token_payload, $rp );
return undef if ( $h != PE_OK );
# Get signature algorithm
my $alg = $self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsAccessTokenSignAlg} || "RS256";
$self->logger->debug("Access Token signature algorithm: $alg");
my $jwt =
$self->createJWT( $access_token_payload, $alg, $rp, "at+JWT" );
return $jwt;
my $claims;
if ( ref($sessionInfo) eq "HASH" ) {
$claims = $self->buildUserInfoResponseFromData( $req, $scope,
$rp, $sessionInfo );
}
else {
return $id;
$claims =
$self->buildUserInfoResponseFromId( $req, $scope, $rp, $sessionInfo );
}
# Release claims, or only sub
if ( $self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsAccessTokenClaims} )
{
foreach ( keys %$claims ) {
$access_token_payload->{$_} = $claims->{$_};
}
}
else {
$access_token_payload->{sub} = $claims->{sub};
}
# Call hook to let the user modify payload
my $h = $self->p->processHook( $req, 'oidcGenerateAccessToken',
$access_token_payload, $rp );
return undef if ( $h != PE_OK );
# Get signature algorithm
my $alg = $self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsAccessTokenSignAlg} || "RS256";
$self->logger->debug("Access Token signature algorithm: $alg");
my $jwt = $self->createJWT( $access_token_payload, $alg, $rp, "at+JWT" );
return $jwt;
}
# Get an session from the supplied Access Token
@ -820,7 +827,26 @@ sub getAccessToken {
my $id = getAccessTokenSessionId($access_token);
return unless $id;
return $self->getOpenIDConnectSession( $id, "access_token" );
my $session = $self->getOpenIDConnectSession( $id, "access_token" );
return undef unless $session;
my $stored_hash = $session->{data}->{sha256_hash};
if ($stored_hash) {
my $incoming_hash = $self->createHash( $access_token, 256 );
if ( $stored_hash eq $incoming_hash ) {
return $session;
}
else {
$self->logger->error(
"Incoming Access token hash $incoming_hash "
. "does not match stored hash $stored_hash. "
. "The access token might have been tampered with." );
return undef;
}
}
else {
return $session;
}
}
# Create a new Refresh Token
@ -851,6 +877,11 @@ sub getRefreshToken {
}
sub updateRefreshToken {
my $self = shift;
return $self->updateToken($@);
}
sub updateToken {
my ( $self, $id, $infos ) = @_;
my %storage = (

View File

@ -107,7 +107,7 @@ sub createNotification {
sub canUpdateSfa {
my ( $self, $req, $action ) = @_;
my $user = $req->userData->{ $self->conf->{whatToTrace} };
my $msg = undef;
my $msg = undef;
# Test action
if ( $action && $action eq 'delete' ) {
@ -157,7 +157,7 @@ sub canUpdateSfa {
$msg = 'notAuthorized';
}
}
$self->userLogger->info("$user is allowed to update 2FA") unless $msg;
$self->logger->debug("$user is allowed to update 2FA") unless $msg;
return $msg;
}