Merge branch 'v2.0'

This commit is contained in:
Yadd 2021-04-23 21:44:48 +02:00
commit 8653dde5b5
19 changed files with 217 additions and 64 deletions

View File

@ -30,6 +30,7 @@ Applications
applications/limesurvey
applications/mattermost
applications/mediawiki
applications/mobilizon
applications/nextcloud
applications/obm
applications/office365
@ -108,6 +109,7 @@ Application Configuration
.. image:: applications/limesurvey_logo.png :doc:`LimeSurvey<applications/limesurvey>`
.. image:: applications/mattermost_logo.png :doc:`Mattermost<applications/mattermost>`
.. image:: applications/mediawiki_logo.png :doc:`Mediawiki<applications/mediawiki>`
.. image:: applications/mobilizon_logo.jpg :doc:`Mobilizon<applications/mobilizon>`
.. image:: applications/nextcloud-logo.png :doc:`NextCloud<applications/nextcloud>`
.. image:: applications/obm_logo.png :doc:`OBM<applications/obm>`
.. image:: applications/logo_office_365.png :doc:`Office 365<applications/office365>`

View File

@ -0,0 +1,51 @@
Mobilizon
=========
|mobilizon_logo.jpg|
Presentation
------------
`Mobilizon <https://joinmobilizon.org>`__ is an online tool to help manage your events, your profiles and your groups.
Mobilizon lets users `authenticate with OpenID Connect <https://docs.joinmobilizon.org/administration/configure/auth/#oauth>`__ through the same plugin used by Keycloak.
First, make sure you have set up LemonLDAP::NG 's
:doc:`OpenID Connect service<..//openidconnectservice>` and added
:doc:`a Relaying Party for your Mobilizon instance<..//idpopenidconnect>`
The only options you need to configure are:
* *Client ID*: choose one
* *Client Secret*: choose one
* *Allowed redirection addresses for login*: ``https://mobilizon.example.com/auth/keycloak/callback``
Mobilizon configuration
-----------------------
Edit ``/etc/mobilizon/config.exs``, and adjust the Client ID, Client Secret and URLs to match your domain ::
config :ueberauth,
Ueberauth,
providers: [
keycloak: {Ueberauth.Strategy.Keycloak, [default_scope: "openid profile email"]}
]
config :mobilizon, :auth,
oauth_consumer_strategies: [
{:keycloak, "LemonLDAP::NG"}
]
config :ueberauth, Ueberauth.Strategy.Keycloak.OAuth,
client_id: "CHANGEME",
client_secret: "CHANGEME",
site: "https://auth.example.com",
authorize_url: "https://auth.example.com/oauth2/authorize",
token_url: "https://auth.example.com/oauth2/token",
userinfo_url: "https://auth.example.com/oauth2/userinfo",
token_method: :post
.. |mobilizon_logo.jpg| image:: /applications/mobilizon_logo.jpg
:class: align-center

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -13,7 +13,7 @@ our $VERSION = '2.1.0';
has modules => ( is => 'rw', default => sub { {} } );
has rules => ( is => 'rw', default => sub { {} } );
has type => ( is => 'rw' );
has catch => ( is => 'rw', default => sub { {} } );
has catch => ( is => 'rw', default => sub { {} } );
has sessionKey => ( is => 'ro', default => '_choice' );
my $_choiceRules;
@ -117,8 +117,10 @@ sub checkChoice {
}
unless ($name) {
# Set by OAuth Resource Owner grant // RESTServer pwdCheck
if ($req->data->{_pwdCheck} and $self->{conf}->{authChoiceAuthBasic}) {
if ( $req->data->{_pwdCheck} and $self->{conf}->{authChoiceAuthBasic} )
{
$name = $self->{conf}->{authChoiceAuthBasic};
}
}
@ -210,7 +212,7 @@ sub getForm {
if ( $auth and $userDB and $passwordDB ) {
# Default URL
$req->{cspFormAction} ||= '';
$req->data->{cspFormAction} ||= {};
if (
defined $url
and not $self->checkXSSAttack( 'URI',
@ -219,11 +221,9 @@ sub getForm {
q%^(https?://)?[^\s/.?#$].[^\s]+$% # URL must be well formatted
)
{
#$url .= $req->env->{'REQUEST_URI'};
# Avoid append same URL
$req->{cspFormAction} .= " $url"
unless $req->{cspFormAction} =~ qr%\b$url\b%;
my $csp_uri = $self->cspGetHost($url);
$req->data->{cspFormAction}->{$csp_uri} = 1;
}
else {
$url .= '#';

View File

@ -2683,6 +2683,8 @@ sub sendLogoutRequestToProvider {
name => $providerName,
}
);
$req->data->{cspChildSrc}->{ $self->p->cspGetHost( $logout->msg_url ) }
= 1;
}
# HTTP-SOAP

View File

@ -15,6 +15,7 @@ package Lemonldap::NG::Portal::Main;
use strict;
use URI::Escape;
use URI;
use JSON;
use Lemonldap::NG::Common::Util qw(getPSessionID);
@ -895,10 +896,14 @@ sub sendHtml {
$csp .= " $url";
}
}
if ( defined $req->{cspFormAction} ) {
$self->logger->debug(
"Set CSP form-action with request URL: " . $req->{cspFormAction} );
$csp .= " " . $req->{cspFormAction};
if ( defined $req->data->{cspFormAction}
and ref( $req->data->{cspFormAction} ) eq "HASH" )
{
my $request_csp_form_action =
join( " ", keys %{ $req->data->{cspFormAction} } );
$self->logger->debug( "Set CSP form-action with request URL: "
. $request_csp_form_action );
$csp .= " " . $request_csp_form_action;
}
# Set SAML Discovery Protocol in form-action
@ -928,11 +933,18 @@ sub sendHtml {
}
# Check if frames need to be embedded
# FIXME: we should use $req->data->{cspChildSrc} anywhere an iframe is
# created in the code, and remove this
my @url;
if ( $req->info ) {
@url = map { s#https?://([^/]+).*#$1#; $_ }
( $req->info =~ /<iframe.*?src="(.*?)"/sg );
}
# Update child-src header from request data
if ( ref( $req->data->{cspChildSrc} ) eq "HASH" ) {
push @url, keys %{ $req->data->{cspChildSrc} };
}
if (@url) {
$csp .= join( ' ', 'child-src', @url, "'self'" ) . ';';
}
@ -1073,7 +1085,7 @@ sub registerLogin {
}
my $history = $req->sessionInfo->{_loginHistory} ||= {};
my $type = ( $req->authResult > 0 ? 'failed' : 'success' ) . 'Login';
my $type = ( $req->authResult > 0 ? 'failed' : 'success' ) . 'Login';
$history->{$type} ||= [];
$self->logger->debug("Current login saved into $type");
@ -1196,4 +1208,16 @@ sub loadTemplate {
return $tpl->output;
}
# This method extracts the scheme://host:port part of a URL for use in
# Content-Security-Polity header
sub cspGetHost {
my ( $self, $url ) = @_;
my $uri = $url // "";
unless ( $uri->isa("URI") ) {
$uri = URI->new($uri);
}
return (
$uri->scheme . "://" . ( $uri->_port ? $uri->host_port : $uri->host ) );
}
1;

View File

@ -176,7 +176,7 @@ sub checkPasswordQuality {
## Min special characters
# Just number of special characters must be checked
if ( $self->conf->{passwordPolicyMinSpeChar} && $speChars eq '__ALL__' ) {
my $spe = $password =~ s/\w//g;
my $spe = $password =~ s/\W//g;
if ( $spe < $self->conf->{passwordPolicyMinSpeChar} ) {
$self->logger->error("Password has not enough special characters");
return PE_PP_INSUFFICIENT_PASSWORD_QUALITY;

View File

@ -9,7 +9,7 @@ use Lemonldap::NG::Portal::Main::Constants qw(
require 't/test-lib.pm';
my $res;
my ($res, $json);
my $client = LLNG::Manager::Test->new( {
ini => {
@ -56,7 +56,7 @@ ok(
'Password min size not respected'
);
expectBadRequest($res);
my $json;
ok( $json = eval { from_json( $res->[2]->[0] ) }, 'Response is JSON' )
or print STDERR "$@\n" . Dumper($res);
ok(

View File

@ -0,0 +1,96 @@
use Test::More;
use strict;
use IO::String;
use JSON;
use Lemonldap::NG::Portal::Main::Constants
'PE_PP_INSUFFICIENT_PASSWORD_QUALITY';
require 't/test-lib.pm';
my ( $res, $json );
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
passwordDB => 'Demo',
portalRequireOldPassword => 1,
passwordPolicyMinSize => 0,
passwordPolicyMinLower => 0,
passwordPolicyMinUpper => 0,
passwordPolicyMinDigit => 0,
passwordPolicyMinSpeChar => 2,
passwordPolicySpecialChar => '__ALL__',
portalDisplayPasswordPolicy => 1
}
}
);
# Try to authenticate
# -------------------
ok(
$res = $client->_post(
'/',
IO::String->new('user=dwho&password=dwho'),
length => 23
),
'Auth query'
);
count(1);
expectOK($res);
my $id = expectCookie($res);
ok(
$res =
$client->_get( '/', cookie => "lemonldap=$id", accept => 'text/html' ),
'Get Menu'
);
ok( $res->[2]->[0] =~ m%<input id="oldpassword" name="oldpassword"%,
' Old password input' )
or print STDERR Dumper( $res->[2]->[0] );
ok(
$res->[2]->[0] =~
m%<span trspan="passwordPolicyMinSpeChar">Minimal special characters:</span> 2%,
' passwordPolicyMinSpeChar'
) or print STDERR Dumper( $res->[2]->[0] );
count(3);
my $query = 'oldpassword=dwho&newpassword=@test&confirmpassword=@test';
ok(
$res = $client->_post(
'/',
IO::String->new($query),
cookie => "lemonldap=$id",
accept => 'application/json',
length => length($query)
),
'Password min special char policy not respected'
);
expectBadRequest($res);
ok( $json = eval { from_json( $res->[2]->[0] ) }, 'Response is JSON' )
or print STDERR "$@\n" . Dumper($res);
ok(
$json->{error} == PE_PP_INSUFFICIENT_PASSWORD_QUALITY,
'Response is PE_PP_INSUFFICIENT_PASSWORD_QUALITY'
) or explain( $json, "error => 28" );
count(3);
$query = 'oldpassword=dwho&newpassword=@%&confirmpassword=@%';
ok(
$res = $client->_post(
'/',
IO::String->new($query),
cookie => "lemonldap=$id",
accept => 'application/json',
length => length($query)
),
'Password min special char respected'
);
expectOK($res);
count(1);
# Test $client->logout
$client->logout($id);
clean_sessions();
done_testing( count() );

View File

@ -19,8 +19,8 @@ my $client = LLNG::Manager::Test->new( {
passwordPolicyMinLower => 0,
passwordPolicyMinUpper => 0,
passwordPolicyMinDigit => 0,
passwordPolicyMinSpeChar => 2,
passwordPolicySpecialChar => '',
passwordPolicyMinSpeChar => 0,
passwordPolicySpecialChar => '__ALL__',
portalDisplayPasswordPolicy => 1
}
}
@ -48,8 +48,8 @@ ok(
ok( $res->[2]->[0] =~ m%<input id="oldpassword" name="oldpassword"%,
' Old password input' )
or print STDERR Dumper( $res->[2]->[0] );
ok( $res->[2]->[0] =~ m%<span trspan="passwordPolicyMinSpeChar">Minimal special characters:</span> 2%,
' passwordPolicyMinSpeChar' )
ok( $res->[2]->[0] =~ m%<span trspan="passwordPolicyNone">%,
' passwordPolicyNone' )
or print STDERR Dumper( $res->[2]->[0] );
count(3);

View File

@ -114,9 +114,9 @@ m%<form id="lformKerberos" action="#" method="post" class="login Kerberos">%,
' Action # found'
) or explain( $res->[2]->[0], '<form id="lformSSL"' );
my $header = getHeader( $res, 'Content-Security-Policy' );
ok( $header =~ m%;form-action \* https://test.example.com;%,
ok( $header =~ m%;form-action \* https://test.example.com;%,
' CSP URL found' )
or explain( $res->[1], 'form-action * https://test.example.com;' );
or explain( $res->[1], 'form-action * https://test.example.com;' );
ok( $res->[2]->[0] !~ /4_demo/, '4_Demo not displayed' );
ok(
$res->[2]->[0] =~ qr%<img src="/static/common/logos/logo_llng_old.png"%,

View File

@ -11,7 +11,7 @@ BEGIN {
require 't/saml-lib.pm';
}
my $maintests = 19;
my $maintests = 18;
my $debug = 'error';
my ( $issuer, $sp, $res );
@ -144,13 +144,8 @@ m#iframe src="http://auth.idp.com(/saml/relaySingleLogoutPOST)\?(relay=.*?)"#s,
'Get iframe request'
) or explain( $res, '' );
( $url, $query ) = ( $1, $2 );
ok(
getHeader( $res, 'Content-Security-Policy' ) =~
/child-src auth.idp.com/,
' Frame is authorized'
)
or explain( $res->[1],
'Content-Security-Policy => ...child-src auth.idp.com' );
expectCspChildOK($res, "auth.idp.com");
expectCspChildOK($res, "http://auth.sp.com");
ok(
$res = $issuer->_get(

View File

@ -11,7 +11,7 @@ BEGIN {
require 't/saml-lib.pm';
}
my $maintests = 18;
my $maintests = 17;
my $debug = 'error';
my ( $issuer, $sp, $res );
@ -117,12 +117,7 @@ m#iframe src="http://auth.sp.com(/saml/proxySingleLogout)\?(SAMLRequest=.*?)"#,
);
$url = $1;
my $query = $2;
ok(
getHeader( $res, 'Content-Security-Policy' ) =~ /child-src auth.sp.com/,
'Frame is authorized'
)
or explain( $res->[1],
'Content-Security-Policy => ...child-src auth.idp.com' );
expectCspChildOK($res, "auth.sp.com");
my $removedCookie = expectCookie($res);
is( $removedCookie, 0, "SSO cookie removed" );

View File

@ -197,11 +197,7 @@ count(1);
# Query IdP with iframe src
my $url = $1;
$query = $2;
ok( getHeader( $res, 'Content-Security-Policy' ) =~ /child-src auth.idp.com/,
'Frame is authorized' )
or
explain( $res->[1], 'Content-Security-Policy => ...child-src auth.idp.com' );
count(1);
expectCspChildOK($res, "auth.idp.com");
switch ('issuer');
ok(

View File

@ -197,11 +197,7 @@ count(1);
# Query IdP with iframe src
my $url = $1;
$query = $2;
ok( getHeader( $res, 'Content-Security-Policy' ) =~ /child-src auth.idp.com/,
'Frame is authorized' )
or
explain( $res->[1], 'Content-Security-Policy => ...child-src auth.idp.com' );
count(1);
expectCspChildOK($res, "auth.idp.com");
switch ('issuer');
ok(

View File

@ -167,11 +167,7 @@ count(1);
# Query IdP with iframe src
my $url = $1;
$query = $2;
ok( getHeader( $res, 'Content-Security-Policy' ) =~ /child-src auth.idp.com/,
'Frame is authorized' )
or
explain( $res->[1], 'Content-Security-Policy => ...child-src auth.idp.com' );
count(1);
expectCspChildOK($res, "auth.idp.com");
switch ('issuer');
ok(

View File

@ -157,11 +157,7 @@ count(1);
# Query IdP with iframe src
my $url = $1;
$query = $2;
ok( getHeader( $res, 'Content-Security-Policy' ) =~ /child-src auth.idp.com/,
'Frame is authorized' )
or
explain( $res->[1], 'Content-Security-Policy => ...child-src auth.idp.com' );
count(1);
expectCspChildOK($res, "auth.idp.com");
switch ('issuer');
ok(

View File

@ -11,7 +11,7 @@ BEGIN {
}
my $userdb = tempdb();
my $maintests = 21;
my $maintests = 20;
my $debug = 'error';
my ( $issuer, $sp, $res );
@ -251,13 +251,7 @@ SKIP: {
# Query IdP with iframe src
$url = $1;
$query = $2;
ok(
getHeader( $res, 'Content-Security-Policy' ) =~
/child-src auth.idp.com/,
'Frame is authorized'
)
or explain( $res->[1],
'Content-Security-Policy => ...child-src auth.idp.com' );
expectCspChildOK($res, "auth.idp.com");
# Get iframe from CAS server
switch ('issuer');

View File

@ -483,6 +483,16 @@ sub exceptCspFormOK {
count(1);
}
sub expectCspChildOK {
my ( $res, $host ) = @_;
return 1 unless ($host);
my $csp = getHeader( $res, 'Content-Security-Policy' );
ok($csp, "Content-Security-Policy header found");
count(1);
like($csp, qr/child-src[^;]*\Q$host\E/, "Found $host in CSP child-src");
count(1);
}
=head4 getCookies($res)
Returns an hash ref with names => values of cookies set by server.