package Lemonldap::NG::Portal::Issuer::OpenIDConnect;

use strict;
use JSON;
use Mouse;
use Lemonldap::NG::Common::FormEncode;
use Lemonldap::NG::Portal::Main::Constants qw(
  PE_BADURL
  PE_CONFIRM
  PE_ERROR
  PE_LOGOUT_OK
  PE_REDIRECT
  PE_OK
  PE_UNAUTHORIZEDPARTNER
);

our $VERSION = '2.0.0';

extends 'Lemonldap::NG::Portal::Main::Issuer',
  'Lemonldap::NG::Portal::Lib::OpenIDConnect',
  'Lemonldap::NG::Common::Conf::AccessLib';

# INTERFACE

sub beforeAuth { 'exportRequestParameters' }

# INITIALIZATION

has configStorage => (
    is      => 'ro',
    lazy    => 1,
    default => sub {
        $_[0]->{p}->HANDLER->localConfig->{configStorage};
    }
);

# OIDC has 7 endpoints managed here as PSGI endpoints or in run() [Main/Issuer.pm
# manage transparent authentication for run()]:
#  - authorize   : in run()
#  - logout      : in run()
#                  => endSessionDone() for unauth users
#  - checksession: => checkSession()   for all
#  - token       : => token()          for unauth users (RP)
#  - userinfo    : => userInfo()       for unauth users (RP)
#  - jwks        : => jwks()           for unauth users (RP)
#  - register    : => registration()   for unauth users (RP)
#
# Other paths will be handle by run() and return PE_ERROR
#
# .well-known/openid-configuration is handled by metadata()

sub init {
    my ($self) = @_;

    # Initialize RP list
    return 0
      unless ( $self->Lemonldap::NG::Portal::Main::Issuer::init()
        and $self->loadRPs );

    # Manage RP requests
    $self->addRouteFromConf(
        'Unauth',
        oidcServiceMetaDataEndSessionURI   => 'endSessionDone',
        oidcServiceMetaDataCheckSessionURI => 'checkSession',
        oidcServiceMetaDataTokenURI        => 'token',
        oidcServiceMetaDataUserInfoURI     => 'userInfo',
        oidcServiceMetaDataJWKSURI         => 'jwks',
        oidcServiceMetaDataRegistrationURI => 'registration',
    );

    # Manage user requests
    $self->addRouteFromConf(
        'Auth',
        oidcServiceMetaDataCheckSessionURI => 'checkSession',
        oidcServiceMetaDataTokenURI        => 'badAuthRequest',
        oidcServiceMetaDataUserInfoURI     => 'badAuthRequest',
        oidcServiceMetaDataJWKSURI         => 'badAuthRequest',
        oidcServiceMetaDataRegistrationURI => 'badAuthRequest',
    );

    # Metadata (.well-known/openid-configuration)
    $self->addUnauthRoute(
        '.well-known' => { 'openid-configuration' => 'metadata' },
        ['GET']
    );
    $self->addAuthRoute(
        '.well-known' => { 'openid-configuration' => 'metadata' },
        ['GET']
    );
    return 1;
}

# RUNNING METHODS

