Merge branch '3497' into 'v2.0'

3497

See merge request lemonldap-ng/lemonldap-ng!834
pipelines/38628
Christophe Maudoux 6 days ago
commit 11895a38c9
  1. 2
      doc/sources/admin/idpopenidconnect.rst
  2. 9
      lemonldap-ng-common/lib/Lemonldap/NG/Common/JWT.pm
  3. 2
      lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/AuthBasic.pm
  4. 39
      lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/OAuth2.pm
  5. 7
      lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/ServiceToken.pm
  6. 54
      lemonldap-ng-handler/t/71-Lemonldap-NG-Handler-PSGI-OAuth2.t
  7. 5
      lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm

@ -230,7 +230,7 @@ Options
identifier (``sub``). Default value is ``whatToTrace``.
- **Additional audiences** (since version ``2.0.8``): You can
specify a space-separated list of audiences that will be added to the
ID Token audiences, and possibly the access token audiences if the
ID token audiences, and possibly the access token audiences if the
access token format is JWT
- **Other RP allowed to exchange access_token** (since version ``2.20.0``):
You can specify a space-separated list of Relying-Party allowed to ask

@ -8,7 +8,7 @@ our @EXPORT_OK =
use JSON;
use MIME::Base64 qw/encode_base64 decode_base64/;
our $VERSION = '2.21.0';
our $VERSION = '2.22';
# Gets the Access Token session ID embedded in a LLNG-emitted JWT
sub getAccessTokenSessionId {
@ -18,12 +18,7 @@ sub getAccessTokenSessionId {
# and use it as session ID
if ( index( $access_token, '.' ) > 0 ) {
my $data = getJWTPayload($access_token);
if ( $data and $data->{jti} ) {
return $data->{jti};
}
else {
return;
}
return ( $data and $data->{jti} ) ? $data->{jti} : undef;
}
# Access Token is the session ID directly

@ -142,7 +142,7 @@ sub createSession {
# Hide user credentials to the protected application
sub hideCookie {
my ( $class, $req ) = @_;
$class->logger->debug("removing Authorization header");
$class->logger->debug('Removing Authorization header');
$class->unset_header_in( $req, 'Authorization' );
}

@ -1,9 +1,9 @@
package Lemonldap::NG::Handler::Lib::OAuth2;
use Lemonldap::NG::Common::JWT qw(getAccessTokenSessionId);
use Lemonldap::NG::Common::JWT qw(getAccessTokenSessionId);
use strict;
our $VERSION = '2.19.0';
our $VERSION = '2.22.1';
sub retrieveSession {
my ( $class, $req, $id ) = @_;
@ -40,20 +40,17 @@ sub retrieveSession {
);
unless ( $session->error ) {
my $data = { %{ $session->data }, $class->_getTokenAttributes($req) };
$class->data($data);
$class->logger->debug("Get session $offlineId from Handler::Main::Run");
# Verify that session is valid
$class->logger->error(
"_utime is not defined. This should not happen. Check if it is well transmitted to handler"
'_utime is not defined. This should not happen. Check if it is well transmitted to handler.'
) unless $session->data->{_utime};
my $ttl = $class->tsv->{timeout} - time + $session->data->{_utime};
$class->logger->debug( "Session TTL = " . $ttl );
$class->logger->debug("Session TTL = $ttl");
if ( time - $session->data->{_utime} > $class->tsv->{timeout} ) {
$class->logger->info("Session $id expired");
@ -75,7 +72,6 @@ sub retrieveSession {
sub fetchId {
my ( $class, $req ) = @_;
my $access_token;
my $authorization = $req->{env}->{HTTP_AUTHORIZATION};
@ -100,6 +96,26 @@ sub fetchId {
return;
}
# Check audiences if defined
my @aud = split /[,\s]+/, $infos->{aud};
if ( $aud[0] ) {
my $vhost = $class->resolveAlias($req);
if ( grep { $_ eq $vhost } @aud ) {
$class->logger->debug(
"$vhost found in audiences list: " . join ', ', @aud );
}
else {
$class->auditLog(
$req,
message => 'VHost not found in audiences',
code => "INVALID_ACCESS_TOKEN",
);
$class->logger->debug(
"$vhost not found in audiences list: " . join ', ', @aud );
return;
}
}
# Store scope and rpid for future session attributes
if ( $infos->{rp} ) {
my $rp = $infos->{rp};
@ -179,6 +195,13 @@ sub getOIDCInfos {
return $infos;
}
# Hide AccesToken to the protected application
sub hideCookie {
my ( $class, $req ) = @_;
$class->logger->debug("Removing Authorization header");
$class->unset_header_in( $req, 'Authorization' );
}
## The OAuth2 handler does not redirect, we simply return a 401 with relevant
# information as described in https://tools.ietf.org/html/rfc6750#section-3
sub goToPortal {

@ -105,4 +105,11 @@ sub fetchId {
return $_session_id;
}
# Hide ServiceToken to the protected application
sub hideCookie {
my ( $class, $req ) = @_;
$class->logger->debug('Removing X_LLNG_TOKEN header');
$class->unset_header_in( $req, 'X_LLNG_TOKEN' );
}
1;

@ -4,7 +4,7 @@ BEGIN {
require 't/test-psgi-lib.pm';
}
my $maintests = 25;
my $maintests = 29;
init(
'Lemonldap::NG::Handler::Server',
@ -60,7 +60,27 @@ Lemonldap::NG::Common::Session->new( {
"_type" => "access_token",
"_utime" => ( time - 72000 + 300 ),
"rp" => "rp-example2",
"scope" => "openid email read"
"scope" => "openid email read",
"aud" => "test1.example.com other.example.com"
}
}
);
# Inject an on-line access token session
Lemonldap::NG::Common::Session->new( {
hashStore => $ENV{LLNG_HASHED_SESSION_STORE},
storageModule => 'Apache::Session::File',
storageModuleOptions => { Directory => 't/sessions' },
id =>
'f1fd4e85000ce35d062f97f5b466fc00abc2fad0406e03e086605f929ec4a249',
force => 1,
kind => 'OIDCI',
info => {
"user_session_id" => $sessionId,
"_type" => "access_token",
"_utime" => ( time - 72000 + 300 ),
"rp" => "rp-example2",
"scope" => "openid email read",
}
}
);
@ -78,7 +98,8 @@ Lemonldap::NG::Common::Session->new( {
"_type" => "refresh_token",
"_utime" => ( time - 72000 + 300 ),
"rp" => "rp-example",
"scope" => "openid email read"
"scope" => "openid email read",
"aud" => "test1.example.com"
}
}
);
@ -142,6 +163,20 @@ like(
'Got invalid token error'
);
# Request with valid Access Token
ok(
$res = $client->_get(
'/read', undef,
'test1.example.com', '',
VHOSTTYPE => 'OAuth2',
HTTP_AUTHORIZATION =>
'Bearer f1fd4e85000ce35d062f97f5b466fc00abc2fad0406e03e086605f929ec4a249',
),
'Valid access token without audience'
);
%h = @{ $res->[1] };
is( $res->[0], 200, "Request accepted" );
# Request with valid Access Token
ok(
$res = $client->_get(
@ -194,6 +229,19 @@ is( $res->[0], 200, "Request accepted" );
ok( $h{'Auth-User'} eq 'dwho', 'Header Auth-User is set to "dwho"' )
or explain( \%h, 'Auth-User => "dwho"' );
# Request with Access token from offline session
ok(
$res = $client->_get(
'/read', undef,
'other.example.com', '',
VHOSTTYPE => 'OAuth2',
HTTP_AUTHORIZATION => 'Bearer 999888777',
),
'Invalid access token (audience)'
);
%h = @{ $res->[1] };
is( $res->[0], 401, "Access was rejected" );
# Request with Access token from offline session
ok(
$res = $client->_get(

@ -1208,7 +1208,6 @@ sub newAccessToken {
my $client_id = $self->rpOptions->{$rp}->{oidcRPMetaDataOptionsClientID};
my $at_info = {
scope => $scope,
rp => $rp,
client_id => $client_id,
@ -1694,7 +1693,7 @@ sub decodeJWT {
decode_jwt( token => $jwt, @$keyArg, decode_payload => 0 ) );
};
if ($@) {
$error = [ "Unable to verify JWT: $@", "Jwt was: $jwt" ];
$error = [ "Unable to verify JWT: $@", "JWT was: $jwt" ];
}
else {
$error = [];
@ -2427,7 +2426,7 @@ sub _createJWT {
my @keyArg;
$extra_headers //= {};
# Set Cript::JWT arguments depending on "alg"
# Set Crypt::JWT arguments depending on "alg"
# a) "none"
if ( $alg eq 'none' ) {
@keyArg = ( allow_none => 1 );

Loading…
Cancel
Save