Group
Extension

Lemonldap-NG-Portal/lib/Lemonldap/NG/Portal/Issuer/JitsiMeetTokens.pm

package Lemonldap::NG::Portal::Issuer::JitsiMeetTokens;

use strict;
use URI;
use Mouse;
use JSON;
use MIME::Base64 qw/decode_base64url encode_base64url/;
use Crypt::JWT   qw(encode_jwt);
use Digest::SHA  qw(sha256_hex);
use Crypt::OpenSSL::X509;
use Crypt::OpenSSL::RSA;

use Lemonldap::NG::Portal::Main::Constants qw(
  PE_REDIRECT
  PE_UNAUTHORIZEDPARTNER
  PE_ERROR
  PE_OK
);

our $VERSION = '2.22.0';

extends 'Lemonldap::NG::Portal::Main::Issuer';
with 'Lemonldap::NG::Portal::Lib::Key';

has rule => ( is => 'rw' );

has jitsi_default_server => (
    is      => "rw",
    lazy    => 1,
    default => sub {
        $_[0]->conf->{jitsiDefaultServer};
    }
);

has jitsi_appid => (
    is      => "rw",
    lazy    => 1,
    default => sub {
        $_[0]->conf->{jitsiAppId};
    }
);

has jitsi_expiration => (
    is      => "rw",
    lazy    => 1,
    default => sub {
        $_[0]->conf->{jitsiExpiration} || "300";
    }
);

has jitsi_signing_alg => (
    is      => "rw",
    lazy    => 1,
    default => sub {
        $_[0]->conf->{jitsiSigningAlg} || "HS256";
    }
);

has jitsi_appsecret => (
    is      => "rw",
    lazy    => 1,
    default => sub {
        $_[0]->conf->{jitsiAppSecret};
    }
);

has jitsi_id_attribute => (
    is      => "rw",
    lazy    => 1,
    default => sub {
        $_[0]->conf->{jitsiIdAttribute} || $_[0]->conf->{whatToTrace};
    }
);

has jitsi_name_attribute => (
    is      => "rw",
    lazy    => 1,
    default => sub {
        $_[0]->conf->{jitsiNameAttribute} || "cn";
    }
);
has jitsi_mail_attribute => (
    is      => "rw",
    lazy    => 1,
    default => sub {
        $_[0]->conf->{jitsiMailAttribute} || "mail";
    }
);

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

    my $compiled_rule =
      $self->p->buildRule( $self->conf->{issuerDBJitsiMeetTokensRule},
        "Jitsi JWT issuer rule" );
    return 0 if !$compiled_rule;

    return 0 unless $self->SUPER::init();

    $self->addUnauthRoute( $self->path() => { asap => "asap" }, ['GET'] );
    $self->addAuthRoute( $self->path() => { asap => "asap" }, ['GET'] );
    $self->rule($compiled_rule);
    return 1;

}

sub asap {
    my ( $self, $req, @path ) = @_;

    my $filename = $path[0];

    my $hash = $filename =~ s/\.pem$//r;

    for my $key_id ( split( /\s*,\s*/, $self->conf->{jitsiSigningKey} ) ) {
        my $key = $self->get_public_key($key_id);
        if (    $key
            and $key->{external_id}
            and $hash eq sha256_hex( $key->{external_id} ) )
        {
            return $self->_sendAsap( $req, $key->{public} );
        }
    }

    return $self->p->sendError( $req, "Unknown key id hash", 404 );
}

