diff --git a/doc/sources/admin/variables.rst b/doc/sources/admin/variables.rst index 7609bb75c335caca9e5ead52c09fe7377e7c3416..fbc51a3690fb9fbadc758aa0377fe09a6c9709fb 100644 --- a/doc/sources/admin/variables.rst +++ b/doc/sources/admin/variables.rst @@ -148,15 +148,17 @@ Key Description OpenID Connect -------------- -============================ =============================================== +============================ ====================================================================== Key Description -============================ =============================================== +============================ ====================================================================== \_oidc_id_token ID Token \_oidc_OP Configuration key of OP used for authentication \_oidc_access_token OAuth2 Access Token used to get UserInfo data +\_oidc_access_token_eol Timestamp after which the Access Token should no longer be valid +\_oidc_refresh_token OAuth2 Refresh Token. This should never be transmitted to applications \_oidc_consent_scope\_\ *rp* Scope for which consent was given for RP *rp* \_oidc_consent_time\_\ *rp* Time when consent was given for RP *rp* -============================ =============================================== +============================ ====================================================================== Other ----- diff --git a/lemonldap-ng-manager/site/coffee/sessions.coffee b/lemonldap-ng-manager/site/coffee/sessions.coffee index 9d4e7cb87d317ecfaa40d67387086c0e49f570a4..d9434ee51cfa0665d2399b2639ab5e292238f471 100644 --- a/lemonldap-ng-manager/site/coffee/sessions.coffee +++ b/lemonldap-ng-manager/site/coffee/sessions.coffee @@ -114,7 +114,7 @@ categories = saml: ['_idp', '_idpConfKey', '_samlToken', '_lassoSessionDump', '_lassoIdentityDump'] groups: ['groups', 'hGroups'] ldap: ['dn'] - OpenIDConnect: ['_oidc_id_token', '_oidc_OP', '_oidc_access_token'] + OpenIDConnect: ['_oidc_id_token', '_oidc_OP', '_oidc_access_token', '_oidc_refresh_token', '_oidc_access_token_eol'] sfaTitle: ['_2fDevices'] oidcConsents: ['_oidcConsents'] diff --git a/lemonldap-ng-manager/site/htdocs/static/js/sessions.js b/lemonldap-ng-manager/site/htdocs/static/js/sessions.js index 0ec970524960d034c9538f4fe31b0bb32e98212b..54978e8c924084bce9645dc75df7702fb2faffda 100644 --- a/lemonldap-ng-manager/site/htdocs/static/js/sessions.js +++ b/lemonldap-ng-manager/site/htdocs/static/js/sessions.js @@ -126,7 +126,7 @@ saml: ['_idp', '_idpConfKey', '_samlToken', '_lassoSessionDump', '_lassoIdentityDump'], groups: ['groups', 'hGroups'], ldap: ['dn'], - OpenIDConnect: ['_oidc_id_token', '_oidc_OP', '_oidc_access_token'], + OpenIDConnect: ['_oidc_id_token', '_oidc_OP', '_oidc_access_token', '_oidc_refresh_token', '_oidc_access_token_eol'], sfaTitle: ['_2fDevices'], oidcConsents: ['_oidcConsents'] }; diff --git a/lemonldap-ng-manager/site/htdocs/static/js/sessions.min.js b/lemonldap-ng-manager/site/htdocs/static/js/sessions.min.js index 22e5813c0e595f3500f9e27f54fe6b9f9ad649a7..9ebafdfe7a1e471efc2dabe8e62140dae123eb7a 100644 --- a/lemonldap-ng-manager/site/htdocs/static/js/sessions.min.js +++ b/lemonldap-ng-manager/site/htdocs/static/js/sessions.min.js @@ -1 +1 @@ -!function(){var f={_whatToTrace:[function(e,t){return"groupBy=substr("+e+",1)"},function(e,t){return e+"="+t+"*&groupBy="+e},function(e,t){return e+"="+t}],ipAddr:[function(e,t){return"groupBy=net("+e+",16,1)"},function(e,t){return t.match(/:/)||(t+="."),e+"="+t+"*&groupBy=net("+e+",32,2)"},function(e,t){return t.match(/:/)||(t+="."),e+"="+t+"*&groupBy=net("+e+",48,3)"},function(e,t){return t.match(/:/)||(t+="."),e+"="+t+"*&groupBy=net("+e+",128,4)"},function(e,t){return e+"="+t+"&groupBy=_whatToTrace"},function(e,t,n){return n.replace(/\&groupBy.*$/,"")+"&_whatToTrace="+t}],_startTime:[function(e,t){return"groupBy=substr("+e+",8)"},function(e,t){return e+"="+t+"*&groupBy=substr("+e+",10)"},function(e,t){return e+"="+t+"*&groupBy=substr("+e+",11)"},function(e,t){return e+"="+t+"*&groupBy=substr("+e+",12)"},function(e,t){return e+"="+t+"*&groupBy=_whatToTrace"},function(e,t,n){return console.log(e),console.log(t),console.log(n),n.replace(/\&groupBy.*$/,"")+"&_whatToTrace="+t}],doubleIp:[function(e,t){return e},function(e,t){return"_whatToTrace="+t+"&groupBy=ipAddr"},function(e,t,n){return n.replace(/\&groupBy.*$/,"")+"&ipAddr="+t}],_session_uid:[function(e,t){return"groupBy=substr("+e+",1)"},function(e,t){return e+"="+t+"*&groupBy="+e},function(e,t){return e+"="+t}]},g={_whatToTrace:function(e,t,n,o){return console.log("overScheme => level",n,"over",o),1===n&&t.length>o?e+"="+t+"*&groupBy=substr("+e+","+(n+o+1)+")":null},ipAddr:function(e,t,n,o){return console.log("overScheme => level",n,"over",o),0 level",n,"over",o),3 level",n,"over",o),1===n&&t.length>o?e+"="+t+"*&groupBy=substr("+e+","+(n+o+1)+")":null}},M={dateTitle:["_utime","_startTime","_updateTime","_lastAuthnUTime","_lastSeen"],connectionTitle:["ipAddr","_timezone","_url"],authenticationTitle:["_session_id","_user","_password","authenticationLevel"],modulesTitle:["_auth","_userDB","_passwordDB","_issuerDB","_authChoice","_authMulti","_userDBMulti","_2f"],saml:["_idp","_idpConfKey","_samlToken","_lassoSessionDump","_lassoIdentityDump"],groups:["groups","hGroups"],ldap:["dn"],OpenIDConnect:["_oidc_id_token","_oidc_OP","_oidc_access_token"],sfaTitle:["_2fDevices"],oidcConsents:["_oidcConsents"]},i={session:[{title:"deleteSession",icon:"trash"}],home:[]};angular.module("llngSessionsExplorer",["ui.tree","ui.bootstrap","llApp"]).controller("SessionsExplorerCtrl",["$scope","$translator","$location","$q","$http",function(H,t,r,e,o){var p,n,d;return H.links=links,H.menulinks=menulinks,H.staticPrefix=staticPrefix,H.scriptname=scriptname,H.formPrefix=formPrefix,H.impPrefix=impPrefix,H.sessionTTL=sessionTTL,H.availableLanguages=availableLanguages,H.waiting=!0,H.showM=!1,H.showT=!0,H.data=[],H.currentScope=null,H.currentSession=null,H.menu=i,H.translateP=t.translateP,H.translate=t.translate,H.translateTitle=function(e){return t.translateField(e,"title")},d="global",H.menuClick=function(e){if(e.popup)window.open(e.popup);else switch(e.action||(e.action=e.title),typeof e.action){case"function":e.action(H.currentNode,H);break;case"string":H[e.action]();break;default:console.log(typeof e.action)}return H.showM=!1},H.deleteOIDCConsent=function(e,t){var i=document.querySelectorAll(".data-"+t);return H.waiting=!0,o.delete(scriptname+"sessions/OIDCConsent/"+d+"/"+H.currentSession.id+"?rp="+e+"&epoch="+t).then(function(e){var t,n,o,r;for(H.waiting=!1,r=[],n=0,o=i.length;nt.title?1:e.title real attribute"),B.push(i)):P.push(i);return I=P.concat(B),L.push({title:"__attributesAndMacros__",nodes:I}),{_utime:E,nodes:L}};return H.currentScope=e,t=e.$modelValue.session,o.get(scriptname+"sessions/"+d+"/"+t).then(function(e){return H.currentSession=n(e.data),H.currentSession.id=t}),H.showT=!1},H.localeDate=function(e){return new Date(1e3*e).toLocaleString()},H.isValid=function(e,t){var n=r.path(),o=Date.now()/1e3;return console.log("Path",n),console.log("Session epoch",e),console.log("Current date",o),console.log("Session TTL",sessionTTL),e=o-e level",n,"over",o),1===n&&t.length>o?e+"="+t+"*&groupBy=substr("+e+","+(n+o+1)+")":null},ipAddr:function(e,t,n,o){return console.log("overScheme => level",n,"over",o),0 level",n,"over",o),3 level",n,"over",o),1===n&&t.length>o?e+"="+t+"*&groupBy=substr("+e+","+(n+o+1)+")":null}},M={dateTitle:["_utime","_startTime","_updateTime","_lastAuthnUTime","_lastSeen"],connectionTitle:["ipAddr","_timezone","_url"],authenticationTitle:["_session_id","_user","_password","authenticationLevel"],modulesTitle:["_auth","_userDB","_passwordDB","_issuerDB","_authChoice","_authMulti","_userDBMulti","_2f"],saml:["_idp","_idpConfKey","_samlToken","_lassoSessionDump","_lassoIdentityDump"],groups:["groups","hGroups"],ldap:["dn"],OpenIDConnect:["_oidc_id_token","_oidc_OP","_oidc_access_token","_oidc_refresh_token","_oidc_access_token_eol"],sfaTitle:["_2fDevices"],oidcConsents:["_oidcConsents"]},i={session:[{title:"deleteSession",icon:"trash"}],home:[]};angular.module("llngSessionsExplorer",["ui.tree","ui.bootstrap","llApp"]).controller("SessionsExplorerCtrl",["$scope","$translator","$location","$q","$http",function(H,t,r,e,o){var p,n,d;return H.links=links,H.menulinks=menulinks,H.staticPrefix=staticPrefix,H.scriptname=scriptname,H.formPrefix=formPrefix,H.impPrefix=impPrefix,H.sessionTTL=sessionTTL,H.availableLanguages=availableLanguages,H.waiting=!0,H.showM=!1,H.showT=!0,H.data=[],H.currentScope=null,H.currentSession=null,H.menu=i,H.translateP=t.translateP,H.translate=t.translate,H.translateTitle=function(e){return t.translateField(e,"title")},d="global",H.menuClick=function(e){if(e.popup)window.open(e.popup);else switch(e.action||(e.action=e.title),typeof e.action){case"function":e.action(H.currentNode,H);break;case"string":H[e.action]();break;default:console.log(typeof e.action)}return H.showM=!1},H.deleteOIDCConsent=function(e,t){var i=document.querySelectorAll(".data-"+t);return H.waiting=!0,o.delete(scriptname+"sessions/OIDCConsent/"+d+"/"+H.currentSession.id+"?rp="+e+"&epoch="+t).then(function(e){var t,n,o,r;for(H.waiting=!1,r=[],n=0,o=i.length;nt.title?1:e.title real attribute"),B.push(i)):P.push(i);return I=P.concat(B),L.push({title:"__attributesAndMacros__",nodes:I}),{_utime:E,nodes:L}};return H.currentScope=e,t=e.$modelValue.session,o.get(scriptname+"sessions/"+d+"/"+t).then(function(e){return H.currentSession=n(e.data),H.currentSession.id=t}),H.showT=!1},H.localeDate=function(e){return new Date(1e3*e).toLocaleString()},H.isValid=function(e,t){var n=r.path(),o=Date.now()/1e3;return console.log("Path",n),console.log("Session epoch",e),console.log("Current date",o),console.log("Session TTL",sessionTTL),e=o-elogger->debug("Token response is valid"); } - my $access_token = $token_response->{access_token}; - my $id_token = $token_response->{id_token}; + my $access_token = $token_response->{access_token}; + my $expires_in = $token_response->{expires_in}; + my $id_token = $token_response->{id_token}; + my $refresh_token = $token_response->{refresh_token}; + + undef $expires_in unless looks_like_number($expires_in); $self->logger->debug("Access token: $access_token"); + $self->logger->debug( + "Access token expires in: " . ( $expires_in || "" ) ); $self->logger->debug("ID token: $id_token"); + $self->logger->debug( + "Refresh token: " . ( $refresh_token || "" ) ); # Verify JWT signature if ( $self->conf->{oidcOPMetaDataOptions}->{$op} @@ -219,8 +228,15 @@ sub extractFormInfo { my $user_id = $id_token_payload_hash->{sub}; # Remember tokens - $req->data->{access_token} = $access_token; - $req->data->{id_token} = $id_token; + $req->data->{access_token} = $access_token; + $req->data->{refresh_token} = $refresh_token if $refresh_token; + $req->data->{id_token} = $id_token; + + # If access token TTL is given save expiration date + # (with security margin) + if ($expires_in) { + $req->data->{access_token_eol} = time + ( $expires_in * 0.9 ); + } $self->logger->debug( "Found user_id: " . $user_id ); $req->user($user_id); @@ -303,6 +319,16 @@ sub setAuthSessionInfo { $req->{sessionInfo}->{_oidc_access_token} = $req->data->{access_token}; + if ( $req->data->{refresh_token} ) { + $req->{sessionInfo}->{_oidc_refresh_token} = + $req->data->{refresh_token}; + } + + if ( $req->data->{access_token_eol} ) { + $req->{sessionInfo}->{_oidc_access_token_eol} = + $req->data->{access_token_eol}; + } + # Keep ID Token in session my $store_IDToken = $self->conf->{oidcOPMetaDataOptions}->{$op} ->{oidcOPMetaDataOptionsStoreIDToken}; diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm index 114c0dcd623fc90ff21b8133b0c5dec533fb3f6d..4307f7eb78cd631d33ee7c4c389fd3904eb734d1 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm @@ -18,6 +18,7 @@ use Lemonldap::NG::Common::JWT qw(getAccessTokenSessionId getJWTPayload getJWTHeader getJWTSignature getJWTSignedData); use MIME::Base64 qw/encode_base64 decode_base64 encode_base64url decode_base64url/; +use Scalar::Util qw/looks_like_number/; use Mouse; use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_REDIRECT); @@ -703,6 +704,134 @@ sub checkIDTokenValidity { return 1; } +# Returns the current OP and a valid Access token +sub getUserInfoParams { + my ( $self, $req ) = @_; + + my $op = $req->data->{_oidcOPCurrent}; + + if ($op) { + + # We are in the middle of an auth process, + # access token has just been fetched already + my $access_token = $req->data->{access_token}; + return ( $op, $access_token ); + } + else { + # Get OP and access token from existing session (refresh) + return $self->getUserInfoParamsFromSession($req); + } +} + +sub getUserInfoParamsFromSession { + my ( $self, $req ) = @_; + my $op = $req->userData->{_oidc_OP}; + + # Save current OP, we will need it for setSessionInfo & friends + $req->data->{_oidcOPCurrent} = $op; + + if ($op) { + my $access_token = $req->userData->{_oidc_access_token}; + my $access_token_eol = $req->userData->{_oidc_access_token_eol}; + if ($access_token_eol) { + return $self->refreshAccessTokenIfExpired( $req, $op ); + } + else { + # We don't know the TTL for this access token, + # so we can only hope that it works + return ( $op, $access_token ); + } + } + else { + $self->logger->warn("No OP found in session"); + return ( $op, undef ); + } +} + +sub refreshAccessTokenIfExpired { + my ( $self, $req, $op, $session ) = @_; + + # Handle unauthenticated OIDC calls + my $data = $session ? $session->data : $req->userData; + + my $access_token = $data->{_oidc_access_token}; + my $access_token_eol = $data->{_oidc_access_token_eol}; + if ( time < $access_token_eol ) { + + # Access Token is still valid, return it + return ( $op, $access_token ); + } + else { + # Refresh Access Token + return ( $op, $self->refreshAccessToken( $req, $op, $session ) ); + } +} + +sub refreshAccessToken { + my ( $self, $req, $op, $session ) = @_; + + # Handle unauthenticated OIDC calls + my $data = $session ? $session->data : $req->userData; + my $session_id = $session ? $session->id : $req->id; + + my $refresh_token = $data->{_oidc_refresh_token}; + + if ($refresh_token) { + + my $content = + $self->getAccessTokenFromTokenEndpoint( $req, $op, 'refresh_token', + { refresh_token => $refresh_token } ); + + if ($content) { + my $token_response = $self->decodeTokenResponse($content); + if ($token_response) { + + my $access_token = $token_response->{access_token}; + my $expires_in = $token_response->{expires_in}; + my $refresh_token = $token_response->{refresh_token}; + + undef $expires_in unless looks_like_number($expires_in); + + $self->logger->debug("Access token: $access_token"); + $self->logger->debug( "Access token expires in: " + . ( $expires_in || "" ) ); + $self->logger->debug( + "Refresh token: " . ( $refresh_token || "" ) ); + + my $updateSession; + + # Remember tokens + $updateSession->{_oidc_access_token} = $access_token; + $updateSession->{_oidc_refresh_token} = $refresh_token + if $refresh_token; + + # If access token TTL is given save expiration date + # (with security margin) + if ($expires_in) { + $updateSession->{_oidc_access_token_eol} = + time + ( $expires_in * 0.9 ); + } + + $self->p->updateSession( $req, $updateSession, $session_id ); + + return ($access_token); + } + else { + $self->logger->warn("Could not decode Token Response for $op"); + return undef; + } + } + else { + $self->logger->warn("Could not fetch new Access Token for $op"); + return undef; + } + } + else { + $self->logger->warn("No Refresh Token was found for $op"); + return undef; + } +} + # Get UserInfo response # return String UserInfo response decoded content sub getUserInfo { diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm index 9b216870d4fb0bfd5d4a5ba537c64a32500d3e2b..d3323e3a5f1683f19b73c6da2e596e07071f6046 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm @@ -203,8 +203,30 @@ sub refresh { $req->user( $data{_user} || $data{ $self->conf->{whatToTrace} } ); $req->id( $data{_session_id} ); foreach ( keys %data ) { - delete $data{$_} - unless ( /^_/ or /^(?:startTime|authenticationLevel)$/ ); + + # Variables that start with _ are kept accross refresh + if (/^_/) { + + # But not OIDC tokens, which can be refreshed + if ( +/^(_oidc_access_token|_oidc_refresh_token|_oidc_access_token_eol)$/ + ) + { + delete $data{$_}; + } + + } + + # Other variables should be refreshed + else { + # But not these two + if (/^(?:startTime|authenticationLevel)$/) { + next; + } + else { + delete $data{$_}; + } + } } $data{_updateTime} = strftime( "%Y%m%d%H%M%S", localtime() ); $self->logger->debug( diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/OpenIDConnect.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/OpenIDConnect.pm index 30444db0861c38775f78f859b7a7813d49ab5a79..b247317fd3b2eda10eb24d71a858da599bbfc550 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/OpenIDConnect.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/OpenIDConnect.pm @@ -27,7 +27,8 @@ sub init { sub getUser { my ( $self, $req ) = @_; - my $op = $req->data->{_oidcOPCurrent}; + + my ( $op, $access_token ) = $self->getUserInfoParams($req); # This is likely to happen when running getUser without extractFormInfo # see #1980 @@ -36,7 +37,10 @@ sub getUser { return PE_ERROR; } - my $access_token = $req->data->{access_token}; + unless ($access_token) { + $self->logger->warn("Could not get Access Token for User Info request"); + return PE_ERROR; + } my $userinfo_content = $self->getUserInfo( $op, $access_token ); diff --git a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code.t b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code.t index 9e03d21eceecd90a494d4fab8acd3902eb3c5649..30152d7d9659739f1313c77872c4167a2f03b36f 100644 --- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code.t +++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code.t @@ -200,9 +200,13 @@ count(2); switch ('rp'); ok( $res = $rp->_get("/sessions/global/$spId"), 'Get UTF-8' ); $res = expectJSON($res); -ok( $res->{cn} eq 'Frédéric Accents', 'UTF-8 values' ) - or explain( $res, 'cn => Frédéric Accents' ); -count(2); +my $access_token_eol = $res->{_oidc_access_token_eol}; +my $access_token_old = $res->{_oidc_access_token}; +ok( $access_token_eol, 'OIDC EOL time is stored' ); +ok( $access_token_old, 'Obtained refresh token' ); +is( $res->{cn}, 'Frédéric Accents', 'UTF-8 values' ); +is( $res->{mail}, 'fa@badwolf.org', 'Correct email' ); +count(5); is( $res->{userinfo_hook}, "op/french", "oidcGotUserInfo called" ); is( $res->{id_token_hook}, "op/french", "oidcGotIDToken called" ); @@ -212,6 +216,77 @@ my $id_token_decoded = id_token_payload( $res->{_oidc_id_token} ); is( $id_token_decoded->{acr}, 'customacr-1', "Correct custom ACR" ); count(1); +# Update session at OP +$Lemonldap::NG::Portal::UserDB::Demo::demoAccounts{french} = { + uid => 'french', + cn => 'Frédéric Accents', + mail => 'fa2@badwolf.org', + guy => '', + type => '', +}; +switch ('op'); +ok( $op->_get( '/refresh', cookie => "lemonldap=$idpId" ) ); +count(1); +switch ('rp'); + +# Test session refresh (before access token refresh) +ok( + $res = $rp->_get( + '/refresh', + cookie => "lemonldap=$spId", + accept => 'text/html' + ), + 'Query RP for refresh' +); +count(1); + +ok( $res = $rp->_get("/sessions/global/$spId"), 'Get session after refresh' ); +count(1); +$res = expectJSON($res); +my $access_token_new = $res->{_oidc_access_token}; +my $access_token_new_eol = $res->{_oidc_access_token_eol}; +is( $access_token_new_eol, $access_token_eol, + "Access token EOL has not changed" ); +is( $access_token_new, $access_token_old, "Access token has not changed" ); +is( $res->{mail}, 'fa2@badwolf.org', 'Updated RP session' ); +count(3); + +# Update session at OP +$Lemonldap::NG::Portal::UserDB::Demo::demoAccounts{french} = { + uid => 'french', + cn => 'Frédéric Accents', + mail => 'fa3@badwolf.org', + guy => '', + type => '', +}; +switch ('op'); +ok( $op->_get( '/refresh', cookie => "lemonldap=$idpId" ) ); +count(1); +switch ('rp'); + +# Test session refresh (with access token refresh) +Time::Fake->offset("+2h"); +ok( + $res = $rp->_get( + '/refresh', + cookie => "lemonldap=$spId", + accept => 'text/html' + ), + 'Query RP for refresh' +); +count(1); + +ok( $res = $rp->_get("/sessions/global/$spId"), 'Get session after refresh' ); +count(1); +$res = expectJSON($res); +$access_token_new = $res->{_oidc_access_token}; +$access_token_new_eol = $res->{_oidc_access_token_eol}; +isnt( $access_token_new_eol, $access_token_eol, + "Access token EOL has changed" ); +isnt( $access_token_new, $access_token_old, "Access token has changed" ); +is( $res->{mail}, 'fa3@badwolf.org', 'Updated RP session' ); +count(3); + # Logout initiated by RP ok( $res = $rp->_get( @@ -346,6 +421,7 @@ sub op { userDB => 'Same', issuerDBOpenIDConnectActivation => "1", restSessionServer => 1, + restExportSecretKeys => 1, oidcRPMetaDataExportedVars => { rp => { email => "mail", @@ -364,6 +440,7 @@ sub op { oidcRPMetaDataOptionsIDTokenSignAlg => "HS512", oidcRPMetaDataOptionsBypassConsent => 0, oidcRPMetaDataOptionsClientSecret => "rpsecret", + oidcRPMetaDataOptionsRefreshToken => 1, oidcRPMetaDataOptionsUserIDAttr => "", oidcRPMetaDataOptionsAccessTokenExpiration => 3600, oidcRPMetaDataOptionsPostLogoutRedirectUris => @@ -398,6 +475,7 @@ sub rp { authentication => 'OpenIDConnect', userDB => 'Same', restSessionServer => 1, + restExportSecretKeys => 1, oidcOPMetaDataExportedVars => { op => { cn => "name", @@ -411,7 +489,7 @@ sub rp { oidcOPMetaDataOptionsCheckJWTSignature => 1, oidcOPMetaDataOptionsJWKSTimeout => 0, oidcOPMetaDataOptionsClientSecret => "rpsecret", - oidcOPMetaDataOptionsScope => "openid profile", + oidcOPMetaDataOptionsScope => "openid profile email", oidcOPMetaDataOptionsStoreIDToken => 0, oidcOPMetaDataOptionsMaxAge => 30, oidcOPMetaDataOptionsDisplay => "",