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;