Merge branch 'v2.0'
This commit is contained in:
commit
8653dde5b5
|
@ -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>` ✔
|
||||
|
|
|
@ -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 |
|
@ -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 .= '#';
|
||||
|
|
|
@ -2683,6 +2683,8 @@ sub sendLogoutRequestToProvider {
|
|||
name => $providerName,
|
||||
}
|
||||
);
|
||||
$req->data->{cspChildSrc}->{ $self->p->cspGetHost( $logout->msg_url ) }
|
||||
= 1;
|
||||
}
|
||||
|
||||
# HTTP-SOAP
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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() );
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"%,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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" );
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue