2014-11-14 15:29:55 +01:00
|
|
|
##@file
|
|
|
|
# OpenIDConnect authentication backend file
|
|
|
|
|
|
|
|
##@class
|
|
|
|
# OpenIDConnect authentication backend class
|
|
|
|
package Lemonldap::NG::Portal::AuthOpenIDConnect;
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use Lemonldap::NG::Portal::Simple;
|
|
|
|
use MIME::Base64;
|
2014-11-14 17:18:50 +01:00
|
|
|
use base qw(Lemonldap::NG::Portal::_OpenIDConnect);
|
2014-11-14 15:29:55 +01:00
|
|
|
|
2015-04-15 16:18:38 +02:00
|
|
|
our $VERSION = '2.0.0';
|
2014-11-14 15:29:55 +01:00
|
|
|
|
|
|
|
## @apmethod int authInit()
|
2014-11-19 12:09:37 +01:00
|
|
|
# Get configuration data
|
2014-11-14 15:29:55 +01:00
|
|
|
# @return Lemonldap::NG::Portal constant
|
|
|
|
sub authInit {
|
2014-11-19 12:09:37 +01:00
|
|
|
my $self = shift;
|
|
|
|
|
2014-11-20 15:03:32 +01:00
|
|
|
return PE_ERROR unless $self->loadOPs;
|
2014-12-01 11:27:47 +01:00
|
|
|
return PE_ERROR unless $self->refreshJWKSdata;
|
2014-11-19 15:17:39 +01:00
|
|
|
|
2014-11-14 15:29:55 +01:00
|
|
|
PE_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
## @apmethod int setAuthSessionInfo()
|
|
|
|
# Set _user value to 'anonymous' and authenticationLevel to 0
|
|
|
|
# @return Lemonldap::NG::Portal constant
|
|
|
|
sub setAuthSessionInfo {
|
|
|
|
my $self = shift;
|
|
|
|
|
|
|
|
$self->{sessionInfo}->{'_user'} = $self->{user};
|
2014-12-11 17:54:27 +01:00
|
|
|
$self->{sessionInfo}->{authenticationLevel} = $self->{oidcAuthnLevel};
|
2014-11-14 15:29:55 +01:00
|
|
|
|
2014-11-20 16:53:26 +01:00
|
|
|
$self->{sessionInfo}->{OpenIDConnect_OP} = $self->{_oidcOPCurrent};
|
2014-11-14 15:29:55 +01:00
|
|
|
$self->{sessionInfo}->{OpenIDConnect_access_token} =
|
|
|
|
$self->{tmp}->{access_token};
|
2015-04-03 15:00:30 +02:00
|
|
|
$self->{sessionInfo}->{OpenIDConnect_IDToken} = $self->{tmp}->{id_token};
|
2014-11-14 15:29:55 +01:00
|
|
|
|
|
|
|
PE_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
## @apmethod int extractFormInfo()
|
|
|
|
# Does nothing
|
|
|
|
# @return Lemonldap::NG::Portal constant
|
|
|
|
sub extractFormInfo {
|
|
|
|
my $self = shift;
|
|
|
|
|
|
|
|
# Check callback
|
2014-12-11 17:54:27 +01:00
|
|
|
my $callback_get_param = $self->{oidcRPCallbackGetParam};
|
2014-11-14 17:18:50 +01:00
|
|
|
my $callback = $self->param($callback_get_param);
|
2014-11-14 15:29:55 +01:00
|
|
|
|
2014-11-20 15:03:32 +01:00
|
|
|
if ($callback) {
|
2014-11-14 15:29:55 +01:00
|
|
|
|
|
|
|
$self->lmLog(
|
|
|
|
"OpenIDConnect callback URI detected: "
|
|
|
|
. $self->url( -path_info => 1, -query => 1 ),
|
|
|
|
'debug'
|
|
|
|
);
|
|
|
|
|
|
|
|
# AuthN Response
|
|
|
|
my $state = $self->param("state");
|
|
|
|
|
2014-11-17 14:55:26 +01:00
|
|
|
# Restore state
|
|
|
|
if ($state) {
|
|
|
|
if ( $self->extractState($state) ) {
|
|
|
|
$self->lmLog( "State $state extracted", 'debug' );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$self->lmLog( "Unable to extract state $state", 'error' );
|
|
|
|
return PE_ERROR;
|
|
|
|
}
|
|
|
|
}
|
2014-11-14 15:29:55 +01:00
|
|
|
|
2014-11-20 15:03:32 +01:00
|
|
|
# Get OpenID Provider
|
|
|
|
my $op = $self->{_oidcOPCurrent};
|
|
|
|
|
|
|
|
unless ($op) {
|
|
|
|
$self->lmLog( "OpenID Provider not found", 'error' );
|
|
|
|
return PE_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
$self->lmLog( "Using OpenID Provider $op", 'debug' );
|
|
|
|
|
2014-11-21 11:32:35 +01:00
|
|
|
# Check error
|
|
|
|
my $error = $self->param("error");
|
|
|
|
if ($error) {
|
|
|
|
my $error_description = $self->param("error_description");
|
|
|
|
my $error_uri = $self->param("error_uri");
|
|
|
|
|
|
|
|
$self->lmLog( "Error returned by $op Provider: $error", 'error' );
|
|
|
|
$self->lmLog( "Error description: $error_description", 'error' )
|
|
|
|
if $error_description;
|
|
|
|
$self->lmLog( "Error URI: $error_uri", 'error' ) if $error_uri;
|
|
|
|
|
|
|
|
return PE_ERROR;
|
|
|
|
}
|
|
|
|
|
2014-11-14 15:29:55 +01:00
|
|
|
# Get access_token and id_token
|
2014-11-21 11:32:35 +01:00
|
|
|
my $code = $self->param("code");
|
2014-11-21 18:15:47 +01:00
|
|
|
my $auth_method =
|
|
|
|
$self->{oidcOPMetaDataOptions}->{$op}
|
|
|
|
->{oidcOPMetaDataOptionsTokenEndpointAuthMethod};
|
2014-11-22 09:46:41 +01:00
|
|
|
|
2014-11-21 18:15:47 +01:00
|
|
|
my $content =
|
|
|
|
$self->getAuthorizationCodeAccessToken( $op, $code, $auth_method );
|
2014-11-14 17:18:50 +01:00
|
|
|
return PE_ERROR unless $content;
|
|
|
|
|
2014-11-14 17:53:56 +01:00
|
|
|
my $json = $self->decodeJSON($content);
|
2014-11-14 15:29:55 +01:00
|
|
|
|
|
|
|
if ( $json->{error} ) {
|
|
|
|
$self->lmLog( "Error in token response:" . $json->{error},
|
|
|
|
'error' );
|
|
|
|
return PE_ERROR;
|
|
|
|
}
|
|
|
|
|
2014-11-22 09:46:41 +01:00
|
|
|
# Check validity of token response
|
|
|
|
unless ( $self->checkTokenResponseValidity($json) ) {
|
|
|
|
$self->lmLog( "Token response is not valid", 'error' );
|
|
|
|
return PE_ERROR;
|
|
|
|
}
|
2014-11-22 09:53:17 +01:00
|
|
|
else {
|
|
|
|
$self->lmLog( "Token response is valid", 'debug' );
|
|
|
|
}
|
2014-11-22 09:46:41 +01:00
|
|
|
|
2014-11-14 15:29:55 +01:00
|
|
|
my $access_token = $json->{access_token};
|
|
|
|
my $id_token = $json->{id_token};
|
|
|
|
|
|
|
|
$self->lmLog( "Access token: $access_token", 'debug' );
|
|
|
|
$self->lmLog( "ID token: $id_token", 'debug' );
|
|
|
|
|
2014-11-17 19:09:55 +01:00
|
|
|
# Verify JWT signature
|
2014-11-20 15:03:32 +01:00
|
|
|
if ( $self->{oidcOPMetaDataOptions}->{$op}
|
|
|
|
->{oidcOPMetaDataOptionsCheckJWTSignature} )
|
|
|
|
{
|
2015-04-25 17:19:12 +02:00
|
|
|
unless ( $self->verifyJWTSignature( $id_token, $op ) ) {
|
2014-11-18 15:24:03 +01:00
|
|
|
$self->lmLog( "JWT signature verification failed", 'error' );
|
|
|
|
return PE_ERROR;
|
|
|
|
}
|
2014-11-18 15:32:15 +01:00
|
|
|
$self->lmLog( "JWT signature verified", 'debug' );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$self->lmLog( "JWT signature check disabled", 'debug' );
|
2014-11-17 19:09:55 +01:00
|
|
|
}
|
2014-11-14 15:29:55 +01:00
|
|
|
|
2014-11-20 10:11:55 +01:00
|
|
|
my $id_token_payload = $self->extractJWT($id_token)->[1];
|
2014-11-14 15:29:55 +01:00
|
|
|
|
|
|
|
my $id_token_payload_hash =
|
2014-11-14 17:53:56 +01:00
|
|
|
$self->decodeJSON( decode_base64($id_token_payload) );
|
2014-11-14 15:29:55 +01:00
|
|
|
|
2015-03-19 16:28:58 +01:00
|
|
|
# Check validity of Access Token (optional)
|
|
|
|
my $at_hash = $id_token_payload_hash->{'at_hash'};
|
|
|
|
if ($at_hash) {
|
2015-03-23 12:54:22 +01:00
|
|
|
unless ( $self->verifyHash( $access_token, $at_hash, $id_token ) ) {
|
2015-03-19 16:28:58 +01:00
|
|
|
$self->lmLog( "Access token hash verification failed",
|
|
|
|
'error' );
|
|
|
|
return PE_ERROR;
|
|
|
|
}
|
|
|
|
$self->lmLog( "Access token hash verified", 'debug' );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$self->lmLog(
|
|
|
|
"No at_hash in ID Token, access token will not be verified",
|
|
|
|
'debug' );
|
|
|
|
}
|
|
|
|
|
2014-11-22 09:53:17 +01:00
|
|
|
# Check validity of ID Token
|
|
|
|
unless ( $self->checkIDTokenValidity( $op, $id_token_payload_hash ) ) {
|
|
|
|
$self->lmLog( "ID Token not valid", 'error' );
|
|
|
|
return PE_ERROR;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$self->lmLog( "ID Token is valid", 'debug' );
|
|
|
|
$self->_dump($id_token_payload_hash);
|
|
|
|
}
|
|
|
|
|
|
|
|
# Get user id defined in 'sub' field
|
2014-11-14 15:29:55 +01:00
|
|
|
my $user_id = $id_token_payload_hash->{sub};
|
|
|
|
|
|
|
|
# Remember tokens
|
|
|
|
$self->{tmp}->{access_token} = $access_token;
|
2015-04-03 15:00:30 +02:00
|
|
|
$self->{tmp}->{id_token} = $id_token;
|
2014-11-14 15:29:55 +01:00
|
|
|
|
|
|
|
$self->lmLog( "Found user_id: " . $user_id, 'debug' );
|
|
|
|
$self->{user} = $user_id;
|
|
|
|
|
|
|
|
return PE_OK;
|
|
|
|
}
|
|
|
|
|
2014-11-20 15:03:32 +01:00
|
|
|
# No callback, choose Provider and send authn request
|
|
|
|
my $op;
|
|
|
|
|
|
|
|
unless ( $op = $self->param("idp") ) {
|
|
|
|
$self->lmLog( "Redirecting user to OP list", 'debug' );
|
|
|
|
|
|
|
|
# Control url parameter
|
|
|
|
my $urlcheck = $self->controlUrlOrigin();
|
|
|
|
return $urlcheck unless ( $urlcheck == PE_OK );
|
|
|
|
|
2014-12-15 15:58:42 +01:00
|
|
|
my @oplist = sort keys %{ $self->{_oidcOPList} };
|
|
|
|
|
2015-10-22 15:40:11 +02:00
|
|
|
# Error if no provider configured
|
|
|
|
if ( $#oplist == -1 ) {
|
|
|
|
$self->lmLog( "No OP configured", 'error' );
|
|
|
|
return PE_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Auto select provider if there is only one
|
2014-12-15 15:58:42 +01:00
|
|
|
if ( $#oplist == 0 ) {
|
|
|
|
$op = shift @oplist;
|
|
|
|
$self->lmLog( "Selecting the only defined OP: $op", 'debug' );
|
2014-11-20 15:03:32 +01:00
|
|
|
}
|
|
|
|
|
2014-12-15 15:58:42 +01:00
|
|
|
else {
|
|
|
|
|
|
|
|
# IDP list
|
|
|
|
my @list = ();
|
2015-06-16 17:43:07 +02:00
|
|
|
|
|
|
|
my $portalPath = $self->{portal};
|
|
|
|
$portalPath =~ s#^https?://[^/]+/?#/#;
|
|
|
|
$portalPath =~ s#[^/]+\.pl$##;
|
|
|
|
|
2014-12-15 15:58:42 +01:00
|
|
|
foreach (@oplist) {
|
2015-06-16 17:43:07 +02:00
|
|
|
my $name = $self->{oidcOPMetaDataOptions}->{$_}
|
|
|
|
->{oidcOPMetaDataOptionsDisplayName};
|
|
|
|
my $icon = $self->{oidcOPMetaDataOptions}->{$_}
|
|
|
|
->{oidcOPMetaDataOptionsIcon};
|
|
|
|
my $img_src;
|
|
|
|
|
|
|
|
if ($icon) {
|
|
|
|
$img_src =
|
|
|
|
( $icon =~ m#^https?://# )
|
|
|
|
? $icon
|
|
|
|
: $portalPath . "skins/common/" . $icon;
|
|
|
|
}
|
|
|
|
|
2014-12-15 15:58:42 +01:00
|
|
|
push @list,
|
|
|
|
{
|
2015-06-16 17:43:07 +02:00
|
|
|
val => $_,
|
|
|
|
name => $name,
|
|
|
|
icon => $img_src,
|
2014-12-15 15:58:42 +01:00
|
|
|
class => "openidconnect",
|
|
|
|
};
|
|
|
|
}
|
|
|
|
$self->{list} = \@list;
|
|
|
|
$self->{confirmRemember} = 0;
|
|
|
|
|
|
|
|
$self->{login} = 1;
|
|
|
|
return PE_CONFIRM;
|
|
|
|
}
|
2014-11-20 15:03:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
# Provider is choosen
|
|
|
|
$self->lmLog( "OpenID Provider $op choosen", 'debug' );
|
|
|
|
|
|
|
|
$self->{_oidcOPCurrent} = $op;
|
|
|
|
|
|
|
|
# AuthN Request
|
|
|
|
$self->lmLog( "Build OpenIDConnect AuthN Request", 'debug' );
|
|
|
|
|
|
|
|
# Save state
|
|
|
|
my $state = $self->storeState(qw/urldc checkLogins _oidcOPCurrent/);
|
|
|
|
|
|
|
|
my $stateSession = $self->storeState();
|
|
|
|
|
|
|
|
# Authorization Code Flow
|
|
|
|
$self->{urldc} = $self->buildAuthorizationCodeAuthnRequest( $op, $state );
|
|
|
|
|
|
|
|
$self->lmLog( "Redirect user to " . $self->{urldc}, 'debug' );
|
|
|
|
|
|
|
|
return $self->_subProcess(qw(autoRedirect));
|
|
|
|
|
2014-11-14 15:29:55 +01:00
|
|
|
PE_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
## @apmethod int authenticate()
|
|
|
|
# Does nothing.
|
|
|
|
# @return Lemonldap::NG::Portal constant
|
|
|
|
sub authenticate {
|
|
|
|
PE_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
## @apmethod int authFinish()
|
|
|
|
# Does nothing.
|
|
|
|
# @return Lemonldap::NG::Portal constant
|
|
|
|
sub authFinish {
|
|
|
|
PE_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
## @apmethod int authLogout()
|
2015-04-03 15:00:30 +02:00
|
|
|
# Send request to endsession endpoint
|
2014-11-14 15:29:55 +01:00
|
|
|
# @return Lemonldap::NG::Portal constant
|
|
|
|
sub authLogout {
|
2015-04-03 15:00:30 +02:00
|
|
|
my $self = shift;
|
|
|
|
|
|
|
|
my $op = $self->{sessionInfo}->{OpenIDConnect_OP};
|
|
|
|
|
|
|
|
# Find endession endpoint
|
|
|
|
my $endsession_endpoint =
|
|
|
|
$self->{_oidcOPList}->{$op}->{conf}->{end_session_endpoint};
|
|
|
|
|
|
|
|
if ($endsession_endpoint) {
|
2015-10-22 11:24:18 +02:00
|
|
|
my $logout_url = $self->{portal};
|
|
|
|
$logout_url =~ s#/$##;
|
|
|
|
$logout_url .= "/?logout=1";
|
2015-04-03 15:00:30 +02:00
|
|
|
my $logout_request =
|
|
|
|
$self->buildLogoutRequest( $endsession_endpoint,
|
|
|
|
$self->{sessionInfo}->{OpenIDConnect_IDToken}, $logout_url );
|
|
|
|
|
|
|
|
$self->lmLog(
|
|
|
|
"OpenID Connect logout to $op will be done on $logout_request",
|
|
|
|
'debug' );
|
|
|
|
|
|
|
|
$self->{urldc} = $logout_request;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$self->lmLog( "No end session endpoint found for $op", 'debug' );
|
|
|
|
}
|
|
|
|
|
2014-11-14 15:29:55 +01:00
|
|
|
PE_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
## @apmethod boolean authForce()
|
|
|
|
# Does nothing
|
|
|
|
# @return result
|
|
|
|
sub authForce {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
## @method string getDisplayType
|
|
|
|
# @return display type
|
|
|
|
sub getDisplayType {
|
|
|
|
return "logo";
|
|
|
|
}
|
|
|
|
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
|
|
|
|
=head1 NAME
|
|
|
|
|
|
|
|
=encoding utf8
|
|
|
|
|
|
|
|
Lemonldap::NG::Portal::AuthOpenIDConnect - Perl extension for building Lemonldap::NG
|
|
|
|
compatible portals with OpenID Connect.
|
|
|
|
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
|
|
|
|
use Lemonldap::NG::Portal::SharedConf;
|
|
|
|
my $portal = new Lemonldap::NG::Portal::Simple(
|
|
|
|
configStorage => {...}, # See Lemonldap::NG::Portal
|
|
|
|
authentication => 'OpenIDConnect',
|
|
|
|
);
|
|
|
|
|
|
|
|
if($portal->process()) {
|
|
|
|
# Write here the menu with CGI methods. This page is displayed ONLY IF
|
|
|
|
# the user was not redirected here.
|
|
|
|
print $portal->header('text/html; charset=utf-8'); # DON'T FORGET THIS (see CGI(3))
|
|
|
|
print "...";
|
|
|
|
|
|
|
|
# or redirect the user to the menu
|
|
|
|
print $portal->redirect( -uri => 'https://portal/menu');
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
print $portal->header('text/html; charset=utf-8'); # DON'T FORGET THIS (see CGI(3))
|
|
|
|
print "<html><body><h1>Unable to work</h1>";
|
|
|
|
print "This server isn't well configured. Contact your administrator.";
|
|
|
|
print "</body></html>";
|
|
|
|
}
|
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
|
|
|
|
OpenID Connect authentication module.
|
|
|
|
|
|
|
|
See L<Lemonldap::NG::Portal::Simple> for usage and other methods.
|
|
|
|
|
|
|
|
=head1 SEE ALSO
|
|
|
|
|
|
|
|
L<Lemonldap::NG::Portal>, L<Lemonldap::NG::Portal::Simple>,
|
|
|
|
L<http://lemonldap-ng.org/>
|
|
|
|
|
|
|
|
=head1 AUTHOR
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item Clement Oudot, E<lt>clem.oudot@gmail.comE<gt>
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=head1 BUG REPORT
|
|
|
|
|
|
|
|
Use OW2 system to report bug or ask for features:
|
|
|
|
L<http://jira.ow2.org>
|
|
|
|
|
|
|
|
=head1 DOWNLOAD
|
|
|
|
|
|
|
|
Lemonldap::NG is available at
|
|
|
|
L<http://forge.objectweb.org/project/showfiles.php?group_id=274>
|
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item Copyright (C) 2014 by Clement Oudot, E<lt>clem.oudot@gmail.comE<gt>
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
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
|
|
|
|
|