The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

use strict;
use JSON;
use URI;
use Crypt::JWT qw(encode_jwt);
BEGIN {
require 't/test-lib.pm';
require 't/oidc-lib.pm';
}
my $access_token;
my $jwk =
Lemonldap::NG::Portal::Lib::OpenIDConnect->key2jwks(oidc_key_op_private_sig);
LWP::Protocol::PSGI->register(
sub {
my $req = Plack::Request->new(@_);
note "Internal request to " . $req->path;
if ( $req->path eq "/oauth2/token" ) {
is( $req->parameters->{client_id}, "rpid", "expected client_id" );
is( $req->parameters->{client_secret},
"rpsecret", "expected client_secret" );
is(
$req->parameters->{redirect_uri},
"expected redirect_uri"
);
is( $req->parameters->{code}, "aaa", "expected code" );
my $key = oidc_key_op_private_sig;
my $response = {
token_type => "Bearer",
access_token => "abc",
expired_in => 3600,
id_token => encode_jwt(
payload => {
aud => "rpid",
exp => time + 1000,
sub => "dwho",
at_hash => "ungWv48Bz-pBQUDeXa4iIw",
},
alg => "RS256",
key => \$key,
extra_headers => { kid => "mykid" }
),
};
return Plack::Response->new( "200",
{ "Content-Type" => "application/json" },
encode_json($response) )->finalize;
}
if ( $req->path eq "/oauth2/jwks" ) {
$main::jwks_call_count += 1;
my $kid = $main::jwks_show_kid ? "mykid" : "wrongkid";
my $jwks = { keys => [ { kid => $kid, %$jwk } ] };
return Plack::Response->new( "200",
{ "Content-Type" => "application/json" },
encode_json($jwks) )->finalize;
}
my $res = Plack::Response->new;
$res->status(500);
return $res->finalize;
}
);
sub tryauth {
my ($rp) = @_;
ok( my $res = $rp->_get( '/', accept => 'text/html' ),
'Unauth SP request' );
my ($url) =
expectRedirection( $res,
$url = URI->new($url);
is( $url->host, "op.example.com", "Correct host" );
my %query = $url->query_form;
is( $query{client_id}, 'rpid', "Correct client_id" );
is( $query{scope}, 'openid profile email', "Correct scope" );
is(
$query{redirect_uri},
"Correct redirect_uri"
);
ok( my $state = $query{state}, "Found state" );
# Post return authorization code
ok(
$res = $rp->_get(
'/',
query => {
openidconnectcallback => 1,
code => "aaa",
state => $state,
},
accept => 'text/html'
),
'Authorization code'
);
return $res;
}
my $metadata = <<EOF;
{
"authorization_endpoint": "https://op.example.com/oauth2/authorize",
}
EOF
$main::jwks_call_count = 0;
$main::jwks_show_kid = 0;
my $rp = rp($metadata);
is( $main::jwks_call_count, 1, "JWKS url was called during startup" );
# Try to authenticate with a token containing a kid that is not found in jwks
my $res = tryauth($rp);
expectPortalError( $res, 106 );
is( $main::jwks_call_count, 2, "JWKS refresh was forced due to wrong kid" );
# Update OP's JWKS to publish the correct kid
$main::jwks_show_kid = 1;
# LemonLDAP immediately refreshes its JWKS
$res = tryauth($rp);
expectCookie($res);
is( $main::jwks_call_count, 3, "JWKS refresh was forced due to wrong kid" );
# The next attempt does not trigger a refresh
$res = tryauth($rp);
expectCookie($res);
is( $main::jwks_call_count, 3, "JWKS url was not called again" );
# After cache expiration, the next attemps triggers a refresh
Time::Fake->offset("+600s");
$res = tryauth($rp);
expectCookie($res);
is( $main::jwks_call_count, 4,
"JWKS url was called again due to cache expiration" );
clean_sessions();
done_testing();
sub rp {
my ($metadata) = @_;
return LLNG::Manager::Test->new( {
ini => {
domain => 'rp.com',
portal => 'http://auth.rp.com/',
authentication => 'OpenIDConnect',
userDB => 'Same',
restSessionServer => 1,
restExportSecretKeys => 1,
oidcOPMetaDataExportedVars => {
op => {
cn => "name",
uid => "sub",
sn => "family_name",
mail => "email",
groups => "groups",
}
},
oidcOPMetaDataOptions => {
op => {
oidcOPMetaDataOptionsCheckJWTSignature => 1,
oidcOPMetaDataOptionsJWKSTimeout => 100,
oidcOPMetaDataOptionsClientSecret => "rpsecret",
oidcOPMetaDataOptionsScope => "openid profile email",
oidcOPMetaDataOptionsStoreIDToken => 0,
oidcOPMetaDataOptionsDisplay => "",
oidcOPMetaDataOptionsClientID => "rpid",
oidcOPMetaDataOptionsStoreIDToken => 1,
oidcOPMetaDataOptionsUseNonce => 0,
oidcOPMetaDataOptionsConfigurationURI =>
}
},
oidcOPMetaDataJSON => {
op => $metadata,
},
customPlugins => 't::OidcHookPlugin',
}
}
);
}