[java-plugin-shibd-saml] branch main updated: Build NameID decoding layer using registry API.

Scott Cantor cantor.2 at osu.edu
Tue Sep 17 18:08:31 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-saml.

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

The following commit(s) were added to refs/heads/main by this push:
     new aca664c  Build NameID decoding layer using registry API.
aca664c is described below

commit aca664cc24bf5e5a7ebf6a5050a3cec184402073
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Sep 17 14:08:24 2024 -0400

    Build NameID decoding layer using registry API.
---
 sp-saml-api/pom.xml                                |   5 +
 .../transcoding/AbstractSAML2NameIDTranscoder.java | 210 +++++++++++++++++++++
 .../nameid/transcoding/SAML2NameIDTranscoder.java  |  41 ++++
 .../saml2/nameid/transcoding/package-info.java     |  18 ++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   8 +
 .../idp/flows/sp/consumer/saml2/saml2-beans.xml    |   2 -
 .../idp/service/attribute/registry/postconfig.xml  |  22 +++
 sp-saml-impl/pom.xml                               |  35 ++++
 .../impl/SAML2ScopedStringNameIDTranscoder.java    | 100 ++++++++++
 .../impl/SAML2StringNameIDTranscoder.java          |  95 ++++++++++
 .../nameid/transcoding/impl/package-info.java      |  18 ++
 .../saml2/profile/impl/ExtractSAMLAttributes.java  | 133 +++++++++----
 .../SAML2ScopedStringNameIDTranscoderTest.java     | 144 ++++++++++++++
 .../impl/SAML2StringNameIDTranscoderTest.java      | 141 ++++++++++++++
 14 files changed, 933 insertions(+), 39 deletions(-)

