Merge branch 'v2.0'

This commit is contained in:
Yadd 2021-06-24 13:39:10 +02:00
commit ce02973702
29 changed files with 432 additions and 185 deletions

View File

@ -49,5 +49,9 @@ with the following parameters (Options -> Basic) :
* **Client Secret**: the same you set in Publik configuration.
* **Allowed redirection addresses for login**: The "Callback URL" for authentic2 : https://authentic2-instance/accounts/oidc/callback/
And in Options -> Logout
* **Allowed redirection addresses for logout**: The "Logout URL" for authentic2 : https://authentic2-instance/logout/
.. |image0| image:: /applications/logo-publik.png
:class: align-center

View File

@ -314,11 +314,9 @@ Options
(RSXXX) or HMAC (HSXXX) based signature algorithms
- **Access Token signature algorithm**: Select one of the available public
key signature algorithms
- **Userinfo signature algorithm** (since version ``2.0.12``): Select one
of the available signature algorithms to release user information as a JWT
on the ``/userinfo`` endpoint. If this option is left empty, user
information will be released as a plain JSON object. The ``None`` value
will release user information as an unsigned JWT.
- **Userinfo response format** (since version ``2.0.12``): By default,
UserInfo is returned as a simple JSON object. You can also choose to
return it as a JWT, using one of the available signature algorithms.
- **Require PKCE** (since version ``2.0.4``): a code challenge is
required at token endpoint (see
`RFC7636 <https://tools.ietf.org/html/rfc7636>`__)

View File

@ -0,0 +1 @@
sphinx_bootstrap_theme

View File

