lemonldap-ng/lemonldap-ng-portal/t/32-OIDC-Password-Grant.t

243 lines
7.4 KiB
Perl

use lib 'inc';
use Test::More;
use strict;
use IO::String;
use LWP::UserAgent;
use LWP::Protocol::PSGI;
use MIME::Base64;
use JSON;
BEGIN {
require 't/test-lib.pm';
require 't/oidc-lib.pm';
}
my $debug = 'error';
# Initialization
my $op = LLNG::Manager::Test->new( {
ini => {
logLevel => $debug,
domain => 'op.com',
portal => 'http://auth.op.com',
requireToken => 1,
macros => {
gender => '"32"',
_whatToTrace => '$uid',
nickname => '"froggie; frenchie"',
},
issuerDBOpenIDConnectActivation => 1,
oidcRPMetaDataExportedVars => {
rp => {
email => "mail;string;always",
preferred_username => "uid",
name => "cn",
gender => "gender;int;auto",
nickname => "nickname",
}
},
oidcRPMetaDataOptions => {
scopelessrp => {
oidcRPMetaDataOptionsDisplayName => "RP",
oidcRPMetaDataOptionsIDTokenExpiration => 3600,
oidcRPMetaDataOptionsClientID => "scopelessrp",
oidcRPMetaDataOptionsAllowOffline => 1,
oidcRPMetaDataOptionsAllowPasswordGrant => 1,
oidcRPMetaDataOptionsIDTokenSignAlg => "HS512",
oidcRPMetaDataOptionsClientSecret => "rpsecret",
oidcRPMetaDataOptionsUserIDAttr => "",
oidcRPMetaDataOptionsAccessTokenExpiration => 120,
oidcRPMetaDataOptionsBypassConsent => 1,
oidcRPMetaDataOptionsRefreshToken => 1,
oidcRPMetaDataOptionsIDTokenForceClaims => 1,
oidcRPMetaDataOptionsRule => '$uid eq "french"',
},
rp => {
oidcRPMetaDataOptionsDisplayName => "RP",
oidcRPMetaDataOptionsIDTokenExpiration => 3600,
oidcRPMetaDataOptionsClientID => "rpid",
oidcRPMetaDataOptionsAllowOffline => 1,
oidcRPMetaDataOptionsAllowPasswordGrant => 1,
oidcRPMetaDataOptionsIDTokenSignAlg => "HS512",
oidcRPMetaDataOptionsClientSecret => "rpsecret",
oidcRPMetaDataOptionsUserIDAttr => "",
oidcRPMetaDataOptionsAccessTokenExpiration => 120,
oidcRPMetaDataOptionsBypassConsent => 1,
oidcRPMetaDataOptionsRefreshToken => 1,
oidcRPMetaDataOptionsIDTokenForceClaims => 1,
oidcRPMetaDataOptionsRule => '$uid eq "french"',
}
},
oidcRPMetaDataScopeRules => {
rp => {
"read" => '$requested',
"french" => '$uid eq "french"',
"always" => '1',
},
},
oidcServicePrivateKeySig => oidc_key_op_private_sig,
oidcServicePublicKeySig => oidc_key_op_public_sig,
}
}
);
my $res;
# Resource Owner Password Credentials Grant
# Access Token Request
# https://tools.ietf.org/html/rfc6749#section-4.3
# Wrong password should fail
my $query = buildForm( {
client_id => 'rpid',
client_secret => 'rpsecret',
grant_type => 'password',
username => 'french',
password => 'invalid',
scope => 'profile email',
}
);
## Wrong password should fail
$res = $op->_post(
"/oauth2/token",
IO::String->new($query),
accept => 'application/json',
length => length($query),
);
expectReject( $res, 400, "invalid_grant" );
# Empty scope should fail
my $query = buildForm( {
client_id => 'scopelessrp',
client_secret => 'rpsecret',
grant_type => 'password',
username => 'french',
password => 'french',
}
);
$res = $op->_post(
"/oauth2/token",
IO::String->new($query),
accept => 'application/json',
length => length($query),
);
expectReject( $res, 400, "invalid_scope" );
$query = buildForm( {
client_id => 'rpid',
client_secret => 'rpsecret',
grant_type => 'password',
username => 'french',
password => 'french',
scope => 'profile email',
}
);
## Login should be valid
$res = $op->_post(
"/oauth2/token",
IO::String->new($query),
accept => 'application/json',
length => length($query),
);
my $payload = expectJSON($res);
my $access_token = $payload->{access_token};
ok( $access_token, "Access Token found" );
count(1);
my $token_res_scope = $payload->{scope};
ok( $token_res_scope, "Scope found in token response" );
is( $payload->{id_token}, undef, "No ID token in original request" );
my $refresh_token = $payload->{refresh_token};
ok( $refresh_token, "Got refresh token" );
count(3);
# Get userinfo
$res = $op->_post(
"/oauth2/userinfo",
IO::String->new(''),
accept => 'application/json',
length => 0,
custom => {
HTTP_AUTHORIZATION => "Bearer " . $access_token,
},
);
$payload = expectJSON($res);
ok( $payload->{'name'} eq "Frédéric Accents", 'Got User Info' );
like( $res->[2]->[0], qr/"gender":32/, "Attribute released as int in JSON" );
is( ref( $payload->{email} ),
"ARRAY", "Single valued attribute forced as array" );
is( ref( $payload->{nickname} ),
"ARRAY", "Multi valued attribute exposed as array" );
$query = "token=$access_token";
ok(
$res = $op->_post(
"/oauth2/introspect",
IO::String->new($query),
accept => 'text/html',
length => length $query,
custom => {
HTTP_AUTHORIZATION => "Basic " . encode_base64("rpid:rpsecret"),
},
),
"Post introspection"
);
$payload = expectJSON($res);
unlike( $payload->{scope}, qr/\bread\b/,
"Scope read not asked, and thus not found" );
like( $payload->{scope}, qr/\bfrench\b/, "Attribute-based scope found" );
like( $payload->{scope}, qr/\balways\b/, "Rule-enforced scope found" );
is( $payload->{scope}, $token_res_scope,
"Token response scope matches token scope" );
# Expire token
Time::Fake->offset("+5m");
ok(
$res = $op->_post(
"/oauth2/introspect",
IO::String->new($query),
accept => 'text/html',
length => length $query,
custom => {
HTTP_AUTHORIZATION => "Basic " . encode_base64("rpid:rpsecret"),
},
),
"Post introspection"
);
$res = expectJSON($res);
is( $res->{active}, 0, "Token is no longer active" );
$query = buildForm( {
grant_type => 'refresh_token',
refresh_token => $refresh_token,
}
);
ok(
$res = $op->_post(
"/oauth2/token",
IO::String->new($query),
accept => 'text/json',
length => length $query,
custom => {
HTTP_AUTHORIZATION => "Basic " . encode_base64("rpid:rpsecret"),
},
),
"Post introspection"
);
$res = expectJSON($res);
is( $res->{id_token}, undef, "No ID token in refreshed response" );
clean_sessions();
done_testing();