diff --git a/sp-saml-api/pom.xml b/sp-saml-api/pom.xml
index 6e935e5..f472b00 100644
--- a/sp-saml-api/pom.xml
+++ b/sp-saml-api/pom.xml
@@ -39,6 +39,11 @@
             <artifactId>shib-attribute-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-saml-attribute-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>${shib-metadata.groupId}</groupId>
             <artifactId>shib-metadata-api</artifactId>
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/AbstractSAML2NameIDTranscoder.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/AbstractSAML2NameIDTranscoder.java
new file mode 100644
index 0000000..984acdf
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/AbstractSAML2NameIDTranscoder.java
@@ -0,0 +1,210 @@
+/*
+ * 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.saml.saml2.nameid.transcoding;
+
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.NameIDType;
+
+import com.google.common.base.Strings;
+
+import net.shibboleth.idp.attribute.AttributeDecodingException;
+import net.shibboleth.idp.attribute.AttributeEncodingException;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
+import net.shibboleth.idp.attribute.transcoding.TranscodingRule;
+import net.shibboleth.idp.saml.attribute.transcoding.AbstractSAMLAttributeTranscoder;
+import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+import net.shibboleth.profile.context.navigate.RelyingPartyIdLookupFunction;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Base class for transcoders that operate on a SAML 2 {@link NameID}.
+ * 
+ * <p>For the moment, these are decode only, as the encoding side was already
+ * handled with a dedicated generation service. The registry concept came later.</p>
+ * 
+ * @param <EncodedType> the type of data that can be handled by the transcoder
+ */
+public abstract class AbstractSAML2NameIDTranscoder<EncodedType extends IdPAttributeValue> extends
+        AbstractSAMLAttributeTranscoder<NameID,EncodedType> implements SAML2NameIDTranscoder<EncodedType> {
+    
+    /** Function used to obtain the requester ID. */
+    @Nonnull private Function<ProfileRequestContext,String> serviceProviderNameLookupStrategy;
+
+    /** Function used to obtain the issuer ID. */
+    @Nonnull private Function<ProfileRequestContext,String> identityProviderNameLookupStrategy;
+    
+    /** Constructor. */
+    public AbstractSAML2NameIDTranscoder() {
+        serviceProviderNameLookupStrategy = new IssuerLookupFunction();
+        identityProviderNameLookupStrategy = new RelyingPartyIdLookupFunction();
+    }
+
+    /**
+     * Set the strategy used to locate the name of the service provider in this transaction.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setServiceProviderNameLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        serviceProviderNameLookupStrategy = Constraint.isNotNull(strategy, "SP name lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the name of the identity provider in this transaction.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIdentityProviderNameLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        identityProviderNameLookupStrategy = Constraint.isNotNull(strategy, "IdP name lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Nonnull public Class<NameID> getEncodedType() {
+        return NameID.class;
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable public String getEncodedName(@Nonnull final TranscodingRule rule) {
+        final String format = rule.getOrDefault(PROP_NAME_FORMAT, String.class, NameIDType.UNSPECIFIED);
+        return "SAML2:NameID:" + format;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull protected NameID buildAttribute(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nullable final IdPAttribute attribute, @Nonnull final Class<? extends NameID> to,
+            @Nonnull final TranscodingRule rule, @Nonnull final List<XMLObject> attributeValues)
+                    throws AttributeEncodingException {
+
+        throw new AttributeEncodingException("NameID transcoders do not support encoding");
+    }
+        
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull protected IdPAttribute buildIdPAttribute(
+            @Nullable final ProfileRequestContext profileRequestContext, @Nonnull final NameID nameID,
+            @Nonnull final TranscodingRule rule, @Nonnull final List<IdPAttributeValue> attributeValues)
+                    throws AttributeDecodingException {
+        
+        if (nameID.getValue() != null && attributeValues.isEmpty()) {
+            throw new AttributeDecodingException("Failed to decode value for NameID with Format " + nameID.getFormat());
+        }
+        
+        final String id = rule.get(AttributeTranscoderRegistry.PROP_ID, String.class);
+        if (Strings.isNullOrEmpty(id)) {
+            throw new AttributeDecodingException("Required transcoder property 'id' not found");
+        }
+        assert id != null;
+        
+        final IdPAttribute idpAttribute = new IdPAttribute(id);
+        idpAttribute.setValues(attributeValues);
+        return idpAttribute;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull protected Iterable<XMLObject> getValues(@Nonnull final NameID input) {
+        return input.getValue() != null ? CollectionSupport.singletonList(input) : CollectionSupport.emptyList();
+    }
+    
+    /**
+     * Compute the effective NameQualifier to use based on both the input and the transaction.
+     * 
+     * @param profileRequestContext profile request context
+     * @param input input object
+     * @param useDefaultQualifier true iff the qualifier should be defaulted in if not set
+     * 
+     * @return the effective NameQualifier to use
+     */
+    @Nonnull protected String getNameQualifier(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nonnull final NameID input, final boolean useDefaultQualifier) {
+        
+        if (input.getNameQualifier() instanceof String qual) {
+            return qual;
+        }
+        
+        if (useDefaultQualifier) {
+            final String qual = identityProviderNameLookupStrategy.apply(profileRequestContext);
+            if (qual != null) {
+                return qual;
+            }
+        }
+        
+        return "";
+    }
+
+    /**
+     * Compute the effective SPNameQualifier to use based on both the input and the transaction.
+     * 
+     * @param profileRequestContext profile request context
+     * @param input input object
+     * @param useDefaultQualifier true iff the qualifier should be defaulted in if not set
+     * 
+     * @return the effective NameQualifier to use
+     */
+    @Nonnull protected String getSPNameQualifier(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nonnull final NameID input, final boolean useDefaultQualifier) {
+        
+        if (input.getSPNameQualifier() instanceof String qual) {
+            return qual;
+        }
+        
+        if (useDefaultQualifier) {
+            final String qual = serviceProviderNameLookupStrategy.apply(profileRequestContext);
+            if (qual != null) {
+                return qual;
+            }
+        }
+        
+        return "";
+    }
+
+    /**
+     * A function to produce a "canonical" name for a SAML 2.0 {@link NameID} for transcoding rules.
+     */
+    public static class NamingFunction implements Function<NameID,String> {
+
+        /** {@inheritDoc} */
+        @Nullable public String apply(@Nullable final NameID input) {
+            
+            if (input == null) {
+                return null;
+            }
+        
+            String format = input.getFormat();
+            if (format == null) {
+                format = NameIDType.UNSPECIFIED;
+            }
+            
+            final StringBuilder builder = new StringBuilder();
+            builder.append("SAML2:NameID:").append(format);
+            return builder.toString();
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/SAML2NameIDTranscoder.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/SAML2NameIDTranscoder.java
new file mode 100644
index 0000000..29d7035
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/SAML2NameIDTranscoder.java
@@ -0,0 +1,41 @@
+/*
+ * 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.saml.saml2.nameid.transcoding;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoder;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.metadata.RequestedAttribute;
+
+/**
+ * Marker interface for transcoders that operate on a SAML 2 {@link Attribute} or {@link RequestedAttribute}.
+ * 
+ * @param <EncodedType> the type of data that can be handled by the transcoder
+ */
+public interface SAML2NameIDTranscoder<EncodedType extends IdPAttributeValue> extends
+        AttributeTranscoder<NameID> {
+
+    /** The NameID format. */
+    @Nonnull @NotEmpty static final String PROP_NAME_FORMAT = "saml2.nameFormat";
+
+    /** Whether to default in missing qualifiers from the active transaction. */
+    @Nonnull @NotEmpty static final String PROP_DEFAULT_QUALIFIERS = "saml2.defaultQualifiers";
+
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/package-info.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/package-info.java
new file mode 100644
index 0000000..ec63d7a
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * API classes for SAML 2.0 NameID decoding classes.
+ */
+package net.shibboleth.sp.saml.saml2.nameid.transcoding;
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index b62b312..1a0e10e 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -43,4 +43,12 @@
         </property>
     </bean>
 
+    <!-- Necessary for NameID decoding to function. -->
+    
+    <bean parent="shibboleth.RegistryNamingFunction" c:claz="org.opensaml.saml.saml2.core.NameID">
+        <constructor-arg name="function">
+            <bean class="net.shibboleth.sp.saml.saml2.nameid.transcoding.AbstractSAML2NameIDTranscoder.NamingFunction" />
+        </constructor-arg>    
+    </bean>
+
 </beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml
index f79800b..04c1f62 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml
@@ -177,8 +177,6 @@
 
     <bean id="ExtractSAMLAttributes"
         class="net.shibboleth.sp.saml.saml2.profile.impl.ExtractSAMLAttributes" scope="prototype"
-        p:responderLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple"
-        p:requesterLookupStrategy-ref="shibboleth.IssuerLookup.Simple"
         p:standardExtractionStrategy-ref="StandardExtractionStrategy" />
 
 </beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/service/attribute/registry/postconfig.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/service/attribute/registry/postconfig.xml
new file mode 100644
index 0000000..db4c846
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/service/attribute/registry/postconfig.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+    xmlns:context="http://www.springframework.org/schema/context"
+    xmlns:util="http://www.springframework.org/schema/util"
+    xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <!-- NameID decoders. -->
+
+    <bean id="SAML2StringNameIDTranscoder"
+        class="net.shibboleth.sp.saml.saml2.nameid.transcoding.impl.SAML2StringNameIDTranscoder" />
+
+    <bean id="SAML2ScopedStringNameIDTranscoder"
+        class="net.shibboleth.sp.saml.saml2.nameid.transcoding.impl.SAML2ScopedStringNameIDTranscoder" />
+    
+</beans>
diff --git a/sp-saml-impl/pom.xml b/sp-saml-impl/pom.xml
index 63b4b34..69510b0 100644
--- a/sp-saml-impl/pom.xml
+++ b/sp-saml-impl/pom.xml
@@ -61,6 +61,11 @@
             <artifactId>shib-attribute-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-saml-attribute-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>${shib-attribute.groupId}</groupId>
             <artifactId>shib-attribute-filter-api</artifactId>
@@ -91,6 +96,36 @@
             <artifactId>idp-testing</artifactId>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-attribute-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-core-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-saml-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/SAML2ScopedStringNameIDTranscoder.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/SAML2ScopedStringNameIDTranscoder.java
new file mode 100644
index 0000000..e69faae
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/SAML2ScopedStringNameIDTranscoder.java
@@ -0,0 +1,100 @@
+/*
+ * 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.saml.saml2.nameid.transcoding.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.NameIDType;
+
+import net.shibboleth.idp.attribute.AttributeEncodingException;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.ScopedStringAttributeValue;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoder;
+import net.shibboleth.idp.attribute.transcoding.TranscodingRule;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.sp.saml.saml2.nameid.transcoding.AbstractSAML2NameIDTranscoder;
+
+/**
+ * {@link AttributeTranscoder} that supports {@link NameID} and {@link ScopedStringAttributeValue} objects.
+ * 
+ * <p>The scope is defined to be the NameQualifier or the issuing entityID and must exist.</p>
+ */
+public class SAML2ScopedStringNameIDTranscoder extends AbstractSAML2NameIDTranscoder<ScopedStringAttributeValue> {
+
+    /** The decoding template. */
+    @Nonnull @NotEmpty public static final String PROP_TEMPLATE = "saml2.valueTemplate";
+
+    /** The default template. */
+    @Nonnull @NotEmpty public static final String DEFAULT_TEMPLATE = "$Name";
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean canEncodeValue(@Nonnull final IdPAttribute attribute, @Nonnull final IdPAttributeValue value) {
+        return value instanceof ScopedStringAttributeValue;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected XMLObject encodeValue(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nonnull final IdPAttribute attribute, @Nonnull final TranscodingRule rule,
+            @Nonnull final ScopedStringAttributeValue value) throws AttributeEncodingException {
+
+        throw new AttributeEncodingException("NameID transcoders do not support encoding");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected IdPAttributeValue decodeValue(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nonnull final NameID nameID, @Nonnull final TranscodingRule rule, @Nullable final XMLObject value) {
+
+        final StringBuilder builder =
+                new StringBuilder(rule.getOrDefault(PROP_TEMPLATE, String.class, DEFAULT_TEMPLATE));
+        
+        
+        final Boolean flag = rule.getOrDefault(PROP_DEFAULT_QUALIFIERS, Boolean.class, false);
+        assert flag != null;
+        
+        int i = builder.indexOf("$Format");
+        if (i >= 0) {
+            final String format = nameID.getFormat();
+            builder.replace(i, i + 7, format != null ? format : NameIDType.UNSPECIFIED);
+        }
+        
+        i = builder.indexOf("$SPNameQualifier");
+        if (i >= 0) {
+            builder.replace(i, i + 16, getSPNameQualifier(profileRequestContext, nameID, flag));
+        }
+
+        i = builder.indexOf("$NameQualifier");
+        if (i >= 0) {
+            builder.replace(i, i + 14, getNameQualifier(profileRequestContext, nameID, flag));
+        }
+
+        i = builder.indexOf("$Name");
+        if (i >= 0) {
+            final String val = nameID.getValue();
+            builder.replace(i, i + 5, val != null ? val : "");
+        }
+        
+        return new ScopedStringAttributeValue(builder.toString(),
+                getNameQualifier(profileRequestContext, nameID, true));
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/SAML2StringNameIDTranscoder.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/SAML2StringNameIDTranscoder.java
new file mode 100644
index 0000000..34869c2
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/SAML2StringNameIDTranscoder.java
@@ -0,0 +1,95 @@
+/*
+ * 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.saml.saml2.nameid.transcoding.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.NameIDType;
+
+import net.shibboleth.idp.attribute.AttributeEncodingException;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoder;
+import net.shibboleth.idp.attribute.transcoding.TranscodingRule;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.sp.saml.saml2.nameid.transcoding.AbstractSAML2NameIDTranscoder;
+
+/**
+ * {@link AttributeTranscoder} that supports {@link NameID} and {@link StringAttributeValue} objects.
+ */
+public class SAML2StringNameIDTranscoder extends AbstractSAML2NameIDTranscoder<StringAttributeValue> {
+
+    /** The decoding template. */
+    @Nonnull @NotEmpty public static final String PROP_TEMPLATE = "saml2.valueTemplate";
+
+    /** The default template. */
+    @Nonnull @NotEmpty public static final String DEFAULT_TEMPLATE = "$Name!!$NameQualifier!!$SPNameQualifier";
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean canEncodeValue(@Nonnull final IdPAttribute attribute, @Nonnull final IdPAttributeValue value) {
+        return value instanceof StringAttributeValue;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected XMLObject encodeValue(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nonnull final IdPAttribute attribute, @Nonnull final TranscodingRule rule,
+            @Nonnull final StringAttributeValue value) throws AttributeEncodingException {
+
+        throw new AttributeEncodingException("NameID transcoders do not support encoding");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected IdPAttributeValue decodeValue(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nonnull final NameID nameID, @Nonnull final TranscodingRule rule, @Nullable final XMLObject value) {
+
+        final StringBuilder builder = new StringBuilder(rule.getOrDefault(PROP_TEMPLATE, String.class, DEFAULT_TEMPLATE));
+        
+        final Boolean flag = rule.getOrDefault(PROP_DEFAULT_QUALIFIERS, Boolean.class, false);
+        assert flag != null;
+        
+        int i = builder.indexOf("$Format");
+        if (i >= 0) {
+            final String format = nameID.getFormat();
+            builder.replace(i, i + 7, format != null ? format : NameIDType.UNSPECIFIED);
+        }
+        
+        i = builder.indexOf("$SPNameQualifier");
+        if (i >= 0) {
+            builder.replace(i, i + 16, getSPNameQualifier(profileRequestContext, nameID, flag));
+        }
+
+        i = builder.indexOf("$NameQualifier");
+        if (i >= 0) {
+            builder.replace(i, i + 14, getNameQualifier(profileRequestContext, nameID, flag));
+        }
+
+        i = builder.indexOf("$Name");
+        if (i >= 0) {
+            final String val = nameID.getValue();
+            builder.replace(i, i + 5, val != null ? val : "");
+        }
+        
+        return StringAttributeValue.valueOf(builder.toString());
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/package-info.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/package-info.java
new file mode 100644
index 0000000..96e9c6f
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * SAML 2.0 NameID transcoder implementations.
+ */
+package net.shibboleth.sp.saml.saml2.nameid.transcoding.impl;
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
index 930168c..d56f21c 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
@@ -14,6 +14,7 @@
 
 package net.shibboleth.sp.saml.saml2.profile.impl;
 
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.function.Function;
@@ -39,8 +40,11 @@ import org.opensaml.saml.saml2.core.AuthnContextDeclRef;
 import org.opensaml.saml.saml2.core.AuthnStatement;
 import org.opensaml.saml.saml2.core.Conditions;
 import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.NameIDType;
 import org.opensaml.saml.saml2.core.Response;
 import org.opensaml.saml.saml2.core.StatusResponseType;
+import org.opensaml.saml.saml2.core.Subject;
 import org.opensaml.saml.saml2.core.SubjectLocality;
 import org.slf4j.Logger;
 
@@ -131,8 +135,10 @@ public class ExtractSAMLAttributes extends AbstractApplicationAction {
         relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
         samlTokenContextLookupStrategy = new ChildContextLookup<>(SAMLTokenContext.class);
 
-        requesterLookupStrategy = new RelyingPartyIdLookupFunction();
-        issuerLookupStrategy = new IssuerLookupFunction();
+        // These appear reversed because we're referring to the attribute issuer and requester,
+        // which is the inverse of the usual assignment of these labels on an inbound assertion.
+        requesterLookupStrategy = new IssuerLookupFunction();
+        issuerLookupStrategy = new RelyingPartyIdLookupFunction();
 
         // PRC -> SAMLTokenContext -> AttributeContext
         attributeContextCreationStrategy = new ChildContextLookup<>(AttributeContext.class, true).compose( 
@@ -164,7 +170,7 @@ public class ExtractSAMLAttributes extends AbstractApplicationAction {
     }
 
     /**
-     * Set the strategy used to locate the requester ID for filtering.
+     * Set the strategy used to locate the attribute requester ID for filtering.
      * 
      * @param strategy lookup strategy
      */
@@ -174,7 +180,7 @@ public class ExtractSAMLAttributes extends AbstractApplicationAction {
     }
 
     /**
-     * Set the strategy used to locate the issuer ID for filtering.
+     * Set the strategy used to locate the attribute issuer ID for filtering.
      * 
      * @param strategy lookup strategy
      */
@@ -253,12 +259,11 @@ public class ExtractSAMLAttributes extends AbstractApplicationAction {
         
         processAttributes(profileRequestContext);
         
-        // TODO: NameID handling
-        
         // Extract standard attributes.
         if (standardExtractionStrategy != null &&
                 profileConfiguration.isExtractStandardAttributes(profileRequestContext)) {
             log.debug("{} Extracting standard attributes", getLogPrefix());
+            assert standardExtractionStrategy != null;
             final Collection<IdPAttribute> standards = standardExtractionStrategy.apply(samlTokenContext);
             if (standards != null && !standards.isEmpty()) {
                 if (log.isDebugEnabled()) {
@@ -301,13 +306,13 @@ public class ExtractSAMLAttributes extends AbstractApplicationAction {
     }
     
     /**
-     * Process the inbound SAML Attributes.
+     * Process the inbound SAML Attributes and NameID.
      * 
      * @param profileRequestContext current profile request context
      */
     private void processAttributes(@Nonnull final ProfileRequestContext profileRequestContext) {
         
-        log.debug("{} Decoding incoming SAML Attributes", getLogPrefix());
+        log.debug("{} Decoding incoming SAML Attributes and NameID", getLogPrefix());
         
         final Multimap<String,IdPAttribute> mapped = HashMultimap.create();
         assert mapped != null;
@@ -316,6 +321,17 @@ public class ExtractSAMLAttributes extends AbstractApplicationAction {
                 ensureApplication().getAttributeTranscoderRegistry().getServiceableComponent()) {
             final Response response = getResponse(profileRequestContext);
             for (final Assertion assertion : response.getAssertions()) {
+                
+                final Subject subject = assertion.getSubject();
+                final NameID nameID = subject != null ? subject.getNameID() : null;
+                if (nameID != null) {
+                    try {
+                        decodeNameID(component.getComponent(), profileRequestContext, nameID, mapped);
+                    } catch (final AttributeDecodingException e) {
+                        log.error("{} Error decoding inbound NameID", getLogPrefix(), e);
+                    }
+                }
+                
                 for (final AttributeStatement statement : assertion.getAttributeStatements()) {
                     for (final Attribute designator : statement.getAttributes()) {
                         assert designator!=null;
@@ -371,7 +387,38 @@ public class ExtractSAMLAttributes extends AbstractApplicationAction {
             }
         }
     }
-    
+
+    /**
+     * Access the registry of transcoding rules to decode the input {@link NameID}.
+     * 
+     * @param registry  registry of transcoding rules
+     * @param profileRequestContext current profile request context
+     * @param input input object
+     * @param results collection to add results to
+     * 
+     * @throws AttributeDecodingException if an error occurs or no results were obtained
+     */
+    private void decodeNameID(@Nonnull final AttributeTranscoderRegistry registry,
+            @Nonnull final ProfileRequestContext profileRequestContext, @Nonnull final NameID input,
+            @Nonnull @Live final Multimap<String,IdPAttribute> results) throws AttributeDecodingException {
+        
+        final Collection<TranscodingRule> transcodingRules = registry.getTranscodingRules(input);
+        if (transcodingRules.isEmpty()) {
+            log.info("{} No transcoding rule for NameID (Format: '{}')", getLogPrefix(),
+                    input.getFormat() != null ? input.getFormat() : NameIDType.UNSPECIFIED);
+            return;
+        }
+        
+        for (final TranscodingRule rules : transcodingRules) {
+            assert rules != null;
+            final AttributeTranscoder<NameID> transcoder = TranscoderSupport.getTranscoder(rules);
+            final IdPAttribute decodedAttribute = transcoder.decode(profileRequestContext, input, rules);
+            if (decodedAttribute != null) {
+                results.put(decodedAttribute.getId(), decodedAttribute);
+            }
+        }
+    }
+
     /**
      * Check for inbound attributes and apply filtering.
      * 
@@ -586,51 +633,56 @@ public class ExtractSAMLAttributes extends AbstractApplicationAction {
             
             if (issuerAttributeId != null) {
                 final Issuer issuer = assertion.getIssuer();
-                if (issuer != null && issuer.getValue() != null) {
+                if (issuer != null && issuer.getValue() instanceof String s) {
+                    assert issuerAttributeId != null;
                     final IdPAttribute attr = new IdPAttribute(issuerAttributeId);
-                    attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(issuer.getValue())));
+                    attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(s)));
                     attributes.add(attr);
                 }
             }
             
-            if (authnInstantAttributeId != null && statement.getAuthnInstant() != null) {
+            if (authnInstantAttributeId != null && statement.getAuthnInstant() instanceof Instant ts) {
+                assert authnInstantAttributeId != null;
                 final IdPAttribute attr = new IdPAttribute(authnInstantAttributeId);
-                attr.setValues(CollectionSupport.singletonList(new DateTimeAttributeValue(statement.getAuthnInstant())));
+                attr.setValues(CollectionSupport.singletonList(new DateTimeAttributeValue(ts)));
                 attributes.add(attr);
             }
 
             final AuthnContext ac = statement.getAuthnContext();
             
             final AuthnContextClassRef classRef = ac != null ? ac.getAuthnContextClassRef() : null;
-            if (authnContextClassRefAttributeId != null && classRef != null && classRef.getURI() != null) {
+            if (authnContextClassRefAttributeId != null && classRef != null && classRef.getURI() instanceof String s) {
+                assert authnContextClassRefAttributeId != null;
                 final IdPAttribute attr = new IdPAttribute(authnContextClassRefAttributeId);
-                attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(classRef.getURI())));
+                attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(s)));
                 attributes.add(attr);
             }
             
             final AuthnContextDeclRef declRef = ac != null ? ac.getAuthnContextDeclRef() : null;
-            if (authnContextDeclRefAttributeId != null && declRef != null && declRef.getURI() != null) {
+            if (authnContextDeclRefAttributeId != null && declRef != null && declRef.getURI() instanceof String s) {
+                assert authnContextDeclRefAttributeId != null;
                 final IdPAttribute attr = new IdPAttribute(authnContextDeclRefAttributeId);
-                attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(declRef.getURI())));
+                attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(s)));
                 attributes.add(attr);
             }
 
-            if (sessionIndexAttributeId != null && statement.getSessionIndex() != null) {
+            if (sessionIndexAttributeId != null && statement.getSessionIndex() instanceof String s) {
+                assert sessionIndexAttributeId != null;
                 final IdPAttribute attr = new IdPAttribute(sessionIndexAttributeId);
-                attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(statement.getSessionIndex())));
+                attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(s)));
                 attributes.add(attr);
             }
             
-            if (consentAttributeId != null) {
-                final String consent = ((StatusResponseType) assertion.getParent()).getConsent();
-                if (consent != null) {
-                    final IdPAttribute attr = new IdPAttribute(consentAttributeId);
-                    attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(consent)));
-                    attributes.add(attr);
-                }
+            if (consentAttributeId != null && assertion.getParent() instanceof StatusResponseType srt
+                    && srt.getConsent() instanceof String consent) {
+                assert consentAttributeId != null;
+                final IdPAttribute attr = new IdPAttribute(consentAttributeId);
+                attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(consent)));
+                attributes.add(attr);
             }
             
             if (authorityAttributeId != null && ac != null && !ac.getAuthenticatingAuthorities().isEmpty()) {
+                assert authorityAttributeId != null;
                 final IdPAttribute attr = new IdPAttribute(authorityAttributeId);
                 attr.setValues(
                     ac.getAuthenticatingAuthorities().stream()
@@ -643,36 +695,43 @@ public class ExtractSAMLAttributes extends AbstractApplicationAction {
             
             final Conditions conditions = assertion.getConditions();
             
-            if (notBeforeAttributeId != null && conditions != null && conditions.getNotBefore() != null) {
+            if (notBeforeAttributeId != null && conditions != null
+                    && conditions.getNotBefore() instanceof Instant ts) {
+                assert notBeforeAttributeId != null;
                 final IdPAttribute attr = new IdPAttribute(notBeforeAttributeId);
-                attr.setValues(CollectionSupport.singletonList(new DateTimeAttributeValue(conditions.getNotBefore())));
+                attr.setValues(CollectionSupport.singletonList(new DateTimeAttributeValue(ts)));
                 attributes.add(attr);
             }
 
-            if (notOnOrAfterAttributeId != null && conditions != null && conditions.getNotOnOrAfter() != null) {
+            if (notOnOrAfterAttributeId != null && conditions != null
+                    && conditions.getNotOnOrAfter() instanceof Instant ts) {
+                assert notOnOrAfterAttributeId != null;
                 final IdPAttribute attr = new IdPAttribute(notOnOrAfterAttributeId);
-                attr.setValues(CollectionSupport.singletonList(new DateTimeAttributeValue(conditions.getNotOnOrAfter())));
+                attr.setValues(CollectionSupport.singletonList(new DateTimeAttributeValue(ts)));
                 attributes.add(attr);
             }
             
-            if (sessionNotOnOrAfterAttributeId != null && statement.getSessionNotOnOrAfter() != null) {
+            if (sessionNotOnOrAfterAttributeId != null
+                    && statement.getSessionNotOnOrAfter() instanceof Instant ts) {
+                assert sessionNotOnOrAfterAttributeId != null;
                 final IdPAttribute attr = new IdPAttribute(sessionNotOnOrAfterAttributeId);
-                attr.setValues(CollectionSupport.singletonList(
-                        new DateTimeAttributeValue(statement.getSessionNotOnOrAfter())));
+                attr.setValues(CollectionSupport.singletonList(new DateTimeAttributeValue(ts)));
                 attributes.add(attr);
             }
             
             final SubjectLocality locality = statement.getSubjectLocality();
             
-            if (addressAttributeId != null && locality != null && locality.getAddress() != null) {
+            if (addressAttributeId != null && locality != null && locality.getAddress() instanceof String s) {
+                assert addressAttributeId != null;
                 final IdPAttribute attr = new IdPAttribute(addressAttributeId);
-                attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(locality.getAddress())));
+                attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(s)));
                 attributes.add(attr);
             }
      
-            if (dnsNameAttributeId != null && locality != null && locality.getDNSName() != null) {
+            if (dnsNameAttributeId != null && locality != null && locality.getDNSName() instanceof String s) {
+                assert dnsNameAttributeId != null;
                 final IdPAttribute attr = new IdPAttribute(dnsNameAttributeId);
-                attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(locality.getDNSName())));
+                attr.setValues(CollectionSupport.singletonList(new StringAttributeValue(s)));
                 attributes.add(attr);
             }
             
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/SAML2ScopedStringNameIDTranscoderTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/SAML2ScopedStringNameIDTranscoderTest.java
new file mode 100644
index 0000000..cba604a
--- /dev/null
+++ b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/SAML2ScopedStringNameIDTranscoderTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.saml.saml2.nameid.transcoding.impl;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.core.testing.OpenSAMLInitBaseTestCase;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.saml.common.SAMLObjectBuilder;
+import org.opensaml.saml.saml2.core.NameID;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.attribute.AttributeEncodingException;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.ScopedStringAttributeValue;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
+import net.shibboleth.idp.attribute.transcoding.BasicNamingFunction;
+import net.shibboleth.idp.attribute.transcoding.TranscoderSupport;
+import net.shibboleth.idp.attribute.transcoding.TranscodingRule;
+import net.shibboleth.idp.attribute.transcoding.impl.AttributeTranscoderRegistryImpl;
+import net.shibboleth.idp.saml.attribute.transcoding.SAML2AttributeTranscoder;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.testing.MockApplicationContext;
+import net.shibboleth.sp.saml.saml2.nameid.transcoding.AbstractSAML2NameIDTranscoder;
+
+/** {@link SAML2ScopedStringNameIDTranscoder} unit test. */
+ at SuppressWarnings("javadoc")
+public class SAML2ScopedStringNameIDTranscoderTest extends OpenSAMLInitBaseTestCase {
+
+    private AttributeTranscoderRegistryImpl registry;
+
+    private SAMLObjectBuilder<NameID> nameIDBuilder;
+
+    @Nonnull @NotEmpty private final static String ID = "NameIDAttribute";
+    @Nonnull @NotEmpty private final static String NAMEID_FORMAT = "Format";
+    @Nonnull @NotEmpty private final static String STRING_1 = "Value The First";
+    @Nonnull @NotEmpty private final static String STRING_2 = "Second string the value is";
+
+    @BeforeClass public void setUp() throws ComponentInitializationException {
+        nameIDBuilder = (SAMLObjectBuilder<NameID>)
+                XMLObjectProviderRegistrySupport.getBuilderFactory().<NameID>ensureBuilder(
+                        NameID.DEFAULT_ELEMENT_NAME);
+        
+        registry = new AttributeTranscoderRegistryImpl();
+        registry.setId("test");
+
+        final SAML2ScopedStringNameIDTranscoder transcoder = new SAML2ScopedStringNameIDTranscoder();
+        transcoder.initialize();
+        
+        registry.setNamingRegistry(CollectionSupport.singletonList(
+                new BasicNamingFunction<>(transcoder.getEncodedType(), new AbstractSAML2NameIDTranscoder.NamingFunction())));
+        
+        final Map<String,Object> ruleset1 = new HashMap<>();
+        ruleset1.put(AttributeTranscoderRegistry.PROP_ID, ID);
+        ruleset1.put(AttributeTranscoderRegistry.PROP_TRANSCODER, transcoder);
+        ruleset1.put(SAML2AttributeTranscoder.PROP_NAME_FORMAT, NAMEID_FORMAT);
+        ruleset1.put(SAML2StringNameIDTranscoder.PROP_TEMPLATE, "$Name");
+        
+        registry.setTranscoderRegistry(CollectionSupport.singletonList(new TranscodingRule(ruleset1)));
+        registry.setApplicationContext(new MockApplicationContext());
+        registry.initialize();
+    }
+    
+    @AfterClass public void tearDown() {
+        registry.destroy();
+        registry = null;
+    }
+
+    @Test(expectedExceptions=AttributeEncodingException.class)
+    public void failedEncode() throws Exception {
+        final IdPAttribute inputAttribute = new IdPAttribute(ID);
+
+        final Collection<TranscodingRule> rulesets = registry.getTranscodingRules(inputAttribute, NameID.class);
+        Assert.assertEquals(rulesets.size(), 1);
+        final TranscodingRule ruleset = rulesets.iterator().next();
+        assert ruleset != null;
+        
+        TranscoderSupport.<NameID>getTranscoder(ruleset).encode(null, inputAttribute, NameID.class, ruleset);
+    }
+
+    @Test public void emptyDecode() throws Exception {
+        
+        final NameID nameID = nameIDBuilder.buildObject();
+        Collection<TranscodingRule> rulesets = registry.getTranscodingRules(nameID);
+        Assert.assertEquals(rulesets.size(), 0);
+        
+        nameID.setFormat(NAMEID_FORMAT);
+
+        rulesets = registry.getTranscodingRules(nameID);
+        Assert.assertEquals(rulesets.size(), 1);
+        final TranscodingRule ruleset = rulesets.iterator().next();
+        assert ruleset != null;
+        
+        final IdPAttribute attr = TranscoderSupport.<NameID>getTranscoder(ruleset).decode(null, nameID, ruleset);
+        assert attr != null;
+        Assert.assertEquals(attr.getId(), ID);
+        Assert.assertTrue(attr.getValues().isEmpty());
+    }
+    
+    @Test public void decode() throws Exception {
+                
+        final NameID nameID = nameIDBuilder.buildObject();
+        nameID.setFormat(NAMEID_FORMAT);
+        nameID.setNameQualifier(STRING_2);
+        nameID.setValue(STRING_1);
+
+        final Collection<TranscodingRule> rulesets = registry.getTranscodingRules(nameID);
+        Assert.assertEquals(rulesets.size(), 1);
+        final TranscodingRule ruleset = rulesets.iterator().next();
+        assert ruleset != null;
+        
+        final IdPAttribute attr = TranscoderSupport.<NameID>getTranscoder(ruleset).decode(null, nameID, ruleset);
+        
+        assert attr != null;
+        Assert.assertEquals(attr.getId(), ID);
+        Assert.assertEquals(attr.getValues().size(), 1);
+        
+        final ScopedStringAttributeValue value = (ScopedStringAttributeValue) attr.getValues().get(0);
+        Assert.assertEquals(value.getValue(), STRING_1);
+        Assert.assertEquals(value.getScope(), STRING_2);
+    }
+
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/SAML2StringNameIDTranscoderTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/SAML2StringNameIDTranscoderTest.java
new file mode 100644
index 0000000..5f0c784
--- /dev/null
+++ b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/nameid/transcoding/impl/SAML2StringNameIDTranscoderTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.saml.saml2.nameid.transcoding.impl;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.core.testing.OpenSAMLInitBaseTestCase;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.saml.common.SAMLObjectBuilder;
+import org.opensaml.saml.saml2.core.NameID;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.attribute.AttributeEncodingException;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
+import net.shibboleth.idp.attribute.transcoding.BasicNamingFunction;
+import net.shibboleth.idp.attribute.transcoding.TranscoderSupport;
+import net.shibboleth.idp.attribute.transcoding.TranscodingRule;
+import net.shibboleth.idp.attribute.transcoding.impl.AttributeTranscoderRegistryImpl;
+import net.shibboleth.idp.saml.attribute.transcoding.SAML2AttributeTranscoder;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.testing.MockApplicationContext;
+import net.shibboleth.sp.saml.saml2.nameid.transcoding.AbstractSAML2NameIDTranscoder;
+
+/** {@link SAML2StringNameIDTranscoder} unit test. */
+ at SuppressWarnings("javadoc")
+public class SAML2StringNameIDTranscoderTest extends OpenSAMLInitBaseTestCase {
+
+    private AttributeTranscoderRegistryImpl registry;
+
+    private SAMLObjectBuilder<NameID> nameIDBuilder;
+
+    @Nonnull @NotEmpty private final static String ID = "NameIDAttribute";
+    @Nonnull @NotEmpty private final static String NAMEID_FORMAT = "Format";
+    @Nonnull @NotEmpty private final static String STRING_1 = "Value The First";
+    @Nonnull @NotEmpty private final static String STRING_2 = "Second string the value is";
+
+    @BeforeClass public void setUp() throws ComponentInitializationException {
+        nameIDBuilder = (SAMLObjectBuilder<NameID>)
+                XMLObjectProviderRegistrySupport.getBuilderFactory().<NameID>ensureBuilder(
+                        NameID.DEFAULT_ELEMENT_NAME);
+        
+        registry = new AttributeTranscoderRegistryImpl();
+        registry.setId("test");
+
+        final SAML2StringNameIDTranscoder transcoder = new SAML2StringNameIDTranscoder();
+        transcoder.initialize();
+        
+        registry.setNamingRegistry(CollectionSupport.singletonList(
+                new BasicNamingFunction<>(transcoder.getEncodedType(), new AbstractSAML2NameIDTranscoder.NamingFunction())));
+        
+        final Map<String,Object> ruleset1 = new HashMap<>();
+        ruleset1.put(AttributeTranscoderRegistry.PROP_ID, ID);
+        ruleset1.put(AttributeTranscoderRegistry.PROP_TRANSCODER, transcoder);
+        ruleset1.put(SAML2AttributeTranscoder.PROP_NAME_FORMAT, NAMEID_FORMAT);
+        ruleset1.put(SAML2StringNameIDTranscoder.PROP_TEMPLATE, "$Name!!$NameQualifier");
+        
+        registry.setTranscoderRegistry(CollectionSupport.singletonList(new TranscodingRule(ruleset1)));
+        registry.setApplicationContext(new MockApplicationContext());
+        registry.initialize();
+    }
+    
+    @AfterClass public void tearDown() {
+        registry.destroy();
+        registry = null;
+    }
+
+    @Test(expectedExceptions=AttributeEncodingException.class)
+    public void failedEncode() throws Exception {
+        final IdPAttribute inputAttribute = new IdPAttribute(ID);
+
+        final Collection<TranscodingRule> rulesets = registry.getTranscodingRules(inputAttribute, NameID.class);
+        Assert.assertEquals(rulesets.size(), 1);
+        final TranscodingRule ruleset = rulesets.iterator().next();
+        assert ruleset != null;
+        
+        TranscoderSupport.<NameID>getTranscoder(ruleset).encode(null, inputAttribute, NameID.class, ruleset);
+    }
+
+    @Test public void emptyDecode() throws Exception {
+        
+        final NameID nameID = nameIDBuilder.buildObject();
+        Collection<TranscodingRule> rulesets = registry.getTranscodingRules(nameID);
+        Assert.assertEquals(rulesets.size(), 0);
+        
+        nameID.setFormat(NAMEID_FORMAT);
+
+        rulesets = registry.getTranscodingRules(nameID);
+        Assert.assertEquals(rulesets.size(), 1);
+        final TranscodingRule ruleset = rulesets.iterator().next();
+        assert ruleset != null;
+        
+        final IdPAttribute attr = TranscoderSupport.<NameID>getTranscoder(ruleset).decode(null, nameID, ruleset);
+        assert attr != null;
+        Assert.assertEquals(attr.getId(), ID);
+        Assert.assertTrue(attr.getValues().isEmpty());
+    }
+    
+    @Test public void decode() throws Exception {
+                
+        final NameID nameID = nameIDBuilder.buildObject();
+        nameID.setFormat(NAMEID_FORMAT);
+        nameID.setNameQualifier(STRING_2);
+        nameID.setValue(STRING_1);
+
+        final Collection<TranscodingRule> rulesets = registry.getTranscodingRules(nameID);
+        Assert.assertEquals(rulesets.size(), 1);
+        final TranscodingRule ruleset = rulesets.iterator().next();
+        assert ruleset != null;
+        
+        final IdPAttribute attr = TranscoderSupport.<NameID>getTranscoder(ruleset).decode(null, nameID, ruleset);
+        
+        assert attr != null;
+        Assert.assertEquals(attr.getId(), ID);
+        Assert.assertEquals(attr.getValues().size(), 1);
+        Assert.assertEquals(((StringAttributeValue)attr.getValues().get(0)).getValue().toString(), STRING_1 + "!!" + STRING_2);
+    }
+
+}
\ 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