[java-plugin-shibd-oidc] 01/03: JSHIBDOIDC-22 - Store off iss, sid in opaque session data to support logout

Codeberg noreply at shibboleth.net
Fri Jun 19 12:35:28 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch dev/JSHIBDOIDC-28
in repository java-plugin-shibd-oidc.

View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-oidc/commit/d84bfb226cfe6a5376e9255e76b64ca73b95e324

commit d84bfb226cfe6a5376e9255e76b64ca73b95e324
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Jun 17 14:46:06 2026 +0100

    JSHIBDOIDC-22 - Store off iss, sid in opaque session data to support
    logout
    
     - Store off the full id_token b64 compact serialised, which can be
    later used to extract everything required for logout
    
    https://shibboleth.atlassian.net/browse/JSHIBDOIDC-22
---
 .../sp/oidc/profile/impl/PrepareAgentResponse.java | 77 ++++++++++++++--------
 .../profile/impl/PrepareAgentResponseTest.java     |  9 +++
 2 files changed, 60 insertions(+), 26 deletions(-)

diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponse.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponse.java
index e19561d..1d65ffd 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponse.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponse.java
@@ -26,12 +26,14 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.slf4j.Logger;
 
+import com.nimbusds.jwt.JWT;
 import com.nimbusds.oauth2.sdk.token.AccessToken;
 import com.nimbusds.oauth2.sdk.token.RefreshToken;
 import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
 
 import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.security.DataSealer;
@@ -128,44 +130,36 @@ public class PrepareAgentResponse extends AbstractTokenConsumerResponseAction {
         if (tokenResponse != null) {
             final AccessToken accessToken = tokenResponse.getTokens().getAccessToken();
             final RefreshToken refreshToken = tokenResponse.getTokens().getRefreshToken();
+            final JWT idToken = tokenResponse.getOIDCTokens().getIDToken();
             
-            log.debug("{} Storing access and refresh tokens in session data", getLogPrefix());
+            log.debug("{} Storing id_token, access and refresh tokens in session data", getLogPrefix());
             final DDF tokens = new DDF("tokens").structure();   
             
-            final DataSealer localDataSealer = dataSealer;
-            
             // Add the refresh token if it exists
             if (refreshToken != null && refreshToken.getValue() != null) {
-                if (localDataSealer != null) {
-                    try {
-                        final String refreshTokenString = refreshToken.getValue();
-                        assert refreshTokenString != null;
-                        tokens.addmember("refresh_token").string(localDataSealer.wrap(refreshTokenString));
-                    } catch (final DataSealerException e) {
-                        log.debug("{} Error sealing refresh token for session data", getLogPrefix(), e);
-                        // Is not an error, but the refresh token won't be stored.
-                    }
-                } else {
-                    tokens.addmember("refresh_token").string(refreshToken.getValue());
+                try {
+                    final String refreshTokenString = refreshToken.getValue();
+                    assert refreshTokenString != null;
+                    tokens.addmember("refresh_token").string(conditionallySeal(refreshTokenString));
+                } catch (final DataSealerException e) {
+                    log.debug("{} Error sealing refresh token for session data", getLogPrefix(), e);
+                    // Is not an error, but the refresh token won't be stored.
                 }
+               
             }
 
             boolean accessTokenAdded = false; 
             if (accessToken != null && accessToken.getValue() != null) {
-                if (localDataSealer != null) {
-                    try {
-                        final String accessTokenString = accessToken.getValue();
-                        assert accessTokenString != null;
-                        tokens.addmember("access_token").string(localDataSealer.wrap(accessTokenString));
-                        accessTokenAdded = true;
-                    } catch (final DataSealerException e) {
-                        log.debug("{} Error sealing access token for session data", getLogPrefix(), e);
-                        // Is not an error,  but the access token and its attributes won't be stored.
-                    }
-                } else {
-                    tokens.addmember("access_token").string(accessToken.getValue());
+                try {
+                    final String accessTokenString = accessToken.getValue();
+                    assert accessTokenString != null;
+                    tokens.addmember("access_token").string(conditionallySeal(accessTokenString));
                     accessTokenAdded = true;
+                } catch (final DataSealerException e) {
+                    log.debug("{} Error sealing access token for session data", getLogPrefix(), e);
+                    // Is not an error,  but the access token and its attributes won't be stored.
                 }
+                
             }
 
             // Only add the attributes of an access_token if one exists
@@ -190,9 +184,40 @@ public class PrepareAgentResponse extends AbstractTokenConsumerResponseAction {
                 }
             }
             
+            if (idToken != null) {
+                final String serialisedIdToken = idToken.serialize();
+                if (serialisedIdToken != null) {
+                    try {
+                        tokens.addmember("id_token").string(conditionallySeal(serialisedIdToken));
+                    } catch (final DataSealerException e) {
+                        log.debug("{} Error sealing id_token for session data", getLogPrefix(), e);
+                        // Is not an error, but the id_token will not be stored for later use e.g., logout
+                    }                    
+                }
+            }
+            
             return tokens;
         }
         return null;
     }
+    
+    /**
+     * If the data sealer is configured, use it to seal the token value. If configured to, failure to seal a token 
+     * results in an exception being thrown. If a sealer is not configured, the token is passed back untouched.
+     * 
+     * @param token the token to seal or pass through
+     * @return the sealed token, or the original token passed-through
+     */
+    @Nonnull private String conditionallySeal(@Nonnull @NotEmpty final String token) throws DataSealerException {
+        
+        final DataSealer localDataSealer = dataSealer;
+        if (localDataSealer != null) {
+             return localDataSealer.wrap(token);
+
+        } else {
+            log.warn("{} DataSealer not configured, storing tokens in plaintext", getLogPrefix());
+        }
+        return token;          
+    }
 
 }
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponseTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponseTest.java
index abac2f4..e81506b 100644
--- a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponseTest.java
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponseTest.java
@@ -112,6 +112,8 @@ public class PrepareAgentResponseTest extends BaseApplicationActionTest{
         final DDF accessTokenDdf = tokens.getmember("access_token");
         assertFalse(accessTokenDdf.isnull());
         assertEquals(accessTokenDdf.string(),"access123");
+        final DDF idTokenDdf = tokens.getmember("id_token");
+        assertFalse(idTokenDdf.isnull());
     }
 
     @Test