@ -18,7 +18,8 @@ sub retrieveSession {
# Update cache
$class->data($data);
} else {
}
else {
$req->data->{oauth2_error} = 'invalid_token';
}
return $data;
@ -93,6 +94,10 @@ sub fetchId {
return;
}
my $infos = $class->getOIDCInfos($access_token_sid);
unless ($infos) {
$req->data->{oauth2_error} = 'invalid_token';
return;
}
# Store scope and rpid for future session attributes
if ( $infos->{rp} ) {
@ -147,6 +152,20 @@ sub getOIDCInfos {
unless ( $oidcSession->error ) {
$class->logger->debug("Get OIDC session $id");
# Verify that session is valid
unless ( $oidcSession->data->{_utime} ) {
$class->logger->error("_utime missing from Access Token session");
return;
}
my $ttl = $class->tsv->{timeout} - time + $oidcSession->data->{_utime};
$class->logger->debug( "Session TTL = " . $ttl );
if ( time - $oidcSession->data->{_utime} > $class->tsv->{timeout} ) {
$class->logger->info("Access Token session $id expired");
return;
}
$infos = { %{ $oidcSession->data } };
}
else {

View File

@ -259,7 +259,7 @@ ok(
$client->_get( '/', undef, 'foo.example.fr', "lemonldap=$sessionId" ),
'Reject "foo.example.fr"'
);
ok( $res->[0] == 302, ' Code is 302' ) or explain( $res, 302 );
ok( $res->[0] == 403, ' Code is 403' ) or explain( $res, 403 );
count(2);
ok(
@ -268,7 +268,7 @@ ok(
),
'Reject "foo.example.org/orgdeny"'
);
ok( $res->[0] == 302, ' Code is 302' ) or explain( $res, 302 );
ok( $res->[0] == 403, ' Code is 403' ) or explain( $res, 403 );
count(2);
ok(
@ -286,7 +286,7 @@ ok(
),
'Reject "abfoo.example.org/orgdeny"'
);
ok( $res->[0] == 302, ' Code is 302' ) or explain( $res, 302 );
ok( $res->[0] == 403, ' Code is 403' ) or explain( $res, 403 );
count(2);
ok(
@ -312,7 +312,7 @@ ok(
$client->_get( '/', undef, 'abfoo.example.org', "lemonldap=$sessionId" ),
'Reject "abfoo.example.org/"'
);
ok( $res->[0] == 302, ' Code is 302' ) or explain( $res, 302 );
ok( $res->[0] == 403, ' Code is 403' ) or explain( $res, 403 );
count(2);
ok(

View File

@ -4,7 +4,7 @@ BEGIN {
require 't/test-psgi-lib.pm';
}
my $maintests = 21;
my $maintests = 25;
init(
'Lemonldap::NG::Handler::Server',
@ -57,7 +57,7 @@ Lemonldap::NG::Common::Session->new( {
info => {
"user_session_id" => $sessionId,
"_type" => "access_token",
"_utime" => time,
"_utime" => ( time - 72000 + 300 ),
"rp" => "rp-example2",
"scope" => "openid email read"
}
@ -74,7 +74,7 @@ Lemonldap::NG::Common::Session->new( {
info => {
"offline_session_id" => '000999000',
"_type" => "refresh_token",
"_utime" => time,
"_utime" => ( time - 72000 + 300 ),
"rp" => "rp-example",
"scope" => "openid email read"
}
@ -117,6 +117,7 @@ ok(
# Check headers
%h = @{ $res->[1] };
is( $res->[0], 401, "Got correct HTTP code" );
is( $h{'WWW-Authenticate'}, 'Bearer', 'Got WWW-Authenticate: Bearer' );
# Request with invalid Access Token
@ -210,6 +211,24 @@ is( $h{'Auth-ClientConfKey'},
'rp-example', 'Client confkey correctly transmitted' );
like( $h{'Auth-Scope'}, qr/\bemail\b/, 'Scope correctly transmitted' );
Time::Fake->offset("+600s");
ok(
$res = $client->_get(
'/read', undef,
'test1.example.com', '',
VHOSTTYPE => 'OAuth2',
HTTP_AUTHORIZATION => 'Bearer 999888777',
),
'Invalid access token'
);
%h = @{ $res->[1] };
is( $res->[0], 401, "Access was rejected" );
is(
$h{'WWW-Authenticate'},
'Bearer error="invalid_token"',
'Got correct error code'
);
count($maintests);
done_testing( count() );
clean();

View File

@ -2457,35 +2457,35 @@ m[^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
'default' => '',
'select' => [ {
'k' => '',
'v' => ''
'v' => 'JSON'
},
{
'k' => 'none',
'v' => 'None'
'v' => 'JWT/None'
},
{
'k' => 'HS256',
'v' => 'HS256'
'v' => 'JWT/HS256'
},
{
'k' => 'HS384',
'v' => 'HS384'
'v' => 'JWT/HS384'
},
{
'k' => 'HS512',
'v' => 'HS512'
'v' => 'JWT/HS512'
},
{
'k' => 'RS256',
'v' => 'RS256'
'v' => 'JWT/RS256'
},
{
'k' => 'RS384',
'v' => 'RS384'
'v' => 'JWT/RS384'
},
{
'k' => 'RS512',
'v' => 'RS512'
'v' => 'JWT/RS512'
}
],
'type' => 'select'

View File

@ -4265,14 +4265,14 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
oidcRPMetaDataOptionsUserInfoSignAlg => {
type => 'select',
select => [
{ k => '', v => '' },
{ k => 'none', v => 'None' },
{ k => 'HS256', v => 'HS256' },
{ k => 'HS384', v => 'HS384' },
{ k => 'HS512', v => 'HS512' },
{ k => 'RS256', v => 'RS256' },
{ k => 'RS384', v => 'RS384' },
{ k => 'RS512', v => 'RS512' },
{ k => '', v => 'JSON' },
{ k => 'none', v => 'JWT/None' },
{ k => 'HS256', v => 'JWT/HS256' },
{ k => 'HS384', v => 'JWT/HS384' },
{ k => 'HS512', v => 'JWT/HS512' },
{ k => 'RS256', v => 'JWT/RS256' },
{ k => 'RS384', v => 'JWT/RS384' },
{ k => 'RS512', v => 'JWT/RS512' },
],
default => '',
},

View File

@ -574,35 +574,35 @@ function templates(tpl,key) {
"select" : [
{
"k" : "",
"v" : ""
"v" : "JSON"
},
{
"k" : "none",
"v" : "None"
"v" : "JWT/None"
},
{
"k" : "HS256",
"v" : "HS256"
"v" : "JWT/HS256"
},
{
"k" : "HS384",
"v" : "HS384"
"v" : "JWT/HS384"
},
{
"k" : "HS512",
"v" : "HS512"
"v" : "JWT/HS512"
},
{
"k" : "RS256",
"v" : "RS256"
"v" : "JWT/RS256"
},
{
"k" : "RS384",
"v" : "RS384"
"v" : "JWT/RS384"
},
{
"k" : "RS512",
"v" : "RS512"
"v" : "JWT/RS512"
}
],
"title" : "oidcRPMetaDataOptionsUserInfoSignAlg",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -689,7 +689,7 @@
"oidcRPMetaDataOptionsRule":"قاعدة الدخول",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"خاصّيّة المستخدم",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataOptionsUserInfoSignAlg":"UserInfo response format",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"اسم أوبين أيدي كونيكت RP",
"oidcRPStateTimeout":"حالة مهلة الجلسة",

View File

@ -689,7 +689,7 @@
"oidcRPMetaDataOptionsRule":"Access rule",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"User attribute",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataOptionsUserInfoSignAlg":"UserInfo response format",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"OpenID Connect RP Name",
"oidcRPStateTimeout":"State session timeout",

View File

@ -689,7 +689,7 @@
"oidcRPMetaDataOptionsRule":"Access rule",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"User attribute",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataOptionsUserInfoSignAlg":"UserInfo response format",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"OpenID Connect RP Name",
"oidcRPStateTimeout":"State session timeout",

View File

@ -689,7 +689,7 @@
"oidcRPMetaDataOptionsRule":"Regla de acceso",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"Atributo de usuario",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataOptionsUserInfoSignAlg":"UserInfo response format",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"OpenID Connect RP Name",
"oidcRPStateTimeout":"Caducidad de estado de sesión",

View File

@ -689,7 +689,7 @@
"oidcRPMetaDataOptionsRule":"Règle d'accès",
"oidcRPMetaDataOptionsTimeouts":"Expiration",
"oidcRPMetaDataOptionsUserIDAttr":"Attribut de l'utilisateur",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Algorithme de signature des informations utilisateur",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Format de réponse Userinfo",
"oidcRPMetaDataScopeRules":"Règles de scope",
"oidcRPName":"Nom du client OpenID Connect",
"oidcRPStateTimeout":"Durée d'une session state",

View File

@ -689,7 +689,7 @@
"oidcRPMetaDataOptionsRule":"Regola di accesso",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"Attributo utente",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataOptionsUserInfoSignAlg":"UserInfo response format",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"Nome di OpenID Connect RP",
"oidcRPStateTimeout":"Durata della sessione stato",

View File

@ -689,7 +689,7 @@
"oidcRPMetaDataOptionsRule":"Reguła dostępu",
"oidcRPMetaDataOptionsTimeouts":"Limit czasu",
"oidcRPMetaDataOptionsUserIDAttr":"Atrybut użytkownika",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataOptionsUserInfoSignAlg":"UserInfo response format",
"oidcRPMetaDataScopeRules":"Zasady dotyczące zakresu",
"oidcRPName":"Nazwa RP OpenID Connect",
"oidcRPStateTimeout":"Limit czasu sesji stanowej",

View File

@ -689,7 +689,7 @@
"oidcRPMetaDataOptionsRule":"Erişim kuralı",
"oidcRPMetaDataOptionsTimeouts":"Zaman aşımları",
"oidcRPMetaDataOptionsUserIDAttr":"Kullanıcı niteliği",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataOptionsUserInfoSignAlg":"UserInfo response format",
"oidcRPMetaDataScopeRules":"Kapsam kuralları",
"oidcRPName":"OpenID Connect RP Adı",
"oidcRPStateTimeout":"Oturum zaman aşımını belirle",
@ -699,7 +699,7 @@
"oidcServiceAllowHybridFlow":"Hibrit Akış",
"oidcServiceAllowImplicitFlow":"Kapalı Akış",
"oidcServiceAllowOffline":"Çevrimdışı erişime izin ver",
"oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
"oidcServiceAllowOnlyDeclaredScopes":"Sadece belirli kapsamlara izin ver",
"oidcServiceAuthorizationCodeExpiration":"Yetkilendirme Kodu sona erme",
"oidcServiceDynamicRegistrationExportedVars":"Dinamik kayıtlanma için dışa aktarılan değişkenler",
"oidcServiceDynamicRegistrationExtraClaims":"Dinamik kayıtlanma için ekstra talepler",

View File

@ -689,7 +689,7 @@
"oidcRPMetaDataOptionsRule":"Quy tắc truy cập",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"thuộc tính người dùng",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataOptionsUserInfoSignAlg":"UserInfo response format",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"OpenID Connect RP Name",
"oidcRPStateTimeout":"Thời gian chờ của trạng thái phiên làm việc",

View File

@ -689,7 +689,7 @@
"oidcRPMetaDataOptionsRule":"Access rule",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"User attribute",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataOptionsUserInfoSignAlg":"UserInfo response format",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"OpenID Connect RP Name",
"oidcRPStateTimeout":"State session timeout",

View File

@ -689,7 +689,7 @@
"oidcRPMetaDataOptionsRule":"存取規則",
"oidcRPMetaDataOptionsTimeouts":"逾時",
"oidcRPMetaDataOptionsUserIDAttr":"使用者屬性",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataOptionsUserInfoSignAlg":"UserInfo response format",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"OpenID 連線 RP 名稱",
"oidcRPStateTimeout":"狀態工作階段逾時",

View File

@ -741,8 +741,11 @@ sub run {
my $access_token;
my $at_hash;
my $release_claims_in_id_token = 1;
if ( $response_type =~ /\btoken\b/ ) {
$release_claims_in_id_token = 0;
# Store data in access token
# Generate access_token
$access_token = $self->newAccessToken(
@ -775,9 +778,14 @@ sub run {
if $hash_level;
}
my $id_token =
$self->_generateIDToken( $req, $oidc_request,
$rp, $scope, { at_hash => $at_hash } );
my $id_token = $self->_generateIDToken(
$req,
$rp,
$scope,
$req->sessionInfo,
$release_claims_in_id_token,
{ at_hash => $at_hash, nonce => $oidc_request->{nonce} }
);
unless ($id_token) {
$self->logger->error("Could not generate ID token");
@ -841,8 +849,11 @@ sub run {
$c_hash = $self->createHash( $code, $hash_level )
if $hash_level;
my $release_claims_in_id_token = 1;
if ( $response_type =~ /\btoken\b/ ) {
$release_claims_in_id_token = 0;
# Generate access_token
$access_token = $self->newAccessToken(
$req, $rp, $scope,
@ -873,12 +884,13 @@ sub run {
if ( $response_type =~ /\bid_token\b/ ) {
$id_token = $self->_generateIDToken(
$req,
$oidc_request,
$rp, $scope,
$req, $rp, $scope,
$req->sessionInfo,
$release_claims_in_id_token,
{
at_hash => $at_hash,
c_hash => $c_hash,
nonce => $oidc_request->{nonce},
}
);
@ -1260,6 +1272,28 @@ sub _handlePasswordGrant {
$self->logger->debug("Generated refresh token: $refresh_token");
}
# Generate ID token
my $id_token = undef;
if ( $self->_hasScope( "openid", $scope ) ) {
# 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 )
if $hash_level;
$id_token =
$self->_generateIDToken( $req, $rp, $scope, $req->sessionInfo, 0,
{ ( $at_hash ? ( at_hash => $at_hash ) : () ), } );
unless ($id_token) {
$self->logger->error(
"Failed to generate ID Token for service: $client_id");
return $self->sendOIDCError( $req, 'server_error', 500 );
}
}
# Send token response
my $expires_in =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
@ -1272,6 +1306,7 @@ sub _handlePasswordGrant {
expires_in => $expires_in + 0,
( ( $scope ne $req_scope ) ? ( scope => "$scope" ) : () ),
( $refresh_token ? ( refresh_token => "$refresh_token" ) : () ),
( $id_token ? ( id_token => "$id_token" ) : () ),
};
$self->logger->debug("Send token response");
@ -1432,45 +1467,17 @@ sub _handleAuthorizationCodeGrant {
my $at_hash = $self->createHash( $access_token, $hash_level )
if $hash_level;
# ID token payload
# TODO: refactor to use _generateIDToken
my $id_token_exp =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsIDTokenExpiration}
|| $self->conf->{oidcServiceIDTokenExpiration};
$id_token_exp += time;
my $id_token_acr = "loa-" . $apacheSession->data->{authenticationLevel};
my $id_token_payload_hash = {
iss => $self->iss, # Issuer Identifier
sub => $user_id, # Subject Identifier
aud => $self->getAudiences($rp), # 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;
if ( $self->force_id_claims($rp) ) {
my $claims = $self->buildUserInfoResponseFromId( $req, $scope,
$rp, $codeSession->data->{user_session_id} );
foreach ( keys %$claims ) {
$id_token_payload_hash->{$_} = $claims->{$_}
unless ( $_ eq "sub" );
}
}
# Create ID Token
my $id_token = $self->createIDToken( $req, $id_token_payload_hash, $rp );
my $nonce = $codeSession->data->{nonce};
my $id_token = $self->_generateIDToken(
$req, $rp, $scope,
$apacheSession->data,
0,
{
( $nonce ? ( nonce => $nonce ) : () ),
( $at_hash ? ( at_hash => $at_hash ) : () ),
}
);
unless ($id_token) {
$self->logger->error(
@ -1533,8 +1540,6 @@ sub _handleRefreshTokenGrant {
}
my $access_token;
my $user_id;
my $auth_time;
my $session;
# If this refresh token is tied to a SSO session
@ -1548,10 +1553,6 @@ sub _handleRefreshTokenGrant {
return $self->sendOIDCError( $req, 'invalid_grant', 400 );
}
$user_id = $self->getUserIDForRP( $req, $rp, $session->data );
$auth_time = $session->data->{_lastAuthnUTime};
# Generate access_token
$access_token = $self->newAccessToken(
$req, $rp,
@ -1610,11 +1611,6 @@ sub _handleRefreshTokenGrant {
$refreshSession->data->{$_} = $req->sessionInfo->{$_};
}
$user_id = $self->getUserIDForRP( $req, $rp, $req->sessionInfo );
$self->logger->debug("Found corresponding user: $user_id");
$auth_time = $refreshSession->data->{auth_time};
# Generate access_token
$access_token = $self->newAccessToken(
$req, $rp,
@ -1640,57 +1636,30 @@ sub _handleRefreshTokenGrant {
my $at_hash = $self->createHash( $access_token, $hash_level )
if $hash_level;
# ID token payload
# TODO: refactor to use _generateIDToken
my $id_token_exp =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsIDTokenExpiration}
|| $self->conf->{oidcServiceIDTokenExpiration};
$id_token_exp += time;
# Authentication level using refresh tokens should probably stay at 0
my $id_token_acr = "loa-0";
my $id_token_payload_hash = {
iss => $self->iss, # Issuer Identifier
sub => $user_id, # Subject Identifier
aud => $self->getAudiences($rp), # Audience
exp => $id_token_exp, # expiration
iat => time, # Issued time
# TODO: is this the right value when using refresh tokens??
auth_time => $auth_time, # Authentication time
acr => $id_token_acr, # Authentication Context Class Reference
azp => $client_id, # Authorized party
# TODO amr
};
my $nonce = $refreshSession->data->{nonce};
$id_token_payload_hash->{nonce} = $nonce if defined $nonce;
$id_token_payload_hash->{'at_hash'} = $at_hash if $at_hash;
# If we forced sending claims in ID token
if ( $self->force_id_claims($rp) ) {
my $claims =
$self->buildUserInfoResponse( $req, $refreshSession->data->{scope},
$rp, $session );
foreach ( keys %$claims ) {
$id_token_payload_hash->{$_} = $claims->{$_}
unless ( $_ eq "sub" );
}
}
# Create ID Token
my $id_token = $self->createIDToken( $req, $id_token_payload_hash, $rp );
my $id_token = undef;
if ( $self->_hasScope( 'openid', $refreshSession->data->{scope} ) ) {
my $nonce = $refreshSession->data->{nonce};
$id_token = $self->_generateIDToken(
$req, $rp,
$refreshSession->data->{scope},
$session->data,
0,
{
( $nonce ? ( nonce => $nonce ) : () ),
( $at_hash ? ( at_hash => $at_hash ) : () ),
}
);
unless ($id_token) {
$self->logger->error(
"Failed to generate ID Token for service: $client_id");
return $self->sendOIDCError( $req, 'server_error', 500 );
unless ($id_token) {
$self->logger->error(
"Failed to generate ID Token for service: $rp");
return $self->sendOIDCError( $req, 'server_error', 500 );
}
$self->logger->debug("Generated id token: $id_token");
}
$self->logger->debug("Generated id token: $id_token");
# Send token response
my $expires_in =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
@ -1701,7 +1670,7 @@ sub _handleRefreshTokenGrant {
access_token => "$access_token",
token_type => 'Bearer',
expires_in => $expires_in + 0,
id_token => "$id_token",
( $id_token ? ( id_token => "$id_token" ) : () ),
};
# TODO
@ -2348,10 +2317,11 @@ sub _convertOldFormatConsents {
}
sub _generateIDToken {
my ( $self, $req, $oidc_request, $rp, $scope, $extra_claims ) = @_;
my ( $self, $req, $rp, $scope, $sessionInfo, $release_user_claims,
$extra_claims )
= @_;
my $response_type = $oidc_request->{'response_type'};
my $client_id = $oidc_request->{'client_id'};
my $client_id = $self->oidcRPList->{$rp}->{oidcRPMetaDataOptionsClientID};
my $id_token_exp =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
@ -2359,7 +2329,7 @@ sub _generateIDToken {
|| $self->conf->{oidcServiceIDTokenExpiration};
$id_token_exp += time;
my $authenticationLevel = $req->{sessionInfo}->{authenticationLevel} || 0;
my $authenticationLevel = $sessionInfo->{authenticationLevel} || 0;
my $id_token_acr = "loa-$authenticationLevel";
foreach ( keys %{ $self->conf->{oidcServiceMetaDataAuthnContext} } ) {
@ -2371,20 +2341,18 @@ sub _generateIDToken {
}
}
my $user_id = $self->getUserIDForRP( $req, $rp, $req->{sessionInfo} );
my $user_id = $self->getUserIDForRP( $req, $rp, $sessionInfo );
my $id_token_payload_hash = {
iss => $self->iss, # Issuer Identifier
sub => $user_id, # Subject Identifier
aud => $self->getAudiences($rp), # Audience
exp => $id_token_exp, # expiration
iat => time, # Issued time
auth_time => $req->{sessionInfo}->{_lastAuthnUTime}
, # Authentication time
iss => $self->iss, # Issuer Identifier
sub => $user_id, # Subject Identifier
aud => $self->getAudiences($rp), # Audience
exp => $id_token_exp, # expiration
iat => time, # Issued time
auth_time => $sessionInfo->{_lastAuthnUTime}, # Authentication time
acr => $id_token_acr, # Authentication Context Class Reference
azp => $client_id, # Authorized party
# TODO amr
nonce => $oidc_request->{'nonce'} # Nonce
};
for ( keys %{$extra_claims} ) {
@ -2392,14 +2360,12 @@ sub _generateIDToken {
if $extra_claims->{$_};
}
if ( $response_type !~ /\btoken\b/
|| $self->force_id_claims($rp) )
{
# Decided by response_type or forced in RP config
if ( $release_user_claims || $self->force_id_claims($rp) ) {
# No access_token
# Claims must be set in id_token
my $claims =
$self->buildUserInfoResponseFromId( $req, $scope, $rp, $req->id );
$self->buildUserInfoResponseFromData( $req, $scope, $rp,
$sessionInfo );
foreach ( keys %$claims ) {
$id_token_payload_hash->{$_} = $claims->{$_}

View File

@ -181,6 +181,10 @@ ok( $res->{cn} eq 'Frédéric Accents', 'UTF-8 values' )
or explain( $res, 'cn => Frédéric Accents' );
count(2);
my $id_token_decoded = id_token_payload( $res->{_oidc_id_token} );
is( $id_token_decoded->{acr}, 'customacr-1', "Correct custom ACR" );
count(1);
# Logout initiated by RP
ok(
$res = $rp->_get(
@ -193,7 +197,7 @@ ok(
);
count(1);
( $url, $query ) = expectRedirection( $res,
qr#http://auth.op.com(/oauth2/logout)\?(post_logout_redirect_uri=.+)$# );
qr#http://auth.op.com(/oauth2/logout)\?.*(post_logout_redirect_uri=.+)$# );
# Push logout to OP
switch ('op');
@ -337,11 +341,11 @@ sub op {
oidcOPMetaDataJSON => {},
oidcOPMetaDataJWKS => {},
oidcServiceMetaDataAuthnContext => {
'loa-4' => 4,
'loa-1' => 1,
'loa-5' => 5,
'loa-2' => 2,
'loa-3' => 3
'loa-4' => 4,
'customacr-1' => 1,
'loa-5' => 5,
'loa-2' => 2,
'loa-3' => 3
},
oidcServicePrivateKeySig => oidc_key_op_private_sig,
oidcServicePublicKeySig => oidc_key_op_public_sig,
@ -378,6 +382,7 @@ sub rp {
oidcOPMetaDataOptionsMaxAge => 30,
oidcOPMetaDataOptionsDisplay => "",
oidcOPMetaDataOptionsClientID => "rpid",
oidcOPMetaDataOptionsStoreIDToken => 1,
oidcOPMetaDataOptionsConfigurationURI =>
"https://auth.op.com/.well-known/openid-configuration"
}

View File

@ -159,9 +159,10 @@ count(4);
# Check attributes in ID Token
my $id_token_decoded = id_token_payload( $prms{id_token} );
ok( $id_token_decoded->{sub} eq "dwho", 'Check sub value' );
is( $id_token_decoded->{sub}, "dwho", 'Check sub value' );
ok( !$id_token_decoded->{name}, 'Claim name must not be in ID token' );
count(2);
is( $id_token_decoded->{azp}, 'rpid', ' azp found' );
count(3);
$op->logout($idpId);

View File

@ -51,6 +51,8 @@ sub runTest {
ok( $id_token, "Got ID token" );
my $id_token_payload = id_token_payload($id_token);
my $auth_time = $id_token_payload->{auth_time};
ok( $auth_time, "Authentication date found in token");
is(
$id_token_payload->{name},
'Frédéric Accents',
@ -117,6 +119,7 @@ sub runTest {
ok( !defined $refresh_token2, "Refresh token not present" );
$id_token_payload = id_token_payload($id_token);
is( $id_token_payload->{auth_time}, $auth_time, 'Original auth_time retained' );
is(
$id_token_payload->{name},
'Frédéric Accents',

View File

@ -52,7 +52,7 @@ my $op = LLNG::Manager::Test->new( {
oidcRPMetaDataOptionsIDTokenSignAlg => "HS512",
oidcRPMetaDataOptionsClientSecret => "rpsecret",
oidcRPMetaDataOptionsUserIDAttr => "",
oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
oidcRPMetaDataOptionsAccessTokenExpiration => 120,
oidcRPMetaDataOptionsBypassConsent => 1,
oidcRPMetaDataOptionsRefreshToken => 1,
oidcRPMetaDataOptionsIDTokenForceClaims => 1,
@ -97,7 +97,7 @@ my $goodquery = buildForm( {
grant_type => 'password',
username => 'french',
password => 'french',
scope => 'openid profile email',
scope => 'profile email openid',
}
);
@ -136,8 +136,12 @@ 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" );
count(1);
ok( $token_res_scope, "Scope found in token response" );
ok( $payload->{id_token}, "Found ID token in original grant" );
my $refresh_token = $payload->{refresh_token};
ok( $refresh_token, "Got refresh token" );
count(3);
# Get userinfo
$res = $op->_post(
@ -180,6 +184,46 @@ 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("+305m");
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);
ok( $res->{id_token}, "Found ID token in refresh grant" );
clean_sessions();
done_testing();

View File

@ -0,0 +1,186 @@
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',
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 => {
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
my $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" );
my $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();

View File

@ -159,6 +159,7 @@ BuildRequires: perl(SOAP::Transport::HTTP)
BuildRequires: perl(strict)
BuildRequires: perl(String::Random)
BuildRequires: perl(Sys::Syslog)
BuildRequires: perl(Test::LeakTrace)
BuildRequires: perl(Test::MockObject)
BuildRequires: perl(Test::Output)
BuildRequires: perl(Test::Pod) >= 1.00