lemonldap-ng/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/OpenIDConnect.pm
2017-01-05 13:39:18 +00:00

316 lines
8.8 KiB
Perl

package Lemonldap::NG::Portal::Auth::OpenIDConnect;
use strict;
use Mouse;
use MIME::Base64 qw/encode_base64 decode_base64/;
use Lemonldap::NG::Portal::Main::Constants qw(
PE_CONFIRM
PE_ERROR
PE_OK
);
our $VERSION = '2.0.0';
extends 'Lemonldap::NG::Portal::Auth::Base',
'Lemonldap::NG::Portal::Lib::OpenIDConnect';
# INTERFACE
has opList => ( is => 'rw', default => sub { [] } );
has opNumber => ( is => 'rw', default => 0 );
# INITIALIZATION
sub init {
my ($self) = @_;
return 0 unless ( $self->loadOPs and $self->refreshJWKSdata );
my @tab = ( sort keys %{ $self->oidcOPList } );
unless (@tab) {
$self->lmLog( "No OP configured", 'error' );
return 0;
}
$self->opNumber( scalar @tab );
my @list = ();
my $portalPath = $self->conf->{portal};
$portalPath =~ s#^https?://[^/]+/?#/#;
foreach (@tab) {
my $name = $self->conf->{oidcOPMetaDataOptions}->{$_}
->{oidcOPMetaDataOptionsDisplayName};
my $icon = $self->conf->{oidcOPMetaDataOptions}->{$_}
->{oidcOPMetaDataOptionsIcon};
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",
};
}
$self->opList( [@list] );
return 1;
}
# RUNNING METHODS
sub extractFormInfo {
my ( $self, $req ) = @_;
# Check callback
if ( $req->param( $self->conf->{oidcRPCallbackGetParam} ) ) {
$self->lmLog( 'OpenIDConnect callback URI detected: ' . $req->uri,
'debug' );
# AuthN Response
my $state = $req->param('state');
# Restore state
if ($state) {
if ( $self->extractState( $req, $state ) ) {
$self->lmLog( "State $state extracted", 'debug' );
}
else {
$self->lmLog( "Unable to extract state $state", 'error' );
return PE_ERROR;
}
}
# Get OpenID Provider
my $op = $req->datas->{_oidcOPCurrent};
unless ($op) {
$self->lmLog( "OpenID Provider not found", 'error' );
return PE_ERROR;
}
$self->lmLog( "Using OpenID Provider $op", 'debug' );
# Check error
my $error = $req->param("error");
if ($error) {
my $error_description = $req->param("error_description");
my $error_uri = $req->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;
}
# Get access_token and id_token
my $code = $req->param("code");
my $auth_method =
$self->conf->{oidcOPMetaDataOptions}->{$op}
->{oidcOPMetaDataOptionsTokenEndpointAuthMethod}
|| 'client_secret_post';
my $content =
$self->getAuthorizationCodeAccessToken( $req, $op, $code,
$auth_method );
return PE_ERROR unless $content;
my $json = $self->decodeJSON($content);
if ( $json->{error} ) {
$self->lmLog( "Error in token response:" . $json->{error},
'error' );
return PE_ERROR;
}
# Check validity of token response
unless ( $self->checkTokenResponseValidity($json) ) {
$self->lmLog( "Token response is not valid", 'error' );
return PE_ERROR;
}
else {
$self->lmLog( "Token response is valid", 'debug' );
}
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' );
# Verify JWT signature
if ( $self->conf->{oidcOPMetaDataOptions}->{$op}
->{oidcOPMetaDataOptionsCheckJWTSignature} )
{
unless ( $self->verifyJWTSignature( $id_token, $op ) ) {
$self->lmLog( "JWT signature verification failed", 'error' );
return PE_ERROR;
}
$self->lmLog( "JWT signature verified", 'debug' );
}
else {
$self->lmLog( "JWT signature check disabled", 'debug' );
}
my $id_token_payload = $self->extractJWT($id_token)->[1];
my $id_token_payload_hash =
$self->decodeJSON( decode_base64($id_token_payload) );
# 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 ) ) {
$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' );
}
# 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' );
}
# Get user id defined in 'sub' field
my $user_id = $id_token_payload_hash->{sub};
# Remember tokens
$req->datas->{access_token} = $access_token;
$req->datas->{id_token} = $id_token;
$self->lmLog( "Found user_id: " . $user_id, 'debug' );
$req->user($user_id);
return PE_OK;
}
# No callback, choose Provider and send authn request
my $op;
unless ( $op = $req->param("idp") ) {
$self->lmLog( "Redirecting user to OP list", 'debug' );
# Auto select provider if there is only one
if ( $self->opNumber == 1 ) {
$op = $self->opList->[0]->{val};
$self->lmLog( "Selecting the only defined OP: $op", 'debug' );
}
else {
# IDP list
my @list = ();
my $portalPath = $self->{portal};
$portalPath =~ s#^https?://[^/]+/?#/#;
$req->datas->{list} = $self->opList;
$req->datas->{confirmRemember} = 0;
$req->datas->{login} = 1;
return PE_CONFIRM;
}
}
# Provider is choosen
$self->lmLog( "OpenID Provider $op choosen", 'debug' );
$req->datas->{_oidcOPCurrent} = $op;
# AuthN Request
$self->lmLog( "Build OpenIDConnect AuthN Request", 'debug' );
# Save state
my $state = $self->storeState( $req, qw/urldc checkLogins _oidcOPCurrent/ );
# Authorization Code Flow
$req->urldc(
$self->buildAuthorizationCodeAuthnRequest( $req, $op, $state ) );
$self->lmLog( "Redirect user to " . $req->{urldc}, 'debug' );
$req->continue(1);
$req->steps( [] );
return PE_OK;
}
sub authenticate {
PE_OK;
}
sub setAuthSessionInfo {
my ( $self, $req ) = @_;
my $op = $req->datas->{_oidcOPCurrent};
$req->{sessionInfo}->{authenticationLevel} = $self->conf->{oidcAuthnLevel};
$req->{sessionInfo}->{OpenIDConnect_OP} = $op;
$req->{sessionInfo}->{OpenIDConnect_access_token} =
$req->datas->{access_token};
# Keep ID Token in session
my $store_IDToken = $self->conf->{oidcOPMetaDataOptions}->{$op}
->{oidcOPMetaDataOptionsStoreIDToken};
if ($store_IDToken) {
$self->lmLog( "Store ID Token in session", 'debug' );
$req->{sessionInfo}->{OpenIDConnect_IDToken} = $req->datas->{id_token};
}
else {
$self->lmLog( "ID Token will not be stored in session", 'debug' );
}
PE_OK;
}
sub authLogout {
my ( $self, $req ) = @_;
my $op = $req->{sessionInfo}->{OpenIDConnect_OP};
# 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(
$self->buildLogoutRequest(
$endsession_endpoint,
$self->{sessionInfo}->{OpenIDConnect_IDToken}, $logout_url
)
);
$self->lmLog(
"OpenID Connect logout to $op will be done on " . $req->urldc,
'debug' );
}
else {
$self->lmLog( "No end session endpoint found for $op", 'debug' );
}
PE_OK;
}
sub getDisplayType {
return "logo";
}
1;