2016-06-10 13:50:37 +02:00
|
|
|
# Base package for simple issuers plugins
|
|
|
|
#
|
|
|
|
# Issuer should just implement a run() method that will be called only for
|
|
|
|
# authenticated users when PATH_INFO starts with issuerDBXXPath
|
|
|
|
#
|
|
|
|
# run() should just return a Lemonldap::NG::Portal::Main::Constants value. It
|
|
|
|
# is called using process() method (Lemonldap::NG::Portal::Main::Process)
|
|
|
|
package Lemonldap::NG::Portal::Main::Issuer;
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use Mouse;
|
2017-03-26 07:26:25 +02:00
|
|
|
use MIME::Base64;
|
2017-03-21 07:23:58 +01:00
|
|
|
use IO::String;
|
2018-07-05 18:45:29 +02:00
|
|
|
use URI::Escape;
|
2018-06-26 19:13:06 +02:00
|
|
|
use Lemonldap::NG::Common::FormEncode;
|
2017-03-23 21:49:52 +01:00
|
|
|
use Lemonldap::NG::Portal::Main::Constants qw(
|
|
|
|
PE_OK
|
|
|
|
PE_RENEWSESSION
|
|
|
|
);
|
2016-06-10 13:50:37 +02:00
|
|
|
|
|
|
|
extends 'Lemonldap::NG::Portal::Main::Plugin';
|
|
|
|
|
|
|
|
our $VERSION = '2.0.0';
|
|
|
|
|
|
|
|
# PROPERTIES
|
|
|
|
|
|
|
|
has type => ( is => 'rw' );
|
|
|
|
|
2016-12-21 23:39:12 +01:00
|
|
|
has path => ( is => 'rw' );
|
|
|
|
|
2018-07-04 22:54:09 +02:00
|
|
|
has ipath => ( is => 'rw' );
|
|
|
|
|
2017-03-21 07:23:58 +01:00
|
|
|
has _ott => (
|
|
|
|
is => 'rw',
|
2017-03-27 18:51:18 +02:00
|
|
|
lazy => 1,
|
2017-03-21 07:23:58 +01:00
|
|
|
default => sub {
|
|
|
|
my $ott = $_[0]->{p}->loadModule('::Lib::OneTimeToken');
|
|
|
|
$ott->timeout( $_[0]->{conf}->{formTimeout} );
|
|
|
|
return $ott;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2016-06-12 18:52:37 +02:00
|
|
|
# INTERFACE
|
|
|
|
|
|
|
|
# Only logout is called in normal use. Issuer that inherits from this
|
|
|
|
# package are called only by their path
|
|
|
|
sub beforeLogout { 'logout' }
|
|
|
|
|
2016-06-10 13:50:37 +02:00
|
|
|
# INITIALIZATION
|
|
|
|
|
|
|
|
sub init {
|
|
|
|
my ($self) = @_;
|
|
|
|
my $type = ref( $_[0] );
|
|
|
|
$type =~ s/.*:://;
|
|
|
|
$self->type($type);
|
|
|
|
if ( my $path = $self->conf->{"issuerDB${type}Path"} ) {
|
2016-06-12 21:26:14 +02:00
|
|
|
$path =~ s/^.*?(\w+).*?$/$1/;
|
2016-12-21 23:39:12 +01:00
|
|
|
$self->path($path);
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->addUnauthRoute(
|
|
|
|
$path => { '*' => '_redirect' },
|
|
|
|
[ 'GET', 'POST' ]
|
|
|
|
);
|
|
|
|
$self->addAuthRoute(
|
|
|
|
$path => { '*' => "_forAuthUser" },
|
|
|
|
[ 'GET', 'POST' ]
|
|
|
|
);
|
2016-06-10 13:50:37 +02:00
|
|
|
}
|
|
|
|
else {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("No path declared for issuer $type. Skipping");
|
2016-06-10 13:50:37 +02:00
|
|
|
}
|
2018-07-04 22:54:09 +02:00
|
|
|
$self->ipath( 'issuerRequest' . $self->path );
|
2016-06-10 13:50:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# RUNNING METHODS
|
|
|
|
|
|
|
|
# Case 1: Unauthentified users are redirected to the main portal
|
|
|
|
|
|
|
|
sub _redirect {
|
2016-12-21 19:06:23 +01:00
|
|
|
my ( $self, $req, @path ) = @_;
|
2018-06-29 14:31:43 +02:00
|
|
|
my $restore;
|
|
|
|
my $ir;
|
|
|
|
unless ( $self->can('ssoMatch') and not $self->ssoMatch($req) ) {
|
2018-07-04 22:54:09 +02:00
|
|
|
$self->logger->debug("Unauth request to $self->{path} issuer");
|
2018-06-29 14:31:43 +02:00
|
|
|
$restore = 1;
|
|
|
|
$self->logger->debug('Processing _redirect');
|
2018-07-04 22:54:09 +02:00
|
|
|
$ir = $req->pdata->{ $self->ipath } ||= $self->storeRequest($req);
|
2018-07-05 15:54:41 +02:00
|
|
|
$req->pdata->{ $self->ipath . 'Path' } = \@path;
|
2018-07-05 18:45:29 +02:00
|
|
|
$req->pdata->{keepPdata} = 1;
|
2018-07-05 14:24:22 +02:00
|
|
|
$req->{urldc} = $self->conf->{portal} . '/' . $self->path;
|
2018-06-29 14:31:43 +02:00
|
|
|
}
|
|
|
|
else {
|
2018-06-30 08:21:38 +02:00
|
|
|
$self->logger->debug('Not seen as Issuer request, skipping');
|
2016-11-28 22:15:57 +01:00
|
|
|
}
|
2016-06-12 21:38:02 +02:00
|
|
|
|
2016-06-12 18:52:37 +02:00
|
|
|
# TODO: launch normal process with 'run' at the end
|
2016-06-12 21:38:02 +02:00
|
|
|
return $self->p->do(
|
|
|
|
$req,
|
|
|
|
[
|
|
|
|
'controlUrl',
|
|
|
|
@{ $self->p->beforeAuth },
|
|
|
|
$self->p->authProcess,
|
2018-07-05 22:56:16 +02:00
|
|
|
@{ $self->p->betweenAuthAndData },
|
|
|
|
$self->p->sessionData,
|
|
|
|
@{ $self->p->afterData },
|
2018-09-05 09:19:01 +02:00
|
|
|
$self->p->validSession,
|
|
|
|
@{ $self->p->endAuth },
|
2018-06-29 14:31:43 +02:00
|
|
|
(
|
|
|
|
$restore
|
|
|
|
? sub {
|
|
|
|
|
|
|
|
# Restore urldc if auth doesn't need to dial with browser
|
|
|
|
$self->restoreRequest( $req, $ir );
|
|
|
|
return $self->run( @_, @path );
|
2018-11-26 14:40:21 +01:00
|
|
|
}
|
2018-06-29 14:31:43 +02:00
|
|
|
: ()
|
|
|
|
)
|
2016-06-12 21:38:02 +02:00
|
|
|
]
|
|
|
|
);
|
2016-06-10 13:50:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Case 3: authentified user, launch
|
|
|
|
sub _forAuthUser {
|
2016-12-22 19:41:11 +01:00
|
|
|
my ( $self, $req, @path ) = @_;
|
2018-07-04 22:54:09 +02:00
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug('Processing _forAuthUser');
|
2018-07-04 22:54:09 +02:00
|
|
|
if ( my $r = $req->pdata->{ $self->ipath } ) {
|
|
|
|
$self->logger->debug("Restoring request to $self->{path} issuer");
|
2017-03-21 08:59:54 +01:00
|
|
|
$self->restoreRequest( $req, $r );
|
2018-07-05 15:54:41 +02:00
|
|
|
@path = @{ $req->pdata->{ $self->ipath . 'Path' } }
|
|
|
|
if ( $req->pdata->{ $self->ipath . 'Path' } );
|
2017-03-21 08:59:54 +01:00
|
|
|
}
|
2018-07-04 22:54:09 +02:00
|
|
|
|
|
|
|
# Clean pdata: keepPdata has been set, so pdata must be cleaned here
|
2018-07-05 14:24:22 +02:00
|
|
|
$self->logger->debug('Cleaning pdata');
|
2018-07-04 22:54:09 +02:00
|
|
|
$req->pdata( {} );
|
2017-10-26 22:21:15 +02:00
|
|
|
$req->urlNotBase64(1) if ( ref($self) =~ /::CAS$/ );
|
2018-07-05 14:24:22 +02:00
|
|
|
$req->mustRedirect(1);
|
2016-06-10 13:50:37 +02:00
|
|
|
return $self->p->do(
|
|
|
|
$req,
|
|
|
|
[
|
2018-07-05 22:56:16 +02:00
|
|
|
'importHandlerData',
|
2016-06-10 13:50:37 +02:00
|
|
|
'controlUrl',
|
|
|
|
@{ $self->p->forAuthUser },
|
|
|
|
sub {
|
2016-12-22 19:41:11 +01:00
|
|
|
return $self->run( @_, @path );
|
2016-06-10 13:50:37 +02:00
|
|
|
},
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-03-21 07:23:58 +01:00
|
|
|
sub storeRequest {
|
|
|
|
my ( $self, $req ) = @_;
|
2018-07-05 14:24:22 +02:00
|
|
|
$self->logger->debug('Store issuer request');
|
2017-03-21 07:23:58 +01:00
|
|
|
my $info = {};
|
|
|
|
$info->{content} = $req->content;
|
|
|
|
foreach ( keys %{ $req->env } ) {
|
|
|
|
$info->{$_} = $req->env->{$_} unless ( ref $req->env->{$_} );
|
|
|
|
}
|
|
|
|
return $self->_ott->createToken($info);
|
|
|
|
}
|
|
|
|
|
|
|
|
sub restoreRequest {
|
|
|
|
my ( $self, $req, $token ) = @_;
|
|
|
|
my $env = $self->_ott->getToken($token);
|
|
|
|
if ($env) {
|
|
|
|
$self->logger->debug("Restoring request from $token");
|
|
|
|
if ( my $c = delete $env->{content} ) {
|
|
|
|
$env->{'psgix.input.buffered'} = 0;
|
|
|
|
$env->{'psgi.input'} = IO::String->new($c);
|
|
|
|
}
|
|
|
|
$req->{env} = {};
|
2017-03-21 08:59:54 +01:00
|
|
|
foreach ( keys %$env ) {
|
2018-07-05 14:24:22 +02:00
|
|
|
$self->logger->debug(
|
|
|
|
"Restore $_" . ( ref $env->{$_} ? '' : "\t" . $env->{$_} ) );
|
2017-03-21 08:59:54 +01:00
|
|
|
$req->env->{$_} = $env->{$_} unless /^plack/;
|
2017-03-21 07:23:58 +01:00
|
|
|
}
|
|
|
|
}
|
2018-07-05 18:45:29 +02:00
|
|
|
$req->{uri} = uri_unescape( $req->env->{REQUEST_URI} );
|
|
|
|
$req->{uri} =~ s|^//+|/|g;
|
2017-03-21 07:23:58 +01:00
|
|
|
return $req;
|
|
|
|
}
|
|
|
|
|
2017-03-23 21:49:52 +01:00
|
|
|
sub reAuth {
|
|
|
|
my ( $self, $req ) = @_;
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->data->{customScript} =
|
2018-05-16 13:22:15 +02:00
|
|
|
qq'<script type="text/javascript" src="$self->{p}->{staticPrefix}/common/js/autoRenew.min.js"></script>'
|
|
|
|
if ( $self->conf->{skipRenewConfirmation} );
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->data->{_url} =
|
2018-07-04 22:54:09 +02:00
|
|
|
encode_base64( $self->conf->{portal} . $req->path_info, '' );
|
|
|
|
$req->pdata->{ $self->ipath } = $self->storeRequest($req);
|
2018-07-05 18:45:29 +02:00
|
|
|
$req->pdata->{keepPdata} = 1;
|
2017-03-23 21:49:52 +01:00
|
|
|
return PE_RENEWSESSION;
|
|
|
|
}
|
|
|
|
|
2016-06-10 13:50:37 +02:00
|
|
|
1;
|
2016-12-19 17:15:31 +01:00
|
|
|
__END__
|
|
|
|
|
|
|
|
=pod
|
|
|
|
|
|
|
|
=encoding utf8
|
|
|
|
|
|
|
|
=head1 NAME
|
|
|
|
|
|
|
|
Lemonldap::NG::Portal::Main::Issuer - Base class for identity providers.
|
|
|
|
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
|
|
|
|
package Lemonldap::NG::Portal::Issuer::My;
|
|
|
|
use strict;
|
|
|
|
use Mouse;
|
|
|
|
extends 'Lemonldap::NG::Portal::Main::Issuer';
|
|
|
|
use Lemonldap::NG::Portal::Main::Constants qw(PE_OK);
|
2018-06-26 12:08:51 +02:00
|
|
|
|
|
|
|
# Required: URL root path
|
|
|
|
use constant path => 'saml';
|
2016-12-19 17:15:31 +01:00
|
|
|
|
|
|
|
# Optional initialization method
|
|
|
|
sub init {
|
|
|
|
my ($self) = @_;
|
|
|
|
...
|
|
|
|
# Must return 1 (succeed) or 0 (failure)
|
|
|
|
}
|
|
|
|
|
|
|
|
# Required methods are run() and logout(), they are launched only for
|
|
|
|
# authenticated users
|
|
|
|
# $req is a Lemonldap::NG::Portal::Main::Request object
|
|
|
|
# They must return a Lemonldap::NG::Portal::Main::Constants constant
|
|
|
|
sub run {
|
|
|
|
my ( $self, $req ) = @_
|
|
|
|
...
|
|
|
|
return PE_OK
|
|
|
|
}
|
|
|
|
|
|
|
|
sub logout {
|
|
|
|
my ( $self, $req ) = @_
|
|
|
|
...
|
|
|
|
return PE_OK
|
|
|
|
}
|
|
|
|
1;
|
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
|
|
|
|
Lemonldap::NG::Portal::Main::Issuer is a base class to write identity providers
|
|
|
|
for Lemonldap::NG web-SSO system. It provide several methods to write easily
|
|
|
|
an IdP and manage authentication if the identity request comes before
|
|
|
|
authentication.
|
|
|
|
|
|
|
|
=head1 WRITING AN IDENTITY PROVIDER
|
|
|
|
|
|
|
|
To write a classic identity provider, you just have to inherit this class and
|
|
|
|
write run() and logout() methods. These methods must return a
|
|
|
|
Lemonldap::NG::Portal::Main::Constants constant.
|
|
|
|
|
|
|
|
A classic identity provider needs a "issuerDBE<gt>XXXE<lt>Path" parameter in
|
|
|
|
LLNG configuration to declare its base URI path (see
|
|
|
|
L<Lemonldap::NG::Manager::Build>). Example: /saml/. All requests that starts
|
2016-12-19 21:51:51 +01:00
|
|
|
with /saml/ will call run() after authentication if needed, and no one else.
|
2016-12-19 17:15:31 +01:00
|
|
|
|
|
|
|
The logout() function is called when user asks for logout on this server. If
|
|
|
|
you want to write an identity provider, you must implement a single logout
|
|
|
|
system.
|
|
|
|
|
|
|
|
=head2 managing other URI path
|
|
|
|
|
|
|
|
Lemonldap::NG::Portal::Main::Issuer provides methods to bind a method to an
|
|
|
|
URI path:
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item addAuthRoute() for authenticated users
|
|
|
|
|
|
|
|
=item addUnauthRoute() for unauthenticated users
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
They must be called during initialization process (so you must write the
|
|
|
|
optional init() sub).
|
|
|
|
|
2018-06-30 08:41:45 +02:00
|
|
|
Be careful with C<add*authRoute()>: you can't catch here your root path (=
|
|
|
|
path declared in C<$self-E<gt>path>) because it is caught by this module,
|
|
|
|
but you can catch sub-routes (ie C</path/something>).
|
2018-06-26 12:08:51 +02:00
|
|
|
|
2016-12-19 17:15:31 +01:00
|
|
|
Example:
|
|
|
|
|
|
|
|
sub init {
|
|
|
|
my ($self) = @_;
|
|
|
|
...
|
|
|
|
$self->addUnauthRoute( saml => { soap => 'soapServer' }, [ 'POST' ] );
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
sub soapServer {
|
|
|
|
my ( $self, $req ) = @_;
|
|
|
|
...
|
|
|
|
# You must return a valid PSGI response
|
|
|
|
return [ 200, [ 'Content-Type' => 'application/xml' ], [] ];
|
|
|
|
}
|
|
|
|
|
2018-06-30 08:41:45 +02:00
|
|
|
=head2 avoid conflicts in path
|
|
|
|
|
|
|
|
If you share base URI path with another plugin (a C<Auth::*> module for
|
|
|
|
example), it is recommended to write a C<ssoMatch> function that returns true
|
|
|
|
if C<$req-E<gt>uri> has to be handled by Issuer module. See C<Issuer::SAML>
|
|
|
|
or C<Issuer::OpenIDConnect> to have some examples.
|
|
|
|
|
2016-12-19 17:15:31 +01:00
|
|
|
=head1 SEE ALSO
|
|
|
|
|
|
|
|
L<http://lemonldap-ng.org/>
|
|
|
|
|
2017-01-04 21:51:46 +01:00
|
|
|
=head1 AUTHORS
|
2016-12-19 17:15:31 +01:00
|
|
|
|
|
|
|
=over
|
|
|
|
|
2017-01-04 21:51:46 +01:00
|
|
|
=item LemonLDAP::NG team L<http://lemonldap-ng.org/team>
|
2016-12-19 17:15:31 +01:00
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=head1 BUG REPORT
|
|
|
|
|
|
|
|
Use OW2 system to report bug or ask for features:
|
2017-11-11 14:06:23 +01:00
|
|
|
L<https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues>
|
2016-12-19 17:15:31 +01:00
|
|
|
|
|
|
|
=head1 DOWNLOAD
|
|
|
|
|
|
|
|
Lemonldap::NG is available at
|
|
|
|
L<http://forge.objectweb.org/project/showfiles.php?group_id=274>
|
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
|
2017-01-04 21:51:46 +01:00
|
|
|
See COPYING file for details.
|
2016-12-19 17:15:31 +01:00
|
|
|
|
|
|
|
This library is free software; you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation; either version 2, 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 General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program. If not, see L<http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
=cut
|