lemonldap-ng/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/OpenIDConnect.pm

359 lines
9.9 KiB
Perl
Raw Normal View History

2016-12-31 15:40:26 +01:00
package Lemonldap::NG::Portal::Auth::OpenIDConnect;
2016-12-30 08:03:48 +01:00
use strict;
use Mouse;
2017-01-01 10:43:48 +01:00
use MIME::Base64 qw/encode_base64 decode_base64/;
2016-12-30 08:03:48 +01:00
use Lemonldap::NG::Portal::Main::Constants qw(
PE_ERROR
2018-06-29 17:51:39 +02:00
PE_IDPCHOICE
2016-12-30 08:03:48 +01:00
PE_OK
);
2019-02-12 18:21:38 +01:00
our $VERSION = '2.1.0';
2016-12-30 08:03:48 +01:00
2018-02-19 22:11:43 +01:00
extends 'Lemonldap::NG::Portal::Main::Auth',
2016-12-30 08:03:48 +01:00
'Lemonldap::NG::Portal::Lib::OpenIDConnect';
# INTERFACE
2019-05-11 20:18:43 +02:00
has opList => ( is => 'rw', default => sub { [] } );
2016-12-30 08:03:48 +01:00
has opNumber => ( is => 'rw', default => 0 );
2017-03-11 19:12:03 +01:00
has path => ( is => 'rw', default => 'oauth2' );
2016-12-30 08:03:48 +01:00
use constant sessionKind => 'OIDC';
2016-12-30 08:03:48 +01:00
# INITIALIZATION
sub init {
2019-04-05 22:58:48 +02:00
my $self = shift;
2017-09-26 19:50:38 +02:00
2016-12-31 15:40:26 +01:00
return 0 unless ( $self->loadOPs and $self->refreshJWKSdata );
2016-12-30 08:03:48 +01:00
my @tab = ( sort keys %{ $self->oidcOPList } );
unless (@tab) {
2017-02-15 07:41:50 +01:00
$self->logger->error("No OP configured");
2016-12-30 08:03:48 +01:00
return 0;
}
$self->opNumber( scalar @tab );
2019-05-11 20:18:43 +02:00
my @list = ();
2016-12-31 15:40:26 +01:00
my $portalPath = $self->conf->{portal};
2019-02-05 23:12:17 +01:00
2016-12-30 08:03:48 +01:00
foreach (@tab) {
2019-04-10 22:14:46 +02:00
my $name = $_;
$name =
$self->conf->{oidcOPMetaDataOptions}->{$_}
->{oidcOPMetaDataOptionsDisplayName}
if $self->conf->{oidcOPMetaDataOptions}->{$_}
2016-12-30 08:03:48 +01:00
->{oidcOPMetaDataOptionsDisplayName};
my $icon = $self->conf->{oidcOPMetaDataOptions}->{$_}
->{oidcOPMetaDataOptionsIcon};
2019-04-10 16:37:39 +02:00
my $order = $self->conf->{oidcOPMetaDataOptions}->{$_}
2019-04-10 22:14:46 +02:00
->{oidcOPMetaDataOptionsSortNumber} // 0;
2016-12-30 08:03:48 +01:00
my $img_src;
if ($icon) {
$img_src =
( $icon =~ m#^https?://# )
? $icon
: $portalPath . $self->p->staticPrefix . "/common/" . $icon;
}
push @list,
{
val => $_,
name => $name,
icon => $img_src,
class => "openidconnect",
2019-04-10 16:37:39 +02:00
order => $order
2016-12-30 08:03:48 +01:00
};
}
2017-03-11 19:12:03 +01:00
$self->addRouteFromConf(
'Unauth',
oidcServiceMetaDataFrontChannelURI => 'alreadyLoggedOut',
2017-03-11 19:12:03 +01:00
oidcServiceMetaDataBackChannelURI => 'backLogout',
);
$self->addRouteFromConf(
'Auth',
oidcServiceMetaDataFrontChannelURI => 'frontLogout',
oidcServiceMetaDataBackChannelURI => 'backLogout',
);
2019-04-10 09:21:55 +02:00
@list =
2019-04-10 17:21:33 +02:00
sort {
$a->{order} <=> $b->{order}
2019-04-10 17:21:33 +02:00
or $a->{name} cmp $b->{name}
or $a->{val} cmp $b->{val}
} @list;
2017-01-01 10:43:48 +01:00
$self->opList( [@list] );
2016-12-30 08:03:48 +01:00
return 1;
}
# RUNNING METHODS
sub extractFormInfo {
my ( $self, $req ) = @_;
# Check callback
if ( $req->param( $self->conf->{oidcRPCallbackGetParam} ) ) {
2017-02-15 07:41:50 +01:00
$self->logger->debug(
'OpenIDConnect callback URI detected: ' . $req->uri );
2016-12-30 08:03:48 +01:00
# AuthN Response
my $state = $req->param('state');
# Restore state
if ($state) {
2017-01-01 10:43:48 +01:00
if ( $self->extractState( $req, $state ) ) {
2017-02-15 07:41:50 +01:00
$self->logger->debug("State $state extracted");
2016-12-30 08:03:48 +01:00
}
else {
$self->userLogger->error("Unable to extract state $state");
2016-12-30 08:03:48 +01:00
return PE_ERROR;
}
}
# Get OpenID Provider
my $op = $req->data->{_oidcOPCurrent};
2016-12-30 08:03:48 +01:00
unless ($op) {
$self->userLogger->error("OpenID Provider not found");
2016-12-30 08:03:48 +01:00
return PE_ERROR;
}
2017-02-15 07:41:50 +01:00
$self->logger->debug("Using OpenID Provider $op");
2016-12-30 08:03:48 +01:00
# Check error
my $error = $req->param("error");
if ($error) {
my $error_description = $req->param("error_description");
my $error_uri = $req->param("error_uri");
2017-02-15 07:41:50 +01:00
$self->logger->error("Error returned by $op Provider: $error");
$self->logger->error("Error description: $error_description")
2016-12-30 08:03:48 +01:00
if $error_description;
2017-02-15 07:41:50 +01:00
$self->logger->error("Error URI: $error_uri") if $error_uri;
2016-12-30 08:03:48 +01:00
return PE_ERROR;
}
# Get access_token and id_token
my $code = $req->param("code");
my $auth_method =
$self->conf->{oidcOPMetaDataOptions}->{$op}
2017-01-01 10:43:48 +01:00
->{oidcOPMetaDataOptionsTokenEndpointAuthMethod}
|| 'client_secret_post';
2016-12-30 08:03:48 +01:00
my $content =
$self->getAuthorizationCodeAccessToken( $req, $op, $code,
$auth_method );
return PE_ERROR unless $content;
my $json = $self->decodeJSON($content);
if ( $json->{error} ) {
2017-02-15 07:41:50 +01:00
$self->logger->error( "Error in token response:" . $json->{error} );
2016-12-30 08:03:48 +01:00
return PE_ERROR;
}
# Check validity of token response
unless ( $self->checkTokenResponseValidity($json) ) {
2017-02-15 07:41:50 +01:00
$self->logger->error("Token response is not valid");
2016-12-30 08:03:48 +01:00
return PE_ERROR;
}
else {
2017-02-15 07:41:50 +01:00
$self->logger->debug("Token response is valid");
2016-12-30 08:03:48 +01:00
}
my $access_token = $json->{access_token};
my $id_token = $json->{id_token};
2017-02-15 07:41:50 +01:00
$self->logger->debug("Access token: $access_token");
$self->logger->debug("ID token: $id_token");
2016-12-30 08:03:48 +01:00
# Verify JWT signature
if ( $self->conf->{oidcOPMetaDataOptions}->{$op}
->{oidcOPMetaDataOptionsCheckJWTSignature} )
{
unless ( $self->verifyJWTSignature( $id_token, $op ) ) {
2017-02-15 07:41:50 +01:00
$self->logger->error("JWT signature verification failed");
2016-12-30 08:03:48 +01:00
return PE_ERROR;
}
2017-02-15 07:41:50 +01:00
$self->logger->debug("JWT signature verified");
2016-12-30 08:03:48 +01:00
}
else {
2017-02-15 07:41:50 +01:00
$self->logger->debug("JWT signature check disabled");
2016-12-30 08:03:48 +01:00
}
my $id_token_payload = $self->extractJWT($id_token)->[1];
my $id_token_payload_hash =
2019-12-19 17:31:02 +01:00
$self->decodeJSON( $self->decodeBase64url($id_token_payload) );
2016-12-30 08:03:48 +01:00
# Check validity of Access Token (optional)
my $at_hash = $id_token_payload_hash->{at_hash};
if ($at_hash) {
unless ( $self->verifyHash( $access_token, $at_hash, $id_token ) ) {
2017-02-15 07:41:50 +01:00
$self->userLogger->error(
"Access token hash verification failed");
2016-12-30 08:03:48 +01:00
return PE_ERROR;
}
2017-02-15 07:41:50 +01:00
$self->logger->debug("Access token hash verified");
2016-12-30 08:03:48 +01:00
}
else {
2017-02-15 07:41:50 +01:00
$self->logger->debug(
"No at_hash in ID Token, access token will not be verified");
2016-12-30 08:03:48 +01:00
}
# Check validity of ID Token
unless ( $self->checkIDTokenValidity( $op, $id_token_payload_hash ) ) {
$self->userLogger->error('ID Token not valid');
2016-12-30 08:03:48 +01:00
return PE_ERROR;
}
else {
2017-02-15 07:41:50 +01:00
$self->logger->debug('ID Token is valid');
2016-12-30 08:03:48 +01:00
}
# Get user id defined in 'sub' field
my $user_id = $id_token_payload_hash->{sub};
# Remember tokens
$req->data->{access_token} = $access_token;
$req->data->{id_token} = $id_token;
2016-12-30 08:03:48 +01:00
2017-02-15 07:41:50 +01:00
$self->logger->debug( "Found user_id: " . $user_id );
2017-01-01 10:43:48 +01:00
$req->user($user_id);
2016-12-30 08:03:48 +01:00
return PE_OK;
}
# No callback, choose Provider and send authn request
my $op;
unless ( $op = $req->param("idp") ) {
2017-02-15 07:41:50 +01:00
$self->logger->debug("Redirecting user to OP list");
2016-12-30 08:03:48 +01:00
# Auto select provider if there is only one
if ( $self->opNumber == 1 ) {
2017-01-01 10:43:48 +01:00
$op = $self->opList->[0]->{val};
2017-02-15 07:41:50 +01:00
$self->logger->debug("Selecting the only defined OP: $op");
2016-12-30 08:03:48 +01:00
}
else {
# IDP list
2019-04-14 21:13:43 +02:00
my $portalPath = $self->{conf}->{portal};
2016-12-30 08:03:48 +01:00
$portalPath =~ s#^https?://[^/]+/?#/#;
2019-12-19 17:31:02 +01:00
$req->data->{list} = $self->opList;
2016-12-30 08:03:48 +01:00
$req->data->{login} = 1;
2018-06-29 17:51:39 +02:00
return PE_IDPCHOICE;
2016-12-30 08:03:48 +01:00
}
}
# Provider is choosen
2017-02-15 07:41:50 +01:00
$self->logger->debug("OpenID Provider $op choosen");
2016-12-30 08:03:48 +01:00
$req->data->{_oidcOPCurrent} = $op;
2016-12-30 08:03:48 +01:00
# AuthN Request
2017-02-15 07:41:50 +01:00
$self->logger->debug("Build OpenIDConnect AuthN Request");
2016-12-30 08:03:48 +01:00
# Save state
2017-01-01 10:43:48 +01:00
my $state = $self->storeState( $req, qw/urldc checkLogins _oidcOPCurrent/ );
2016-12-30 08:03:48 +01:00
# Authorization Code Flow
2017-01-01 10:43:48 +01:00
$req->urldc(
$self->buildAuthorizationCodeAuthnRequest( $req, $op, $state ) );
2016-12-30 08:03:48 +01:00
2017-02-15 07:41:50 +01:00
$self->logger->debug( "Redirect user to " . $req->{urldc} );
2017-01-01 10:43:48 +01:00
$req->continue(1);
$req->steps( [] );
2016-12-30 08:03:48 +01:00
return PE_OK;
}
sub authenticate {
PE_OK;
}
sub setAuthSessionInfo {
my ( $self, $req ) = @_;
my $op = $req->data->{_oidcOPCurrent};
2016-12-30 08:03:48 +01:00
$req->{sessionInfo}->{authenticationLevel} = $self->conf->{oidcAuthnLevel};
2017-03-19 07:29:35 +01:00
$req->{sessionInfo}->{_oidc_OP} = $op;
$req->{sessionInfo}->{_oidc_access_token} =
$req->data->{access_token};
2016-12-30 08:03:48 +01:00
# Keep ID Token in session
2017-01-01 10:43:48 +01:00
my $store_IDToken = $self->conf->{oidcOPMetaDataOptions}->{$op}
->{oidcOPMetaDataOptionsStoreIDToken};
2016-12-30 08:03:48 +01:00
if ($store_IDToken) {
2017-02-15 07:41:50 +01:00
$self->logger->debug("Store ID Token in session");
$req->{sessionInfo}->{_oidc_id_token} = $req->data->{id_token};
2016-12-30 08:03:48 +01:00
}
else {
2017-02-15 07:41:50 +01:00
$self->logger->debug("ID Token will not be stored in session");
2016-12-30 08:03:48 +01:00
}
PE_OK;
}
sub authLogout {
my ( $self, $req ) = @_;
2017-03-19 07:29:35 +01:00
my $op = $req->{sessionInfo}->{_oidc_OP};
2016-12-30 08:03:48 +01:00
# Find endession endpoint
my $endsession_endpoint =
$self->oidcOPList->{$op}->{conf}->{end_session_endpoint};
if ($endsession_endpoint) {
my $logout_url = $self->conf->{portal} . '?logout=1';
$req->urldc(
2017-01-01 10:43:48 +01:00
$self->buildLogoutRequest(
$endsession_endpoint, $req->{sessionInfo}->{_oidc_id_token},
$logout_url
2017-01-01 10:43:48 +01:00
)
);
2016-12-30 08:03:48 +01:00
2017-02-15 07:41:50 +01:00
$self->logger->debug(
"OpenID Connect logout to $op will be done on " . $req->urldc );
2016-12-30 08:03:48 +01:00
}
else {
2017-02-15 07:41:50 +01:00
$self->logger->debug("No end session endpoint found for $op");
2016-12-30 08:03:48 +01:00
}
PE_OK;
}
2019-03-19 05:56:36 +01:00
sub getForm {
2016-12-30 08:03:48 +01:00
return "logo";
}
sub alreadyLoggedOut {
my ( $self, $req ) = @_;
$self->userLogger->info(
'Front-channel logout request for an already logged out user');
2017-03-14 15:40:09 +01:00
my $img = $self->conf->{staticPrefix} . '/common/icons/ok.png';
# No need to protect this frame
$req->frame(1);
my $frame = qq'<html><body><img src="$img"></body></html>';
return [
200,
[ 'Content-Type' => 'text/html', 'Content-Length' => length($frame) ],
[$frame]
];
}
2017-03-11 19:12:03 +01:00
sub frontLogout {
my ( $self, $req ) = @_;
}
sub backLogout {
my ( $self, $req ) = @_;
}
2016-12-30 08:03:48 +01:00
1;