# Main method (launched only for authenticated users, see Main/Issuer.pm)
# run() manages only "authorize" and "logout" endpoints.
sub run {
    my ( $self, $req, $path ) = @_;
    if ($path) {

        # AUTHORIZE
        if ( $path eq $self->conf->{oidcServiceMetaDataAuthorizeURI} ) {
            $self->logger->debug(
                "URL  detected as an OpenID Connect AUTHORIZE URL");

            # Get and save parameters
            my $oidc_request = {};
            foreach my $param (
                qw/response_type scope client_id state redirect_uri nonce
                response_mode display prompt max_age ui_locales id_token_hint
                login_hint acr_values request request_uri/
              )
            {
                if ( $req->param($param) ) {
                    $oidc_request->{$param} = $req->param($param);
                    $self->logger->debug( "OIDC request parameter $param: "
                          . $oidc_request->{$param} );
                    $self->p->setHiddenFormValue( $req, $param,
                        $oidc_request->{$param},
                        '', 0 );
                }
            }

            # Detect requested flow
            my $response_type = $oidc_request->{'response_type'};
            my $flow          = $self->getFlowType($response_type);

            unless ($flow) {
                $self->logger->error("Unknown response type: $response_type");
                return PE_ERROR;
            }
            $self->logger->debug(
                "OIDC $flow flow requested (response type: $response_type)");

            # Extract request_uri/request parameter
            if ( $oidc_request->{'request_uri'} ) {
                my $request =
                  $self->getRequestJWT( $oidc_request->{'request_uri'} );

                if ($request) {
                    $oidc_request->{'request'} = $request;
                }
                else {
                    $self->logger->error("Error with Request URI resolution");
                    return PE_ERROR;
                }
            }

            if ( $oidc_request->{'request'} ) {
                my $request =
                  $self->getJWTJSONData( $oidc_request->{'request'} );

                # Override OIDC parameters by request content
                foreach ( keys %$request ) {
                    $self->logger->debug(
"Override $_ OIDC param by value present in request parameter"
                    );
                    $oidc_request->{$_} = $request->{$_};
                    $self->p->setHiddenFormValue( $req, $_, $request->{$_},
                        '' );
                }
            }

            # Check all required parameters
            unless ( $oidc_request->{'redirect_uri'} ) {
                $self->logger->error("Redirect URI is required");
                return PE_ERROR;
            }
            unless ( $oidc_request->{'scope'} ) {
                $self->logger->error("Scope is required");
                return PE_ERROR;
            }
            unless ( $oidc_request->{'client_id'} ) {
                $self->logger->error("Client ID is required");
                return PE_ERROR;
            }
            if ( $flow eq "implicit" and not defined $oidc_request->{'nonce'} )
            {
                $self->logger->error("Nonce is required for implicit flow");
                return PE_ERROR;
            }

            # Check client_id
            my $client_id = $oidc_request->{'client_id'};
            $self->logger->debug("Request from client id $client_id");

            # Verify that client_id is registered in configuration
            my $rp = $self->getRP($client_id);

            unless ($rp) {
                $self->logger->error(
"No registered Relying Party found with client_id $client_id"
                );
                return PE_UNAUTHORIZEDPARTNER;
            }
            else {
                $self->logger->debug("Client id $client_id match RP $rp");
            }

            # Check if this RP is authorizated
            if ( my $rule = $self->rpRules->{$rp} ) {
                unless ( $rule->( $req, $req->sessionInfo ) ) {
                    $self->userLogger->warn( 'User '
                          . $req->sessionInfo->{ $self->conf->{whatToTrace} }
                          . "was not authorizated to access to $rp" );
                    return PE_UNAUTHORIZEDPARTNER;
                }
            }

            # Check redirect_uri
            my $redirect_uri  = $oidc_request->{'redirect_uri'};
            my $redirect_uris = $self->conf->{oidcRPMetaDataOptions}->{$rp}
              ->{oidcRPMetaDataOptionsRedirectUris};

            if ($redirect_uris) {
                my $redirect_uri_allowed = 0;
                foreach ( split( /\s+/, $redirect_uris ) ) {
                    $redirect_uri_allowed = 1 if $redirect_uri eq $_;
                }
                unless ($redirect_uri_allowed) {
                    $self->userLogger->error(
                        "Redirect URI $redirect_uri not allowed");
                    return PE_BADURL;
                }
            }

            # Check if flow is allowed
            if ( $flow eq "authorizationcode"
                and not $self->conf->{oidcServiceAllowAuthorizationCodeFlow} )
            {
                $self->userLogger->error(
                    "Authorization code flow is not allowed");
                return $self->returnRedirectError(
                    $req,           $oidc_request->{'redirect_uri'},
                    "server_error", "Authorization code flow not allowed",
                    undef,          $oidc_request->{'state'},
                    0
                );
            }
            if ( $flow eq "implicit"
                and not $self->conf->{oidcServiceAllowImplicitFlow} )
            {
                $self->logger->error("Implicit flow is not allowed");
                return $self->returnRedirectError(
                    $req,           $oidc_request->{'redirect_uri'},
                    "server_error", "Implicit flow not allowed",
                    undef,          $oidc_request->{'state'},
                    1
                );
            }
            if ( $flow eq "hybrid"
                and not $self->conf->{oidcServiceAllowHybridFlow} )
            {
                $self->logger->error("Hybrid flow is not allowed");
                return $self->returnRedirectError(
                    $req,           $oidc_request->{'redirect_uri'},
                    "server_error", "Hybrid flow not allowed",
                    undef,          $oidc_request->{'state'},
                    1
                );
            }

            # Check if user needs to be reauthenticated
            my $prompt = $oidc_request->{'prompt'};
            if (
                    $prompt
                and $prompt =~ /\blogin\b/
                and (
                    time - $req->sessionInfo->{_utime} >
                    $self->conf->{portalForceAuthnInterval} )
              )
            {
                $self->logger->debug(
"Reauthentication requested by Relying Party in prompt parameter"
                );
                return $self->reAuth($req);
            }

            my $max_age         = $oidc_request->{'max_age'};
            my $_lastAuthnUTime = $req->{sessionInfo}->{_lastAuthnUTime};
            if ( $max_age && time > $_lastAuthnUTime + $max_age ) {
                $self->logger->debug(
"Reauthentication forced cause authentication time ($_lastAuthnUTime) is too old (>$max_age s)"
                );
                return $self->reAuth($req);
            }

            # Check openid scope
            unless ( $oidc_request->{'scope'} =~ /\bopenid\b/ ) {
                $self->logger->debug("No openid scope found");

                #TODO manage standard OAuth request
                return PE_OK;
            }

            # Check Request JWT signature
            if ( $oidc_request->{'request'} ) {
                unless (
                    $self->verifyJWTSignature(
                        $oidc_request->{'request'},
                        undef, $rp
                    )
                  )
                {
                    $self->logger->error(
                        "Request JWT signature could not be verified");
                    return PE_ERROR;
                }
                else {
                    $self->logger->debug("Request JWT signature verified");
                }
            }

            # Check id_token_hint
            my $id_token_hint = $oidc_request->{'id_token_hint'};
            if ($id_token_hint) {

                $self->logger->debug("Check sub of ID Token $id_token_hint");

                # Check that id_token_hint sub match current user
                my $sub = $self->getIDTokenSub($id_token_hint);
                my $user_id_attribute =
                  $self->conf->{oidcRPMetaDataOptions}->{$rp}
                  ->{oidcRPMetaDataOptionsUserIDAttr}
                  || $self->conf->{whatToTrace};
                my $user_id = $req->{sessionInfo}->{$user_id_attribute};
                unless ( $sub eq $user_id ) {
                    $self->userLogger->error(
                        "ID Token hint sub $sub do not match user $user_id");
                    return $self->returnRedirectError(
                        $req,
                        $oidc_request->{'redirect_uri'},
                        'invalid_request',
                        "current user do not match id_token_hint sub",
                        undef,
                        $oidc_request->{'state'},
                        ( $flow ne "authorizationcode" )
                    );
                }
                else {
                    $self->logger->debug(
                        "ID Token hint sub $sub match current user");
                }
            }

            # Obtain consent
            my $bypassConsent = $self->conf->{oidcRPMetaDataOptions}->{$rp}
              ->{oidcRPMetaDataOptionsBypassConsent};
            if ($bypassConsent) {
                $self->logger->debug(
                    "Consent is disabled for RP $rp, user will not be prompted"
                );
            }
            else {
                my $ask_for_consent = 1;
                if (    $req->{sessionInfo}->{"_oidc_consent_time_$rp"}
                    and $req->{sessionInfo}->{"_oidc_consent_scope_$rp"} )
                {
                    $ask_for_consent = 0;
                    my $consent_time =
                      $req->{sessionInfo}->{"_oidc_consent_time_$rp"};
                    my $consent_scope =
                      $req->{sessionInfo}->{"_oidc_consent_scope_$rp"};

                    $self->logger->debug(
"Consent already given for Relying Party $rp (time: $consent_time, scope: $consent_scope)"
                    );

                    # Check accepted scope
                    foreach my $requested_scope (
                        split( /\s+/, $oidc_request->{'scope'} ) )
                    {
                        if ( $consent_scope =~ /\b$requested_scope\b/ ) {
                            $self->logger->debug(
                                "Scope $requested_scope already accepted");
                        }
                        else {
                            $self->logger->debug(
"Scope $requested_scope was not previously accepted"
                            );
                            $ask_for_consent = 1;
                            last;
                        }
                    }

                    # Check prompt parameter
                    $ask_for_consent = 1 if ( $prompt =~ /\bconsent\b/ );
                }
                if ($ask_for_consent) {
                    if (    $req->param('confirm')
                        and $req->param('confirm') == 1 )
                    {
                        $self->p->updatePersistentSession(
                            { "_oidc_consent_time_$rp" => time } );
                        $self->p->updatePersistentSession(
                            {
                                "_oidc_consent_scope_$rp" =>
                                  $oidc_request->{'scope'}
                            }
                        );
                        $self->logger->debug(
                            "Consent given for Relying Party $rp");
                    }
                    elsif ( $req->param('confirm')
                        and $req->param('confirm') == -1 )
                    {
                        $self->logger->debug(
                            "User refused consent for Relying party $rp");
                        return $self->returnRedirectError(
                            $req,
                            $oidc_request->{'redirect_uri'},
                            "consent_required",
                            "consent not given",
                            undef,
                            $oidc_request->{'state'},
                            ( $flow ne "authorizationcode" )
                        );
                    }
                    else {
                        $self->logger->debug(
                            "Obtain user consent for Relying Party $rp");

                        # Return error if prompt is none
                        if ( $prompt and $prompt =~ /\bnone\b/ ) {
                            $self->logger->debug(
                                "Consent is needed but prompt is none");
                            return $self->returnRedirectError(
                                $req,
                                $oidc_request->{'redirect_uri'},
                                "consent_required",
                                "consent required",
                                undef,
                                $oidc_request->{'state'},
                                ( $flow ne "authorizationcode" )
                            );
                        }

                        my $display_name =
                          $self->conf->{oidcRPMetaDataOptions}->{$rp}
                          ->{oidcRPMetaDataOptionsDisplayName};
                        my $icon = $self->conf->{oidcRPMetaDataOptions}->{$rp}
                          ->{oidcRPMetaDataOptionsIcon};
                        my $img_src;

                        if ($icon) {
                            $img_src =
                              ( $icon =~ m#^https?://# )
                              ? $icon
                              : $self->p->staticPrefic . "/common/" . $icon;
                        }

                        # HERE
                        $req->info('<div class="oidc_consent_message">');
                        $req->info( '<img src="' . $img_src . '" />' )
                          if $img_src;
                        $req->info(
qq'<h3 trspan="oidcConsent,$display_name">The application $display_name would like to know:</h3><ul>'
                        );

                        my $scope_messages = {
                            openid  => 'yourIdentity',
                            profile => 'yourProfile',
                            email   => 'yourEmail',
                            address => 'yourAddress',
                            phone   => 'yourPhone',
                        };
                        foreach my $requested_scope (
                            split( /\s/, $oidc_request->{'scope'} ) )
                        {
                            my $message = $scope_messages->{$requested_scope}
                              || 'anotherInformation';
                            $req->info(
                                qq'<li trspan="$message">$message</li>');
                        }
                        $req->info('</ul></div>');
                        $req->datas->{activeTimer} = 0;
                        return PE_CONFIRM;
                    }
                }
            }

            # Create session_state
            my $session_state =
              $self->createSessionState( $req->id, $client_id );

            # Authorization Code Flow
            if ( $flow eq "authorizationcode" ) {

                # Store data in session
                my $codeSession = $self->getOpenIDConnectSession(
                    undef,
                    {
                        redirect_uri    => $oidc_request->{'redirect_uri'},
                        scope           => $oidc_request->{'scope'},
                        user_session_id => $req->id,
                        _utime          => time,
                        nonce           => $oidc_request->{'nonce'},
                    }
                );

                # Generate code
                my $code = $codeSession->id();

                $self->logger->debug("Generated code: $code");

                # Build Response
                my $response_url = $self->buildAuthorizationCodeAuthnResponse(
                    $oidc_request->{'redirect_uri'},
                    $code, $oidc_request->{'state'},
                    $session_state
                );

                $self->logger->debug("Redirect user to $response_url");
                $req->urldc($response_url);

                return PE_REDIRECT;
            }

            # Implicit Flow
            if ( $flow eq "implicit" ) {

                my $access_token;
                my $at_hash;

                if ( $response_type =~ /\btoken\b/ ) {

                    # Store data in access token
                    # Generate access_token
                    my $accessTokenSession = $self->getOpenIDConnectSession(
                        undef,
                        {
                            scope           => $oidc_request->{'scope'},
                            rp              => $rp,
                            user_session_id => $req->id,
                            _utime          => time,
                        }
                    );

                    unless ($accessTokenSession) {
                        $self->logger->error(
                            "Unable to create OIDC session for access_token");
                        $self->returnRedirectError( $req,
                            $oidc_request->{'redirect_uri'},
                            "server_error", undef, undef,
                            $oidc_request->{'state'}, 1 );
                    }

                    $access_token = $accessTokenSession->id;

                    $self->logger->debug(
                        "Generated access token: $access_token");

                    # Compute hash to store in at_hash
                    my $alg = $self->conf->{oidcRPMetaDataOptions}->{$rp}
                      ->{oidcRPMetaDataOptionsIDTokenSignAlg};
                    my ($hash_level) = ( $alg =~ /(?:\w{2})(\d{3})/ );
                    $at_hash = $self->createHash( $access_token, $hash_level );
                }

                # ID token payload
                my $id_token_exp = $self->conf->{oidcRPMetaDataOptions}->{$rp}
                  ->{oidcRPMetaDataOptionsIDTokenExpiration};
                $id_token_exp += time;

                my $authenticationLevel =
                  $req->{sessionInfo}->{authenticationLevel};
                my $id_token_acr;
                foreach (
                    keys %{ $self->conf->{oidcServiceMetaDataAuthnContext} } )
                {
                    if ( $self->conf->{oidcServiceMetaDataAuthnContext}->{$_} eq
                        $authenticationLevel )
                    {
                        $id_token_acr = $_;
                        last;
                    }
                }

                my $user_id_attribute =
                  $self->conf->{oidcRPMetaDataOptions}->{$rp}
                  ->{oidcRPMetaDataOptionsUserIDAttr}
                  || $self->conf->{whatToTrace};
                my $user_id = $req->{sessionInfo}->{$user_id_attribute};

                my $id_token_payload_hash = {
                    iss => $self->conf->{oidcServiceMetaDataIssuer}
                    ,    # Issuer Identifier
                    sub => $user_id,         # Subject Identifier
                    aud => [$client_id],     # Audience
                    exp => $id_token_exp,    # expiration
                    iat => time,             # Issued time
                    auth_time => $req->{sessionInfo}->{_lastAuthnUTime}
                    ,                        # Authentication time
                    azp   => $client_id,                 # Authorized party
                                                         # TODO amr
                    nonce => $oidc_request->{'nonce'}    # Nonce
                };

                $id_token_payload_hash->{'at_hash'} = $at_hash if $at_hash;
                $id_token_payload_hash->{'acr'} = $id_token_acr
                  if $id_token_acr;

                # Create ID Token
                my $id_token =
                  $self->createIDToken( $id_token_payload_hash, $rp );

                $self->logger->debug("Generated id token: $id_token");

                # Send token response
                my $expires_in = $self->conf->{oidcRPMetaDataOptions}->{$rp}
                  ->{oidcRPMetaDataOptionsAccessTokenExpiration};

                # Build Response
                my $response_url = $self->buildImplicitAuthnResponse(
                    $oidc_request->{'redirect_uri'},
                    $access_token, $id_token, $expires_in,
                    $oidc_request->{'state'},
                    $session_state
                );

                $self->logger->debug("Redirect user to $response_url");
                $req->urldc($response_url);

                return PE_REDIRECT;
            }

            # Hybrid Flow
            if ( $flow eq "hybrid" ) {

                my $access_token;
                my $id_token;
                my $at_hash;
                my $c_hash;

                # Hash level
                my $alg = $self->conf->{oidcRPMetaDataOptions}->{$rp}
                  ->{oidcRPMetaDataOptionsIDTokenSignAlg};
                my ($hash_level) = ( $alg =~ /(?:\w{2})(\d{3})/ );

                # Store data in session
                my $codeSession = $self->getOpenIDConnectSession(
                    undef,
                    {
                        redirect_uri    => $oidc_request->{'redirect_uri'},
                        scope           => $oidc_request->{'scope'},
                        user_session_id => $req->id,
                        _utime          => time,
                        nonce           => $oidc_request->{'nonce'},
                    }
                );

                # Generate code
                my $code = $codeSession->id();

                $self->logger->debug("Generated code: $code");

                # Compute hash to store in c_hash
                $c_hash = $self->createHash( $code, $hash_level );

                if ( $response_type =~ /\btoken\b/ ) {

                    # Generate access_token
                    my $accessTokenSession = $self->getOpenIDConnectSession(
                        undef,
                        {
                            scope           => $oidc_request->{'scope'},
                            rp              => $rp,
                            user_session_id => $req->id,
                            _utime          => time,
                        }
                    );

                    unless ($accessTokenSession) {
                        $self->logger->error(
                            "Unable to create OIDC session for access_token");
                        return $self->returnRedirectError( $req,
                            $oidc_request->{'redirect_uri'},
                            "server_error", undef, undef,
                            $oidc_request->{'state'}, 1 );
                    }

                    $access_token = $accessTokenSession->id;

                    $self->logger->debug(
                        "Generated access token: $access_token");

                    # Compute hash to store in at_hash
                    $at_hash = $self->createHash( $access_token, $hash_level );
                }

                if ( $response_type =~ /\bid_token\b/ ) {

                    # ID token payload
                    my $id_token_exp =
                      $self->conf->{oidcRPMetaDataOptions}->{$rp}
                      ->{oidcRPMetaDataOptionsIDTokenExpiration};
                    $id_token_exp += time;

                    my $id_token_acr =
                      "loa-" . $req->{sessionInfo}->{authenticationLevel};

                    my $user_id_attribute =
                      $self->conf->{oidcRPMetaDataOptions}->{$rp}
                      ->{oidcRPMetaDataOptionsUserIDAttr}
                      || $self->conf->{whatToTrace};
                    my $user_id = $req->{sessionInfo}->{$user_id_attribute};

                    my $id_token_payload_hash = {
                        iss => $self->conf->{oidcServiceMetaDataIssuer}
                        ,    # Issuer Identifier
                        sub => $user_id,         # Subject Identifier
                        aud => [$client_id],     # Audience
                        exp => $id_token_exp,    # expiration
                        iat => time,             # Issued time
                        auth_time => $req->{sessionInfo}->{_lastAuthnUTime}
                        ,                        # Authentication time
                        acr => $id_token_acr
                        ,    # Authentication Context Class Reference
                        azp   => $client_id,                 # Authorized party
                                                             # TODO amr
                        nonce => $oidc_request->{'nonce'}    # Nonce
                    };

                    $id_token_payload_hash->{'at_hash'} = $at_hash if $at_hash;
                    $id_token_payload_hash->{'c_hash'}  = $c_hash  if $c_hash;

                    # Create ID Token
                    $id_token =
                      $self->createIDToken( $id_token_payload_hash, $rp );

                    $self->logger->debug("Generated id token: $id_token");
                }

                my $expires_in = $self->conf->{oidcRPMetaDataOptions}->{$rp}
                  ->{oidcRPMetaDataOptionsAccessTokenExpiration};

                # Build Response
                my $response_url = $self->buildHybridAuthnResponse(
                    $oidc_request->{'redirect_uri'}, $code,
                    $access_token,                   $id_token,
                    $expires_in,                     $oidc_request->{'state'},
                    $session_state
                );

                $self->logger->debug("Redirect user to $response_url");
                $req->urldc($response_url);
                return PE_REDIRECT;
            }

            $self->logger->debug("No flow has been selected");
            return PE_OK;
        }

        # LOGOUT
        elsif ( $path eq $self->conf->{oidcServiceMetaDataEndSessionURI} ) {
            $self->logger->debug(
                "URL detected as an OpenID Connect END SESSION URL");

            # Set hidden fields
            my $oidc_request = {};
            foreach my $param (qw/id_token_hint post_logout_redirect_uri state/)
            {
                if ( $oidc_request->{$param} = $req->param($param) ) {
                    $self->logger->debug( "OIDC request parameter $param: "
                          . $oidc_request->{$param} );
                    $self->p->setHiddenFormValue( $req, $param,
                        $oidc_request->{$param}, '' );
                }
            }

            my $post_logout_redirect_uri =
              $oidc_request->{'post_logout_redirect_uri'};
            my $state = $oidc_request->{'state'};

            # Ask consent for logout
            if ( $req->param('confirm') ) {
                my $err;
                if ( $req->param('confirm') == 1 ) {
                    $req->steps(
                        [
                            @{ $self->p->beforeLogout }, 'authLogout',
                            'deleteSession'
                        ]
                    );
                    $err = $req->error( $self->p->process($req) );
                    if ( $err and $err != PE_LOGOUT_OK ) {
                        if ( $err > 0 ) {
                            $self->logger->error(
                                "Logout process returns error code $err");
                            return PE_ERROR;
                        }
                        return $err;
                    }
                }

                if ($post_logout_redirect_uri) {

                    # Check redirect URI is allowed
                    my $redirect_uri_allowed = 0;
                    foreach ( keys %{ $self->conf->{oidcRPMetaDataOptions} } ) {
                        my $logout_rp = $_;
                        my $redirect_uris =
                          $self->conf->{oidcRPMetaDataOptions}->{$logout_rp}
                          ->{oidcRPMetaDataOptionsPostLogoutRedirectUris};

                        foreach ( split( /\s+/, $redirect_uris ) ) {
                            if ( $post_logout_redirect_uri eq $_ ) {
                                $self->logger->debug(
"$post_logout_redirect_uri is an allowed logout redirect URI for RP $logout_rp"
                                );
                                $redirect_uri_allowed = 1;
                            }
                        }
                    }

                    unless ($redirect_uri_allowed) {
                        $self->logger->error(
                            "$post_logout_redirect_uri is not allowed");
                        return PE_BADURL;
                    }

                    # Build Response
                    my $response_url =
                      $self->buildLogoutResponse( $post_logout_redirect_uri,
                        $state );

                    $self->logger->debug("Redirect user to $response_url");
                    $req->urldc($response_url);
                    return PE_REDIRECT;
                }
                return $req->param('confirm') == 1
                  ? ( $err ? $err : PE_LOGOUT_OK )
                  : PE_OK;
            }

            $req->info(
'<div class="oidc_logout_message"><h3 trspan="logoutConfirm">Do you want to logout?</h3></div>'
            );
            $req->datas->{activeTimer} = 0;
            return PE_CONFIRM;
        }
    }
    $self->logger->error("Unknown OIDC endpoint $path, skipping");
    return PE_ERROR;
}

# Handle token endpoint
sub token {
    my ( $self, $req ) = @_;
    $self->logger->debug("URL detected as an OpenID Connect TOKEN URL");

    # Check authentication
    my ( $client_id, $client_secret ) =
      $self->getEndPointAuthenticationCredentials($req);

    unless ( $client_id && $client_secret ) {
        $self->logger->error(
"No authentication provided to get token, or authentication type not supported"
        );
        return $self->p->sendError( $req, 'invalid_request', 400 );
    }

    # Verify that client_id is registered in configuration
    my $rp = $self->getRP($client_id);

    unless ($rp) {
        $self->userLogger->error(
            "No registered Relying Party found with client_id $client_id");
        return $self->p->sendError( $req, 'invalid_request', 400 );
    }
    else {
        $self->logger->debug("Client id $client_id match RP $rp");
    }

    # Check client_secret
    unless ( $client_secret eq $self->conf->{oidcRPMetaDataOptions}->{$rp}
        ->{oidcRPMetaDataOptionsClientSecret} )
    {
        $self->logger->error("Wrong credentials for $rp");
        return $self->p->sendError( 'invalid_request', 400 );
    }

    # Get code session
    my $code = $req->param('code');

    $self->logger->debug("OpenID Connect Code: $code");

    my $codeSession = $self->getOpenIDConnectSession($code);

    unless ($codeSession) {
        $self->logger->error("Unable to find OIDC session $code");
        $self->p->sendError( $req, 'invalid_request', 400 );
    }

    # Check we have the same redirect_uri value
    unless ( $req->param("redirect_uri") eq $codeSession->data->{redirect_uri} )
    {
        $self->userLogger->error( "Provided redirect_uri is different from "
              . $codeSession->{redirect_uri} );
        $self->p->sendError( $req, 'invalid_request', 400 );
    }

    # Get user identifier
    my $apacheSession =
      $self->p->getApacheSession( $codeSession->data->{user_session_id},
        noInfo => 1 );

    unless ($apacheSession) {
        $self->userLogger->error(
            "Unable to find user session linked to OIDC session $code");
        $codeSession->remove();
        $self->p->sendError( $req, 'invalid_request', 400 );
    }

    my $user_id_attribute =
      $self->conf->{oidcRPMetaDataOptions}->{$rp}
      ->{oidcRPMetaDataOptionsUserIDAttr}
      || $self->conf->{whatToTrace};
    my $user_id = $apacheSession->data->{$user_id_attribute};

    $self->logger->debug("Found corresponding user: $user_id");

    # Generate access_token
    my $accessTokenSession = $self->getOpenIDConnectSession(
        undef,
        {
            scope           => $codeSession->data->{scope},
            rp              => $rp,
            user_session_id => $apacheSession->id,
            _utime          => time,
        }
    );

    unless ($accessTokenSession) {
        $self->userLogger->error(
            "Unable to create OIDC session for access_token");
        $codeSession->remove();
        $self->p->sendError( $req, 'invalid_request', 400 );
    }

    my $access_token = $accessTokenSession->id;

    $self->logger->debug("Generated access token: $access_token");

    # Compute hash to store in at_hash
    my $alg = $self->conf->{oidcRPMetaDataOptions}->{$rp}
      ->{oidcRPMetaDataOptionsIDTokenSignAlg};
    my ($hash_level) = ( $alg =~ /(?:\w{2})(\d{3})/ );
    my $at_hash = $self->createHash( $access_token, $hash_level );

    # ID token payload
    my $id_token_exp = $self->conf->{oidcRPMetaDataOptions}->{$rp}
      ->{oidcRPMetaDataOptionsIDTokenExpiration};
    $id_token_exp += time;

    my $id_token_acr = "loa-" . $apacheSession->data->{authenticationLevel};

    my $id_token_payload_hash = {
        iss => $self->conf->{oidcServiceMetaDataIssuer},    # Issuer Identifier
        sub => $user_id,                                    # Subject Identifier
        aud => [$client_id],                                # Audience
        exp => $id_token_exp,                               # expiration
        iat => time,                                        # Issued time
        auth_time => $apacheSession->data->{_lastAuthnUTime}
        ,    # Authentication time
        acr => $id_token_acr,    # Authentication Context Class Reference
        azp => $client_id,       # Authorized party
                                 # TODO amr
    };

    my $nonce = $codeSession->data->{nonce};
    $id_token_payload_hash->{nonce} = $nonce if defined $nonce;
    $id_token_payload_hash->{'at_hash'} = $at_hash if $at_hash;

    # Create ID Token
    my $id_token = $self->createIDToken( $id_token_payload_hash, $rp );

    $self->logger->debug("Generated id token: $id_token");

    # Send token response
    my $expires_in = $self->conf->{oidcRPMetaDataOptions}->{$rp}
      ->{oidcRPMetaDataOptionsAccessTokenExpiration};

    my $token_response = {
        access_token => $access_token,
        token_type   => 'Bearer',
        expires_in   => $expires_in,
        id_token     => $id_token,
    };

    my $cRP = join ',', $rp, $apacheSession->data->{_oidcConnectedRP} || '';
    $self->p->updateSession( $req, { _oidcConnectedRP => $cRP },
        $apacheSession->id );

    $self->logger->debug("Send token response");

    $codeSession->remove();
    return $self->p->sendJSONresponse( $req, $token_response );
}

# Handle uerinfo endpoint
sub userInfo {
    my ( $self, $req ) = @_;
    $self->logger->debug("URL detected as an OpenID Connect USERINFO URL");

    my $access_token = $self->getEndPointAccessToken($req);

    unless ($access_token) {
        $self->logger->error("Unable to get access_token");
        return $self->returnBearerError( 'invalid_request',
            "Access token not found in request", 401 );
    }

    $self->logger->debug("Received Access Token $access_token");

    my $accessTokenSession = $self->getOpenIDConnectSession($access_token);

    unless ($accessTokenSession) {
        $self->userLogger->error(
            "Unable to get access token session for id $access_token");
        return $self->returnBearerError( 'invalid_request',
            'Invalid request', 401 );
    }

    # Get access token session data
    my $scope           = $accessTokenSession->data->{scope};
    my $rp              = $accessTokenSession->data->{rp};
    my $user_session_id = $accessTokenSession->data->{user_session_id};

    my $userinfo_response =
      $self->buildUserInfoResponse( $scope, $rp, $user_session_id );
    unless ($userinfo_response) {
        return $self->returnBearerError( 'invalid_request', 'Invalid request',
            401 );
    }

    my $userinfo_sign_alg = $self->conf->{oidcRPMetaDataOptions}->{$rp}
      ->{oidcRPMetaDataOptionsUserInfoSignAlg};

    unless ($userinfo_sign_alg) {
        return $self->p->sendJSONresponse( $req, $userinfo_response );
    }
    else {
        my $userinfo_jwt =
          $self->createJWT( $userinfo_response, $userinfo_sign_alg, $rp );
        $self->logger->debug("Return UserInfo as JWT: $userinfo_jwt");
        return [
            200,
            [
                'Content-Type'   => 'application/jwt',
                'Content-Length' => length($userinfo_jwt)
            ],
            [$userinfo_jwt]
        ];
    }
}

# Handle jwks endpoint
sub jwks {
    my ( $self, $req ) = @_;
    $self->logger->debug("URL detected as an OpenID Connect JWKS URL");

    my $jwks = { keys => [] };

    my $public_key_sig = $self->conf->{oidcServicePublicKeySig};
    my $key_id_sig     = $self->conf->{oidcServiceKeyIdSig};
    if ($public_key_sig) {
        my $key = $self->key2jwks($public_key_sig);
        $key->{kty} = "RSA";
        $key->{use} = "sig";
        $key->{kid} = $key_id_sig if $key_id_sig;
        push @{ $jwks->{keys} }, $key;
    }
    $self->logger->debug("Send JWKS response sent");
    return $self->p->sendJSONresponse( $req, $jwks );
}

# Handle register endpoint
sub registration {
    my ( $self, $req ) = @_;
    $self->logger->debug("URL detected as an OpenID Connect REGISTRATION URL");

    # TODO: check Initial Access Token

    # Specific message to allow DOS detection
    my $source_ip = $req->address;
    $self->logger->notice(
        "OpenID Connect Registration request from $source_ip");

    # Check dynamic registration is allowed
    unless ( $self->conf->{oidcServiceAllowDynamicRegistration} ) {
        $self->logger->error("Dynamic registration is not allowed");
        $self->p->sendError( $req, 'server_error' );
    }

    # Get client metadata
    my $client_metadata_json = $req->content;
    unless ($client_metadata_json) {
        return $self->p->sendError( $req, 'Missing POST datas', 400 );
    }

    $self->logger->debug("Client metadata received: $client_metadata_json");

    my $client_metadata       = $self->decodeJSON($client_metadata_json);
    my $registration_response = {};

    # Check redirect_uris
    unless ( $client_metadata->{redirect_uris} ) {
        $self->logger->error("Field redirect_uris is mandatory");
        return $self->p->sendError( $req, 'invalid_client_metadata', 400 );
    }

    # RP identifier
    my $registration_time = time;
    my $rp                = "register-$registration_time";

    # Generate Client ID and Client Password
    my $client_id     = random_string("ssssssssssssssssssssssssssssss");
    my $client_secret = random_string("ssssssssssssssssssssssssssssss");

    # Register known parameters
    my $client_name =
      $client_metadata->{client_name} || "Self registered client";
    my $logo_uri = $client_metadata->{logo_uri};
    my $id_token_signed_response_alg =
      $client_metadata->{id_token_signed_response_alg} || "RS256";
    my $userinfo_signed_response_alg =
      $client_metadata->{userinfo_signed_response_alg};
    my $redirect_uris = $client_metadata->{redirect_uris};

    # Register RP in global configuration
    my $conf = $self->confAcc->getConf( { raw => 1 } );

    $conf->{cfgAuthor}   = "OpenID Connect Registration ($client_name)";
    $conf->{cfgAuthorIP} = $source_ip;
    $conf->{cfgVersion}  = $VERSION;

    $conf->{oidcRPMetaDataExportedVars}->{$rp} = {};
    $conf->{oidcRPMetaDataOptions}->{$rp}->{oidcRPMetaDataOptionsClientID} =
      $client_id;
    $conf->{oidcRPMetaDataOptions}->{$rp}->{oidcRPMetaDataOptionsClientSecret}
      = $client_secret;
    $conf->{oidcRPMetaDataOptions}->{$rp}->{oidcRPMetaDataOptionsDisplayName} =
      $client_name;
    $conf->{oidcRPMetaDataOptions}->{$rp}->{oidcRPMetaDataOptionsIcon} =
      $logo_uri;
    $conf->{oidcRPMetaDataOptions}->{$rp}->{oidcRPMetaDataOptionsIDTokenSignAlg}
      = $id_token_signed_response_alg;
    $conf->{oidcRPMetaDataOptions}->{$rp}->{oidcRPMetaDataOptionsRedirectUris}
      = join( ' ', @$redirect_uris );
    $conf->{oidcRPMetaDataOptions}->{$rp}
      ->{oidcRPMetaDataOptionsUserInfoSignAlg} = $userinfo_signed_response_alg
      if defined $userinfo_signed_response_alg;

    if ( $self->confAcc->saveConf($conf) ) {

        # Reload RP list
        $self->loadRPs();

        # Send registration response
        $registration_response->{'client_id'}            = $client_id;
        $registration_response->{'client_secret'}        = $client_secret;
        $registration_response->{'client_id_issued_at'}  = $registration_time;
        $registration_response->{'client_id_expires_at'} = 0;
        $registration_response->{'client_name'}          = $client_name;
        $registration_response->{'logo_uri'}             = $logo_uri;
        $registration_response->{'id_token_signed_response_alg'} =
          $id_token_signed_response_alg;
        $registration_response->{'redirect_uris'} = $redirect_uris;
        $registration_response->{'userinfo_signed_response_alg'} =
          $userinfo_signed_response_alg
          if defined $userinfo_signed_response_alg;
    }
    else {
        $self->logger->error(
            "Configuration not saved: $Lemonldap::NG::Common::Conf::msg");
        return $self->p->sendError( $req, 'server_error', 500 );
    }

    $self->logger->debug("Registration response sent");
    return $self->p->sendJSONresponse( $req, $registration_response,
        code => 201 );
}

# Handle logout endpoint for unauthenticated users
sub endSessionDone {
    my ( $self, $req ) = @_;
    $self->logger->debug("URL  detected as an OpenID Connect END SESSION URL");
    $self->logger->debug("User is already logged out");

    my $post_logout_redirect_uri = $req->param('post_logout_redirect_uri');
    my $state                    = $req->param('state');

    if ($post_logout_redirect_uri) {

        # Check redirect URI is allowed
        my $redirect_uri_allowed = 0;
        foreach ( keys %{ $self->conf->{oidcRPMetaDataOptions} } ) {
            my $logout_rp = $_;
            my $redirect_uris =
              $self->conf->{oidcRPMetaDataOptions}->{$logout_rp}
              ->{oidcRPMetaDataOptionsPostLogoutRedirectUris};

            foreach ( split( /\s+/, $redirect_uris ) ) {
                if ( $post_logout_redirect_uri eq $_ ) {
                    $self->logger->debug(
"$post_logout_redirect_uri is an allowed logout redirect URI for RP $logout_rp"
                    );
                    $redirect_uri_allowed = 1;
                }
            }
        }

        unless ($redirect_uri_allowed) {
            $self->logger->error("$post_logout_redirect_uri is not allowed");
            return $self->p->login($req);
        }

        # Build Response
        my $response_url =
          $self->buildLogoutResponse( $post_logout_redirect_uri, $state );

        $self->logger->debug("Redirect user to $response_url");
        return [ 302, [ Location => $response_url ], [] ];
    }

    # Else, normal login process
    return $self->p->login($req);
}

# Handle checksession endpoint
sub checkSession {
    my ( $self, $req ) = @_;
    $self->logger->debug(
        "URL  detected as an OpenID Connect CHECK SESSION URL");

    # TODO: access_control_allow_origin => '*'
    $req->frame(1);
    return $self->p->sendHtml(
        $req,
        '../common/oidc_checksession',
        params => {
            COOKIENAME => $self->conf->{cookieName},
        }
    );
}

sub badAuthRequest {
    my ( $self, $req ) = @_;
    return $self->p->sendError( $req,
        $req->uri . ' may not be called by an authenticated user', 400 );
}

# Nothing to do here
sub logout {
    my ( $self, $req ) = @_;
    if ( my $s = $req->userData->{_oidcConnectedRP} ) {
        my @rps = grep /\w/, split( ',', $s );
        foreach my $rp (@rps) {
            my $rpConf = $self->conf->{oidcRPMetaDataOptions}->{$rp};
            unless ($rpConf) {
                $self->logger->error("Unknown RP $rp");
                return PE_ERROR;
            }
            if ( my $url = $rpConf->{oidcRPMetaDataOptionsLogoutUrl} ) {
                if ( $rpConf->{oidcRPMetaDataOptionsLogoutType} eq 'front' ) {
                    if ( $rpConf->{oidcRPMetaDataOptionsLogoutSessionRequired} )
                    {
                        my $user_id_attribute =
                          $self->conf->{oidcRPMetaDataOptions}->{$rp}
                          ->{oidcRPMetaDataOptionsUserIDAttr}
                          || $self->conf->{whatToTrace};
                        my $user_id = $req->{sessionInfo}->{$user_id_attribute};
                        $url .= ( $url =~ /\?/ ? '&' : '?' )
                          . build_urlencoded(
                            iss => $self->conf->{oidcServiceMetaDataIssuer},
                            sid => $user_id
                          );
                    }
                    $req->info( qq'<iframe src="$url" class="noborder">'
                          . '</iframe>' );
                }
                else {
                    # TODO
                }
            }
        }
    }
    return PE_OK;
}

# Internal methods

sub metadata {
    my ( $self, $req ) = @_;
    my $issuerDBOpenIDConnectPath = $self->conf->{issuerDBOpenIDConnectPath};
    my $authorize_uri    = $self->conf->{oidcServiceMetaDataAuthorizeURI};
    my $token_uri        = $self->conf->{oidcServiceMetaDataTokenURI};
    my $userinfo_uri     = $self->conf->{oidcServiceMetaDataUserInfoURI};
    my $jwks_uri         = $self->conf->{oidcServiceMetaDataJWKSURI};
    my $registration_uri = $self->conf->{oidcServiceMetaDataRegistrationURI};
    my $endsession_uri   = $self->conf->{oidcServiceMetaDataEndSessionURI};
    my $checksession_uri = $self->conf->{oidcServiceMetaDataCheckSessionURI};

    my $path   = $self->path . '/';
    my $issuer = $self->conf->{oidcServiceMetaDataIssuer};
    $path = "/" . $path unless ( $issuer =~ /\/$/ );
    my $baseUrl = $issuer . $path;

    my @acr = keys %{ $self->conf->{oidcServiceMetaDataAuthnContext} };

    # Add a slash to path value if issuer has no trailing slash

    # Create OpenID configuration hash;
    return $self->p->sendJSONresponse(
        $req,
        {
            issuer => $issuer,

            # Endpoints
            token_endpoint         => $baseUrl . $token_uri,
            userinfo_endpoint      => $baseUrl . $userinfo_uri,
            jwks_uri               => $baseUrl . $jwks_uri,
            authorization_endpoint => $baseUrl . $authorize_uri,
            end_session_endpoint   => $baseUrl . $endsession_uri,
            check_session_iframe   => $baseUrl . $checksession_uri,

            # Logout capabilities
            backchannel_logout_supported          => JSON::true,
            backchannel_logout_session_supported  => JSON::true,
            frontchannel_logout_supported         => JSON::true,
            frontchannel_logout_session_supported => JSON::true,
            (
                $self->conf->{oidcServiceAllowDynamicRegistration}
                ? ( registration_endpoint => $baseUrl . $registration_uri )
                : ()
            ),

            # Scopes
            scopes_supported => [qw/openid profile email address phone/],
            response_types_supported => [
                "code",
                "id_token",
                "id_token token",
                "code id_token",
                "code token",
                "code id_token token"
            ],
            grant_types_supported   => [qw/authorization_code implicit hybrid/],
            acr_values_supported    => \@acr,
            subject_types_supported => ["public"],
            token_endpoint_auth_methods_supported =>
              [qw/client_secret_post client_secret_basic/],
            claims_supported                 => [qw/sub iss auth_time acr/],
            request_parameter_supported      => JSON::true,
            request_uri_parameter_supported  => JSON::true,
            require_request_uri_registration => JSON::false,

            # Algorithms
            id_token_signing_alg_values_supported =>
              [qw/none HS256 HS384 HS512 RS256 RS384 RS512/],
            userinfo_signing_alg_values_supported =>
              [qw/none HS256 HS384 HS512 RS256 RS384 RS512/],
        }
    );

    # response_modes_supported}

    # id_token_encryption_alg_values_supported
    # id_token_encryption_enc_values_supported

    # userinfo_encryption_alg_values_supported
    # userinfo_encryption_enc_values_supported
    # request_object_signing_alg_values_supported
    # request_object_encryption_alg_values_supported
    # request_object_encryption_enc_values_supported

    # token_endpoint_auth_signing_alg_values_supported
    # display_values_supported
    # claim_types_supported
    # RECOMMENDED # claims_supported
    # service_documentation
    # claims_locales_supported
    # ui_locales_supported
    # claims_parameter_supported

    # op_policy_uri
    # op_tos_uri
}

# Store request parameters in %ENV
sub exportRequestParameters {
    my ( $self, $req ) = @_;

    foreach my $param (
        qw/response_type scope client_id state redirect_uri nonce
        response_mode display prompt max_age ui_locales id_token_hint
        login_hint acr_values request request_uri/
      )
    {
        if ( $req->param($param) ) {
            $req->env->{ "llng_oidc_" . $param } = $req->param($param);
        }
    }

    # Extract request_uri/request parameter
    my $request = $req->param('request');
    if ( $req->param('request_uri') ) {
        $request = $self->getRequestJWT( $req->param('request_uri') );
    }

    if ($request) {
        my $request_data = $self->getJWTJSONData($request);
        foreach ( keys %$request_data ) {
            $req->env->{ "llng_oidc_" . $_ } = $request_data->{$_};
        }
    }

    if ( $req->param('client_id') ) {
        my $rp = $self->getRP( $req->param('client_id') );
        $req->env->{"llng_oidc_rp"} = $rp if $rp;
    }

    return PE_OK;
}

1;