@@ -119,6 +121,8 @@ public class PrepareAgentResponseTest extends BaseApplicationActionTest{
         final DataSealer sealer = Mockito.mock(DataSealer.class);
         Mockito.when(sealer.wrap("access123")).thenReturn("sealedAccess");
         Mockito.when(sealer.wrap("refresh123")).thenReturn("sealedRefresh");
+        // This is the b64 compact JSON serialisation of the very basic JWT used in the response.
+        Mockito.when(sealer.wrap("eyJhbGciOiJub25lIn0.e30.")).thenReturn("sealedIdToken");
         action.setDataSealer(sealer);
 
         action.initialize();
@@ -152,6 +156,9 @@ public class PrepareAgentResponseTest extends BaseApplicationActionTest{
         final DDF accessTokenDdf = tokens.getmember("access_token");
         assertFalse(accessTokenDdf.isnull());
         assertEquals(accessTokenDdf.string(),"sealedAccess");
+        final DDF idTokenDdf = tokens.getmember("id_token");
+        assertFalse(idTokenDdf.isnull());
+        assertEquals(idTokenDdf.string(),"sealedIdToken");
     }
 
     @SuppressWarnings("null")
@@ -192,6 +199,8 @@ public class PrepareAgentResponseTest extends BaseApplicationActionTest{
         assertTrue(refreshTokenDdf.isempty());
         final DDF accessTokenDdf = tokens.getmember("access_token");
         assertTrue(accessTokenDdf.isempty());
+        final DDF idTokenDdf = tokens.getmember("id_token");
+        assertTrue(idTokenDdf.isempty());
     }
 
 

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list