[java-plugin-shibd] branch main updated: WIP on state token manager.

Scott Cantor cantor.2 at osu.edu
Mon Aug 12 19:51:25 UTC 2024


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

scantor pushed a commit to branch main
in repository java-plugin-shibd.

View the commit online:
http://git.shibboleth.net/view/?p=java-plugin-shibd.git;a=commit;h=5b37edcd3b74ea626f4b296c692119effa512263

The following commit(s) were added to refs/heads/main by this push:
     new 5b37edc  WIP on state token manager.
5b37edc is described below

commit 5b37edcd3b74ea626f4b296c692119effa512263
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Mon Aug 12 15:51:21 2024 -0400

    WIP on state token manager.
---
 .../shibboleth/sp/AbstractStateTokenManager.java   |  97 +++++++++++++++
 .../main/java/net/shibboleth/sp/Application.java   |   7 ++
 .../java/net/shibboleth/sp/StateTokenManager.java  |  67 +++++++++++
 .../net/shibboleth/sp/impl/BasicApplication.java   |  26 +++-
 .../sp/impl/StorageServiceStateTokenManager.java   | 133 +++++++++++++++++++++
 5 files changed, 329 insertions(+), 1 deletion(-)

diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/AbstractStateTokenManager.java b/sp-server-api/src/main/java/net/shibboleth/sp/AbstractStateTokenManager.java
new file mode 100644
index 0000000..f240912
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/AbstractStateTokenManager.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp;
+
+import java.time.Duration;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+
+/**
+ * Base class for {@link StateTokenManager} implementations.
+ */
+public abstract class AbstractStateTokenManager extends AbstractIdentifiableInitializableComponent
+        implements StateTokenManager {
+    
+    /** Identifier generation. */
+    @NonnullAfterInit private IdentifierGenerationStrategy identifierStrategy;
+    
+    /** Expiration for state token. */
+    @Nonnull private Duration expiration;
+    
+    /** Constructor. */
+    @SuppressWarnings("null")
+    public AbstractStateTokenManager() {
+        expiration = Duration.ofMinutes(30);
+    }
+    
+    /**
+     * Set {@link IdentifierGenerationStrategy} to use.
+     * 
+     * @param strategy identifier generator strategy
+     */
+    public void setIdentifierGenerationStrategy(@Nonnull final IdentifierGenerationStrategy strategy) {
+        checkSetterPreconditions();
+        
+        identifierStrategy = Constraint.isNotNull(strategy, "IdentifierGenerationStrategy cannot be null");
+    }
+    
+    /**
+     * Get the expiration limit for state tokens.
+     * 
+     * @return expiration limit
+     */
+    @Nonnull public Duration getExpiration() {
+        return expiration;
+    }
+    
+    /**
+     * Set the expiration limit for state tokens.
+     * 
+     * <p>Defaults to PT30M.</p>
+     * 
+     * @param exp expiration limit
+     */
+    public void setExpiration(@Nonnull final Duration exp) {
+        checkSetterPreconditions();
+        
+        expiration = Constraint.isNotNull(exp, "Expiration cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (identifierStrategy == null) {
+            throw new ComponentInitializationException("IdentifierGenerationStrategy cannot be null");
+        }
+    }
+
+    /**
+     * Generate a state token.
+     * 
+     * @return a new state token
+     */
+    @Nonnull protected String generateToken() {
+        return identifierStrategy.generateIdentifier();
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/Application.java b/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
index 0a13a70..1d6aa3c 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
@@ -77,6 +77,13 @@ public interface Application extends RelyingPartyConfigurationResolver {
      * @return ordered list of unprefixed flow IDs
      */
     @Nonnull List<String> getSessionInitiators(@Nullable final ProfileRequestContext profileRequestContext);
+
+    /**
+     * Get the {@link StateTokenManager} to use for thie application.
+     * 
+     * @return the manager to use
+     */
+    @Nonnull StateTokenManager getStateTokenManager();
     
     /**
      * Get {@link MetadataResolver} for this {@link Application}.
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/StateTokenManager.java b/sp-server-api/src/main/java/net/shibboleth/sp/StateTokenManager.java
new file mode 100644
index 0000000..e93b420
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/StateTokenManager.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp;
+
+import java.io.IOException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Interface to a service that manages "state" tokens, used in most SSO protocols
+ * to manage stateful request/response correlation and to limit exposure of the
+ * resource URLs accessed by clients to allow recovery of the URL for final redirection.
+ * 
+ * <p>SAML refers to this notion as <em>RelayState</em>, while OpenID Connect just refers to it
+ * as <em>state</em>.</p>
+ * 
+ * <p>There are multiple possible implementations of this concept, some involving cookies.</p>
+ * 
+ * <p>The value type is a byte array to accomodate non-Unicode data from agents.</p>
+ */
+public interface StateTokenManager {
+    
+    /**
+     * Preserves a value by transforming it into a state token.
+     * 
+     * @param agent agent owning the state
+     * @param application application owning the state
+     * @param value input value to preserve
+     * 
+     * @return state token representing value
+     * 
+     * @throws IOException if creation of token fails 
+     */
+    @Nonnull String preserveToStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
+            @Nonnull final byte[] value) throws IOException;
+
+    /**
+     * Recovers a value from a state token.
+     * 
+     * <p>In most implementations, the state token mapping should be cleared on successful use of this
+     * method.</p>
+     * 
+     * @param agent agent owning the state
+     * @param application application owning the state
+     * @param token state token
+     * 
+     * @return the recovered value, or null if unable to recover without underlying cause
+     * 
+     * @throws IOException if recovery from token fails
+     */
+    @Nullable byte[] recoverFromStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
+            @Nonnull final String token) throws IOException;
+
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
index 38bd005..d3a0e5d 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
@@ -41,6 +41,7 @@ import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.FunctionSupport;
 import net.shibboleth.shared.service.ReloadableService;
 import net.shibboleth.sp.Application;
+import net.shibboleth.sp.StateTokenManager;
 
 /**
  * Basic implementation of an {@link Application}.
@@ -56,6 +57,9 @@ public class BasicApplication extends DefaultRelyingPartyConfigurationResolver i
     /** Session initiator list lookup strategy. */
     @Nonnull private Function<ProfileRequestContext,List<String>> sessionInitiatorLookupStrategy;
     
+    /** State token management. */
+    @NonnullAfterInit private StateTokenManager stateTokenManager;
+    
     /** Metadata source. */
     @NonnullAfterInit private ReloadableService<MetadataResolver> metadataResolver;
     
@@ -80,7 +84,9 @@ public class BasicApplication extends DefaultRelyingPartyConfigurationResolver i
     protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
         
-        if (metadataResolver == null) {
+        if (stateTokenManager == null) {
+            throw new ComponentInitializationException("StateTokenManager cannot be null");
+        } else if (metadataResolver == null) {
             throw new ComponentInitializationException("MetadataResolver cannot be null");
         } else if (transcodingRegistry == null) {
             throw new ComponentInitializationException("AttributeTranscoderRegistry cannot be null");
@@ -198,6 +204,24 @@ public class BasicApplication extends DefaultRelyingPartyConfigurationResolver i
         sessionInitiatorLookupStrategy = Constraint.isNotNull(strategy,
                 "Session initiators lookup strategy cannot be null");
     }
+    
+    /** {@inheritDoc} */
+    @Nonnull public StateTokenManager getStateTokenManager() {
+        checkComponentActive();
+        assert stateTokenManager != null;
+        return stateTokenManager;
+    }
+    
+    /**
+     * Set the {@link StateTokenManager} to use.
+     * 
+     * @param manager state token manager
+     */
+    public void setStateTokenManager(@Nonnull final StateTokenManager manager) {
+        checkSetterPreconditions();
+        
+        stateTokenManager = Constraint.isNotNull(manager, "StateTokenManager cannot be null");
+    }
         
     /** {@inheritDoc} */
     @Nonnull public ReloadableService<MetadataResolver> getMetadataResolver() {
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/StorageServiceStateTokenManager.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/StorageServiceStateTokenManager.java
new file mode 100644
index 0000000..8ed36d7
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/StorageServiceStateTokenManager.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.impl;
+
+import java.io.IOException;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.DecodingException;
+import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.AbstractStateTokenManager;
+import net.shibboleth.sp.Agent;
+import net.shibboleth.sp.Application;
+import net.shibboleth.sp.StateTokenManager;
+
+/**
+ * {@link StateTokenManager} implemented with a {@link StorageService}.
+ */
+public class StorageServiceStateTokenManager extends AbstractStateTokenManager {
+    
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(StorageServiceStateTokenManager.class);
+    
+    /** Storage back-end. */
+    @NonnullAfterInit private StorageService storageService;
+    
+    /**
+     * Set {@link StorageService} to use.
+     * 
+     * @param storage storage service
+     */
+    public void setStorageService(@Nonnull final StorageService storage) {
+        checkSetterPreconditions();
+        
+        storageService = Constraint.isNotNull(storage, "StorageService cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (storageService == null) {
+            throw new ComponentInitializationException("StorageService cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull public String preserveToStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
+            @Nonnull final byte[] value) throws IOException {
+        
+        final String context = getContext(agent, application);
+        final String key = generateToken();
+        
+        String encoded;
+        try {
+            encoded = Base64Support.encode(value, false);
+        } catch (final EncodingException e) {
+            throw new IOException(e);
+        }
+        
+        if (storageService.create(context, key, encoded, Instant.now().plus(getExpiration()).toEpochMilli())) {
+            log.trace("Created state token mapping ('{}', '{}') to value '{}'", context, key, encoded);
+            return key;
+        }
+        
+        throw new IOException("Unable to create storage record for state token");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public byte[] recoverFromStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
+            @Nonnull final String token) throws IOException {
+        
+        final String context = getContext(agent, application);
+        final StorageRecord<String> record = storageService.read(context, token);
+        if (record != null) {
+            log.trace("Recovered state token mapping ('{}', '{}') to value '{}'", context, token, record.getValue());
+            try {
+                storageService.delete(context, token);
+            } catch (final IOException e) {
+                log.warn("Unable to delete state token ('{}', '{}') from storage", context, token, e);
+            }
+            try {
+                return Base64Support.decode(record.getValue());
+            } catch (final DecodingException e) {
+                throw new IOException(e);
+            }
+        }
+        
+        return null;
+    }
+    
+    /**
+     * Compute the storage context to use.
+     * 
+     * @param agent calling agent
+     * @param application calling application
+     * 
+     * @return storage context for request
+     */
+    @SuppressWarnings("null")
+    @Nonnull private String getContext(@Nonnull final Agent agent, @Nonnull final Application application) {
+        final StringBuilder builder = new StringBuilder(StorageServiceStateTokenManager.class.getName());
+        builder.append('!').append(agent.getId()).append('!').append(application.getId());
+        return builder.toString();
+    }
+
+}
\ No newline at end of file

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


More information about the commits mailing list