sub _sendAsap {
    my ( $self, $req, $pem ) = @_;

    my $res;

    # Try to parse as RSA key
    eval {
        my $pubkey = Crypt::OpenSSL::RSA->new_public_key($pem);
        $res = $pubkey->get_public_key_x509_string;
    };
    my $parse_pubkey_error = $@;

    if ( !$res ) {

        # Try to parse as X.509 cert
        eval {
            my $x509 = Crypt::OpenSSL::X509->new_from_string( $pem,
                Crypt::OpenSSL::X509::FORMAT_PEM );
            my $pub  = $x509->pubkey;
            my $type = $x509->pubkey_type;
            if ( ($type) eq "rsa" ) {
                my $pubkey = Crypt::OpenSSL::RSA->new_public_key($pub);
                $res = $pubkey->get_public_key_x509_string;
            }
            else {
                die "Unsupported pubkey type $type";
            }
        };
    }
    my $parse_cert_error = $@;

    if ($res) {
        return [
            200,
            [
                'Content-Type'   => 'application/x-pem-file',
                'Content-Length' => length($res),
                $req->spliceHdrs,
            ],
            [$res]
        ];
    }
    else {
        $self->logger->error(
                "Could not parse public key as RSA ($parse_pubkey_error)"
              . " or X.509 ($parse_cert_error)" );
        return $self->p->sendError( $req, "Unsupported public key format",
            500 );
    }
}

sub run {
    my ( $self, $req, @path ) = @_;

    # Check activation rule
    unless ( $self->rule->( $req, $req->sessionInfo ) ) {
        $self->userLogger->error('Jitsi JWT service not authorized');
        return PE_UNAUTHORIZEDPARTNER;
    }

    if ( $path[0] eq "login" ) {
        return $self->jitsi($req);
    }
    return PE_OK;
}

# Nothing to do here for now
sub logout {
    return PE_OK;
}

sub jitsi {
    my ( $self, $req ) = @_;
    my $room = $req->param('room');

    if ( !$self->jitsi_default_server ) {
        $self->logger->error("Jitsi Server URL not set in configuration");
        return PE_ERROR;
    }
    if ( !$self->jitsi_appid ) {
        $self->logger->error("Jitsi Application ID not set in configuration");
        return PE_ERROR;
    }
    if ( !$room ) {
        $self->logger->error("Missing room parameter");
        return PE_ERROR;
    }

    my $payload = {
        iss     => $self->p->buildUrl(),
        room    => $room,
        exp     => ( time + $self->jitsi_expiration ),
        sub     => '*',
        aud     => $self->jitsi_appid,
        context => {
            user => {
                id => $req->userData->{ $self->jitsi_id_attribute },
                (
                    $self->jitsi_name_attribute
                    ? ( name => $req->userData->{ $self->jitsi_name_attribute }
                      )
                    : ()
                ),
                (
                    $self->jitsi_mail_attribute
                    ? ( email =>
                          $req->userData->{ $self->jitsi_mail_attribute } )
                    : ()
                ),
                affiliation => "owner"
            }
        }
    };
    my $server;
    my $u = URI->new_abs( $room, URI->new( $self->jitsi_default_server ) );

    my @extra_headers;
    my $key;
    if ( $self->jitsi_signing_alg =~ /^HS/ ) {
        if ( !$self->jitsi_appsecret ) {
            $self->logger->error(
                "Jitsi Application secret not set in configuration");
            return PE_ERROR;
        }
        $key = $self->jitsi_appsecret;

    }
    else {
        my ($key_id) = split( /\s*,\s*/, $self->conf->{jitsiSigningKey} );
        if ( !$key_id ) {
            $self->logger->error("jitsiSigningKey is not set");
            return PE_ERROR;
        }

        my $pkey = $self->get_private_key($key_id);
        if ( !$pkey ) {
            $self->logger->error("Jitsi signing key $key_id was not found");
            return PE_ERROR;
        }

        if ( !$pkey->{external_id} ) {
            $self->logger->error(
                    "Jitsi signing key does not have an identified."
                  . " You must set oidcServiceKeyIdSig" );
            return PE_ERROR;
        }
        @extra_headers = ( kid => $pkey->{external_id} );
        my $private = $pkey->{private};
        $key = \$private;
    }

    my $jwt = eval {
        encode_jwt(
            payload       => to_json($payload),
            alg           => $self->jitsi_signing_alg,
            key           => $key,
            extra_headers => { typ => "JWT", @extra_headers },
        );
    };
    if ($@) {
        $self->logger->error("Could not encode JWT: $@");
        return $self->p->doPE( $req, PE_ERROR );
    }

    $u->query_form( jwt => $jwt );
    $req->urldc( $u->as_string );
    return PE_REDIRECT;
}

1;


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.