[java-idp-oidc] branch main updated: JOIDC-155 - Support non-URI client_id values used as resource indicators

Henri Mikkonen henri.mikkonen at iki.fi
Thu Jan 4 17:30:48 UTC 2024


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

hjmikkon pushed a commit to branch main
in repository java-idp-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=8b4a4fff1774665ce39f89e2dbfb837f2470bf28

The following commit(s) were added to refs/heads/main by this push:
     new 8b4a4fff JOIDC-155 - Support non-URI client_id values used as resource indicators
8b4a4fff is described below

commit 8b4a4fff1774665ce39f89e2dbfb837f2470bf28
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Jan 4 19:27:37 2024 +0200

    JOIDC-155 - Support non-URI client_id values used as resource indicators
    
    https://shibboleth.atlassian.net/browse/JOIDC-155
    
    Updated authorization request, OIDC authentication request and token request message
    decoders to switch possibly existing 'resource' request parameters into 'shibboleth.oidc.op.resource'.
    The lookup functions (DefaultRequestAudienceLookupFunction and TokenRequestAudienceLookupFunction)
    were updated accordingly to lookup resource parameter values via the custom paramter name.
    
    Also updated the unit and flow tests to cover this.
---
 .../DefaultRequestAudienceLookupFunction.java      |  16 +-
 .../TokenRequestAudienceLookupFunction.java        |  21 +--
 .../impl/OIDCAuthenticationRequestDecoder.java     |   6 +-
 .../op/decoding/impl/OIDCTokenRequestDecoder.java  |   6 +-
 .../decoding/impl/BaseOAuth2RequestDecoder.java    |  37 ++++
 .../impl/OAuth2AuthorizationRequestDecoder.java    |   6 +-
 .../impl/OIDCAuthenticationRequestDecoderTest.java |  84 ++++++---
 .../decoding/impl/OIDCTokenRequestDecoderTest.java |  34 ++++
 .../OAuth2AuthorizationRequestDecoderTest.java     |  99 +++++++++++
 .../oauth2/profile/impl/ValidateAudienceTest.java  |  19 +-
 .../oidc/op/profile/flow/AbstractOidcFlowTest.java |   2 +-
 .../oidc/op/profile/flow/AuthorizeFlowTest.java    | 194 +++++++++++++++++++++
 .../plugin/oidc/op/profile/flow/TokenFlowTest.java | 127 +++++++++++++-
 .../shibboleth/idp/module/conf/relying-party.xml   |   2 +-
 14 files changed, 590 insertions(+), 63 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestAudienceLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestAudienceLookupFunction.java
index d78c1ea1..b70b639c 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestAudienceLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestAudienceLookupFunction.java
@@ -14,11 +14,9 @@
 
 package net.shibboleth.idp.plugin.oidc.op.profile.context.navigate;
 
-import java.net.URI;
 import java.text.ParseException;
 import java.util.Collections;
 import java.util.List;
-import java.util.stream.Collectors;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -31,10 +29,17 @@ import com.nimbusds.oauth2.sdk.AuthorizationRequest;
 /**
  * A function that returns resource value of the authorization request.
  * 
+ * Since 4.1.0, the resource value is fetched via custom parameter name {@link #CUSTOM_RESOURCE_PARAM_NAME}. The
+ * message decoders switches the incoming resource -parameter names into that. The request objects still contain
+ * the standard resource parameter name.
+ * 
  * @since 3.2.0
  */
 public class DefaultRequestAudienceLookupFunction extends AbstractAuthorizationRequestLookupFunction<List<String>> {
 
+    /** Custom name for the resource parameter that allows non-uri values. */
+    public static final String CUSTOM_RESOURCE_PARAM_NAME = "shibboleth.oidc.op.resource";
+
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(DefaultRequestAudienceLookupFunction.class);
 
@@ -57,11 +62,8 @@ public class DefaultRequestAudienceLookupFunction extends AbstractAuthorizationR
             log.error("Unable to parse request object");
             return null;
         }
-        
-        return req.getResources() == null ? null :
-            req.getResources().stream()
-                .map(URI::toString)
-                .collect(Collectors.toUnmodifiableList());
+
+        return req.getCustomParameter(CUSTOM_RESOURCE_PARAM_NAME);
     }
 
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestAudienceLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestAudienceLookupFunction.java
index 487a6935..18c1a63a 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestAudienceLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestAudienceLookupFunction.java
@@ -14,35 +14,26 @@
 
 package net.shibboleth.idp.plugin.oidc.op.profile.context.navigate;
 
-import java.net.URI;
 import java.util.List;
-import java.util.stream.Collectors;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import com.nimbusds.oauth2.sdk.TokenRequest;
 
-import net.shibboleth.shared.annotation.constraint.NonnullElements;
-
 /**
  * A function that returns a copy of requested resource list from a {@link TokenRequest}.
+ * 
+ * Since 4.1.0, the resource value is fetched via custom parameter name
+ * {@link DefaultRequestAudienceLookupFunction#CUSTOM_RESOURCE_PARAM_NAME}. The token request message decoder switches
+ * the incoming resource -parameter names into that.
  */
 public class TokenRequestAudienceLookupFunction extends AbstractTokenRequestLookupFunction<List<String>> {
 
     /** {@inheritDoc} */
     @Override
-    @Nullable @NonnullElements List<String> doLookup(@Nonnull final TokenRequest req) {
-
-        if (req != null) {
-            final List<URI> uris = req.getResources();
-            if (uris != null) {
-                return uris.stream()
-                        .map(URI::toString)
-                        .collect(Collectors.toUnmodifiableList());
-            }
-        }
-        return null;
+    @Nullable List<String> doLookup(@Nonnull final TokenRequest req) {
+        return req.getCustomParameter(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_NAME);
     }
 
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCAuthenticationRequestDecoder.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCAuthenticationRequestDecoder.java
index 15a75fac..6ab68ac1 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCAuthenticationRequestDecoder.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCAuthenticationRequestDecoder.java
@@ -45,7 +45,11 @@ public class OIDCAuthenticationRequestDecoder extends BaseOAuth2RequestDecoder<A
         try {
             final HTTPRequest httpReq = JakartaServletUtils.createHTTPRequest(getHttpServletRequest());
             getProtocolMessageLogger().trace("Inbound request {}", RequestUtil.toString(httpReq));
-            return AuthenticationRequest.parse(httpReq);
+            if (httpReq != null) {
+                switchIntoCustomResource(httpReq);
+                return AuthenticationRequest.parse(httpReq);
+            }
+            throw new MessageDecodingException("Could not create HTTPRequest object from the incoming request");
         } catch (final com.nimbusds.oauth2.sdk.ParseException | IOException e) {
             log.error("Unable to decode inbound request: {}", e.getMessage());
             throw new MessageDecodingException(e);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCTokenRequestDecoder.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCTokenRequestDecoder.java
index 73722611..eae66cea 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCTokenRequestDecoder.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCTokenRequestDecoder.java
@@ -45,7 +45,11 @@ public class OIDCTokenRequestDecoder extends BaseOAuth2RequestDecoder<TokenReque
         try {
             final HTTPRequest httpReq = JakartaServletUtils.createHTTPRequest(getHttpServletRequest());
             getProtocolMessageLogger().trace("Inbound request {}", RequestUtil.toString(httpReq));
-            return TokenRequest.parse(httpReq);
+            if (httpReq != null) {
+                switchIntoCustomResource(httpReq);
+                return TokenRequest.parse(httpReq);
+            }
+            throw new MessageDecodingException("Could not create HTTPRequest object from the incoming request");
         } catch (final ParseException | IOException e) {
             log.error("Unable to decode inbound request: {}", e.getMessage());
             throw new MessageDecodingException(e);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/BaseOAuth2RequestDecoder.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/BaseOAuth2RequestDecoder.java
index 90da6398..ad558511 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/BaseOAuth2RequestDecoder.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/BaseOAuth2RequestDecoder.java
@@ -14,7 +14,11 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oauth2.decoding.impl;
 
+import java.io.UnsupportedEncodingException;
 import java.net.URI;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.Map;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -26,6 +30,9 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.nimbusds.oauth2.sdk.Request;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestAudienceLookupFunction;
 
 /**
  * Base decoder for Nimbus OAuth2 request messages. 
@@ -107,4 +114,34 @@ public abstract class BaseOAuth2RequestDecoder<T extends Request> extends Abstra
             return endpointUri != null ? endpointUri.toString() : null;
         }
     }
+
+    /**
+     * Switches the 'resource' parameter names in the given HTTP request into a custom parameter name. This allows us
+     * to avoid Nimbus library's control over the values of the parameter.
+     * 
+     * @param httpRequest The HTTP request object where the parameter names are switched
+     */
+    protected void switchIntoCustomResource(@Nonnull final HTTPRequest httpRequest) {
+        final Map<String,List<String>> params = httpRequest.getQueryParameters();
+        final String customParameterName = DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_NAME;
+        if (params != null && !params.isEmpty() && params.get("resource") != null) {
+            final List<String> resources = params.get("resource");
+            String query = httpRequest.getQuery();
+            for (final String resource : resources) {
+                String encodedResource;
+                try {
+                    encodedResource = URLEncoder.encode(resource, "UTF-8");
+                } catch (UnsupportedEncodingException e) {
+                    return;
+                }
+                query = query.replace("resource=" + encodedResource, customParameterName + "=" + encodedResource);
+                log.debug("Replaced resource={} with {}={} : {}", encodedResource, customParameterName, encodedResource,
+                        query);
+            }
+            httpRequest.setQuery(query);
+        } else {
+            log.trace("No resource parameter(s) found");
+        }
+    }
+
 }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/OAuth2AuthorizationRequestDecoder.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/OAuth2AuthorizationRequestDecoder.java
index e23947db..31a0c28b 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/OAuth2AuthorizationRequestDecoder.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/OAuth2AuthorizationRequestDecoder.java
@@ -45,7 +45,11 @@ public class OAuth2AuthorizationRequestDecoder extends BaseOAuth2RequestDecoder<
         try {
             final HTTPRequest httpReq = JakartaServletUtils.createHTTPRequest(getHttpServletRequest());
             getProtocolMessageLogger().trace("Inbound request {}", RequestUtil.toString(httpReq));
-            return AuthorizationRequest.parse(httpReq);
+            if (httpReq != null) {
+                switchIntoCustomResource(httpReq);
+                return AuthorizationRequest.parse(httpReq);
+            }
+            throw new MessageDecodingException("Could not create HTTPRequest object from the incoming request");
         } catch (final com.nimbusds.oauth2.sdk.ParseException | IOException e) {
             log.error("Unable to decode inbound request: {}", e.getMessage());
             throw new MessageDecodingException(e);
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCAuthenticationRequestDecoderTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCAuthenticationRequestDecoderTest.java
index 8371a3eb..d2d0d388 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCAuthenticationRequestDecoderTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCAuthenticationRequestDecoderTest.java
@@ -14,6 +14,8 @@
 
 package net.shibboleth.idp.plugin.oidc.op.decoding.impl;
 
+import java.net.URISyntaxException;
+
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.messaging.decoder.MessageDecodingException;
 import org.springframework.mock.web.MockHttpServletRequest;
@@ -25,6 +27,7 @@ import com.nimbusds.oauth2.sdk.ResponseType;
 import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
 
 import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestAudienceLookupFunction;
 import net.shibboleth.shared.primitive.NonnullSupplier;
 
 /**
@@ -32,38 +35,65 @@ import net.shibboleth.shared.primitive.NonnullSupplier;
  */
 public class OIDCAuthenticationRequestDecoderTest {
 
-	private MockHttpServletRequest httpRequest;
-	private OIDCAuthenticationRequestDecoder decoder;
+    private MockHttpServletRequest httpRequest;
+    private OIDCAuthenticationRequestDecoder decoder;
 
-	@BeforeMethod
-	protected void setUp() throws Exception {
-		httpRequest = new MockHttpServletRequest();
-		httpRequest.setMethod("GET");
-		decoder = new OIDCAuthenticationRequestDecoder();
+    @BeforeMethod
+    protected void setUp() throws Exception {
+        httpRequest = new MockHttpServletRequest();
+        httpRequest.setMethod("GET");
+        decoder = new OIDCAuthenticationRequestDecoder();
         decoder.setHttpServletRequestSupplier(new NonnullSupplier<> () {
             public HttpServletRequest get() { return httpRequest;}
-            });
-		decoder.initialize();
-	}
+        });
+        decoder.initialize();
+    }
+
+    @Test
+    public void testRequestDecoding() throws MessageDecodingException {
+        httpRequest
+                .setQueryString("response_type=code&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=openid%20profile&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj");
+        decoder.decode();
+        final MessageContext messageContext = decoder.getMessageContext();
+        // We are not testing nimbus itself here, i.e. we are happy to decode
+        // one parameter successfully
+        Assert.assertEquals(((AuthenticationRequest) messageContext.getMessage()).getResponseType()
+                .toString(), ResponseType.Value.CODE.toString());
+    }
 
     @Test
-	public void testRequestDecoding() throws MessageDecodingException {
-		httpRequest
-				.setQueryString("response_type=code&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=openid%20profile&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj");
-		decoder.decode();
-		final MessageContext messageContext = decoder.getMessageContext();
-		// We are not testing nimbus itself here, i.e. we are happy to decode
-		// one parameter successfully
-		Assert.assertEquals(((AuthenticationRequest) messageContext.getMessage()).getResponseType()
-				.toString(), ResponseType.Value.CODE.toString());
+    public void testRequestDecodingWithURIResource() throws MessageDecodingException, URISyntaxException {
+        httpRequest
+                .setQueryString("response_type=code&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=openid%20profile&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj&resource=https%3A%2F%2Fresource.example.org%2F");
+        decoder.decode();
+        final MessageContext messageContext = decoder.getMessageContext();
+        // We are not testing nimbus itself here, i.e. we are happy to decode
+        // one parameter successfully
+        final AuthenticationRequest request = (AuthenticationRequest) messageContext.getMessage();
+        Assert.assertEquals(request.getResponseType().toString(), ResponseType.Value.CODE.toString());
+        Assert.assertTrue(request.getCustomParameter(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_NAME)
+                .contains("https://resource.example.org/"));
+    }
 
-	}
+    @Test
+    public void testRequestDecodingWithNonURIResource() throws MessageDecodingException {
+        httpRequest
+                .setQueryString("response_type=code&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=openid%20profile&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj&resource=mockResourceId");
+        decoder.decode();
+        final MessageContext messageContext = decoder.getMessageContext();
+        // We are not testing nimbus itself here, i.e. we are happy to decode
+        // one parameter successfully
+        final AuthenticationRequest request = (AuthenticationRequest) messageContext.getMessage();
+        Assert.assertEquals(request.getResponseType().toString(), ResponseType.Value.CODE.toString());
+        Assert.assertTrue(request.getCustomParameter(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_NAME)
+                .contains("mockResourceId"));
+    }
 
-	@Test(expectedExceptions = MessageDecodingException.class)
-	public void testInvalidRequestDecoding() throws MessageDecodingException {
-		// Mandatory response_type parameter removed, decoding should fail
-		httpRequest
-				.setQueryString("client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=openid%20profile&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj");
-		decoder.decode();
-	}
+    @Test(expectedExceptions = MessageDecodingException.class)
+    public void testInvalidRequestDecoding() throws MessageDecodingException {
+        // Mandatory response_type parameter removed, decoding should fail
+        httpRequest
+                .setQueryString("client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=openid%20profile&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj");
+        decoder.decode();
+    }
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCTokenRequestDecoderTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCTokenRequestDecoderTest.java
index c4961bc8..66e6de27 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCTokenRequestDecoderTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCTokenRequestDecoderTest.java
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.oidc.op.decoding.impl;
 
 import java.io.IOException;
+import java.util.List;
 
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.messaging.decoder.MessageDecodingException;
@@ -27,6 +28,7 @@ import com.nimbusds.oauth2.sdk.TokenRequest;
 import com.nimbusds.oauth2.sdk.http.HTTPRequest.Method;
 
 import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestAudienceLookupFunction;
 import net.shibboleth.shared.primitive.NonnullSupplier;
 
 /**
@@ -69,4 +71,36 @@ public class OIDCTokenRequestDecoderTest {
         Assert.assertEquals(message.getAuthorizationGrant().getType().getValue(), "authorization_code");
     }
 
+    @Test
+    public void testRequestDecodingWithUriResource() throws MessageDecodingException, IOException {
+        httpRequest.addHeader("Authorization", "Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW");
+        httpRequest.setContentType("application/x-www-form-urlencoded");
+        httpRequest.addParameter("grant_type", "authorization_code");
+        httpRequest.addParameter("code", "SplxlOBeZQQYbYS6WxSbIA");
+        httpRequest.addParameter("redirect_uri", "https://client.example.org/cb");
+        httpRequest.addParameter("resource", "https://resource.example.org");
+        decoder.decode();
+        final MessageContext messageContext = decoder.getMessageContext();
+        final TokenRequest message = (TokenRequest) messageContext.getMessage();
+        Assert.assertEquals(message.getAuthorizationGrant().getType().getValue(), "authorization_code");
+        Assert.assertEquals(message.getCustomParameter(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_NAME),
+                List.of("https://resource.example.org"));
+    }
+
+    @Test
+    public void testRequestDecodingWithNonUriResource() throws MessageDecodingException, IOException {
+        httpRequest.addHeader("Authorization", "Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW");
+        httpRequest.setContentType("application/x-www-form-urlencoded");
+        httpRequest.addParameter("grant_type", "authorization_code");
+        httpRequest.addParameter("code", "SplxlOBeZQQYbYS6WxSbIA");
+        httpRequest.addParameter("redirect_uri", "https://client.example.org/cb");
+        httpRequest.addParameter("resource", "resource.example.org");
+        decoder.decode();
+        final MessageContext messageContext = decoder.getMessageContext();
+        final TokenRequest message = (TokenRequest) messageContext.getMessage();
+        Assert.assertEquals(message.getAuthorizationGrant().getType().getValue(), "authorization_code");
+        Assert.assertEquals(message.getCustomParameter(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_NAME),
+                List.of("resource.example.org"));
+    }
+
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/OAuth2AuthorizationRequestDecoderTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/OAuth2AuthorizationRequestDecoderTest.java
new file mode 100644
index 00000000..58ffbe5e
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/decoding/impl/OAuth2AuthorizationRequestDecoderTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.idp.plugin.oidc.op.oauth2.decoding.impl;
+
+import java.net.URISyntaxException;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
+import com.nimbusds.oauth2.sdk.ResponseType;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestAudienceLookupFunction;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+
+/**
+ * Unit tests for {@link OAuth2AuthorizationRequestDecoder}.
+ */
+public class OAuth2AuthorizationRequestDecoderTest {
+
+    private MockHttpServletRequest httpRequest;
+    private OAuth2AuthorizationRequestDecoder decoder;
+
+    @BeforeMethod
+    protected void setUp() throws Exception {
+        httpRequest = new MockHttpServletRequest();
+        httpRequest.setMethod("GET");
+        decoder = new OAuth2AuthorizationRequestDecoder();
+        decoder.setHttpServletRequestSupplier(new NonnullSupplier<> () {
+            public HttpServletRequest get() { return httpRequest;}
+        });
+        decoder.initialize();
+    }
+
+    @Test
+    public void testRequestDecoding() throws MessageDecodingException {
+        httpRequest
+                .setQueryString("response_type=code&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=profile&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj");
+        decoder.decode();
+        final MessageContext messageContext = decoder.getMessageContext();
+        // We are not testing nimbus itself here, i.e. we are happy to decode
+        // one parameter successfully
+        Assert.assertEquals(((AuthorizationRequest) messageContext.getMessage()).getResponseType()
+                .toString(), ResponseType.Value.CODE.toString());
+	}
+
+    @Test
+    public void testRequestDecodingWithURIResource() throws MessageDecodingException, URISyntaxException {
+        httpRequest
+                .setQueryString("response_type=code&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=profile&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj&resource=https%3A%2F%2Fresource.example.org%2F");
+        decoder.decode();
+        final MessageContext messageContext = decoder.getMessageContext();
+        // We are not testing nimbus itself here, i.e. we are happy to decode
+        // one parameter successfully
+        final AuthorizationRequest request = (AuthorizationRequest) messageContext.getMessage();
+        Assert.assertEquals(request.getResponseType().toString(), ResponseType.Value.CODE.toString());
+        Assert.assertTrue(request.getCustomParameter(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_NAME)
+                .contains("https://resource.example.org/"));
+    }
+
+    @Test
+    public void testRequestDecodingWithNonURIResource() throws MessageDecodingException {
+        httpRequest
+                .setQueryString("response_type=code&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=profile&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj&resource=mockResourceId");
+        decoder.decode();
+        final MessageContext messageContext = decoder.getMessageContext();
+        // We are not testing nimbus itself here, i.e. we are happy to decode
+        // one parameter successfully
+        final AuthorizationRequest request = (AuthorizationRequest) messageContext.getMessage();
+        Assert.assertEquals(request.getResponseType().toString(), ResponseType.Value.CODE.toString());
+        Assert.assertTrue(request.getCustomParameter(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_NAME)
+                .contains("mockResourceId"));
+    }
+
+    @Test(expectedExceptions = MessageDecodingException.class)
+    public void testInvalidRequestDecoding() throws MessageDecodingException {
+        // Mandatory response_type parameter removed, decoding should fail
+        httpRequest
+                .setQueryString("client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=profile&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj");
+        decoder.decode();
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudienceTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudienceTest.java
index 3e663a46..d09dc44a 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudienceTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudienceTest.java
@@ -18,7 +18,9 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.time.Instant;
 import java.util.List;
+import java.util.Map;
 
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestAudienceLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
@@ -80,8 +82,9 @@ public class ValidateAudienceTest extends BaseOIDCResponseActionTest {
                 new ClientSecretBasic(new ClientID("s6BhdRkqt3"), new Secret("foo")),
                 new ClientCredentialsGrant(),
                 null,
-                List.of(new URI("https://sp3.example.org")),
-                null
+                null,
+                Map.of(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_NAME,
+                        List.of("https://sp3.example.org"))
                 );
         setTokenRequest(req);
         
@@ -103,10 +106,9 @@ public class ValidateAudienceTest extends BaseOIDCResponseActionTest {
                 new ClientSecretBasic(new ClientID("s6BhdRkqt3"), new Secret("foo")),
                 new ClientCredentialsGrant(),
                 null,
-                List.of(new URI("https://sp.example.org"),
-                        new URI("https://sp3.example.org"),
-                        new URI("https://sp2.example.org")),
-                null
+                null,
+                Map.of(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_NAME,
+                        List.of("https://sp.example.org", "https://sp3.example.org", "https://sp2.example.org"))
                 );
         setTokenRequest(req);
         
@@ -153,8 +155,9 @@ public class ValidateAudienceTest extends BaseOIDCResponseActionTest {
                new ClientSecretBasic(new ClientID("s6BhdRkqt3"), new Secret("foo")),
                new AuthorizationCodeGrant(new AuthorizationCode("foo"), new URI("http://localhost")),
                null,
-               List.of(new URI("https://sp.example.org")),
-               null);
+               null,
+               Map.of(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_NAME,
+                       List.of("https://sp3.example.org")));
        setTokenRequest(req);
        
        final AuthorizeCodeClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
index 123e61c4..3ee2a1f9 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
@@ -363,7 +363,7 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
         metadata.setRequestObjectJWEEnc(requestObjectEncMethod);
         if (addResources) {
             metadata.setCustomField("audience", List.of("https://rp.example.org", "https://rp2.example.org",
-                    "https://resource.example.org"));
+                    "https://resource.example.org", "resource.example.org"));
         }
         if (publicKey != null) {
             metadata.setJWKSet(buildJWKSet(publicKey));
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
index cb48bfc0..e250c6de 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
@@ -73,6 +73,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     public static final String FLOW_ID = "oidc/authorize";
     
     String resource = "https://resource.example.org";
+    String resourceNonUri = "resource.example.org";
     String issuer = "https://op.example.org";
     String redirectUri = "https://example.org/cb";
     String clientId = "mockClientId";
@@ -206,6 +207,33 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNull(successResponse.getIssuer());
     }
 
+    @Test
+    public void testWithAuthorizationCodeFlowAndNonUriResource() throws IOException, SessionException {
+        request.setMethod("GET");
+        setRequestParameters(List.of(new Pair<>("client_id", "mockClientId"),
+                new Pair<>("response_type", "code"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("redirect_uri", redirectUri),
+                new Pair<>("resource", resourceNonUri)));
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertNotNull(getSidFromAuthorizeCodeClaimsSet(successResponse));
+        Assert.assertNull(successResponse.getIssuer());
+        final List<String> audience = getAudienceFromAuthorizeCodeClaimsSet(successResponse);
+        Assert.assertNotNull(audience);
+        Assert.assertEquals(audience.size(), 1);
+        Assert.assertTrue(audience.contains(resourceNonUri));
+    }
+
     @Test
     public void testWithAuthorizationCodeFlowIssInResponse() throws IOException, SessionException {
         request.setMethod("GET");
@@ -384,6 +412,30 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNull(successResponse.getIssuer());
     }
 
+    @Test
+    public void testWithImplicitFlowAndNonUriResource() throws IOException, SessionException {
+        request.setMethod("GET");
+        setRequestParameters(List.of(new Pair<>("client_id", "mockClientId"),
+                new Pair<>("response_type", "id_token"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("redirect_uri", redirectUri),
+                new Pair<>("resource", resourceNonUri),
+                new Pair<>("nonce", "idhas3h23hi13h1o2i32")));
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNotNull(successResponse.getIDToken());
+        Assert.assertNotNull(getSidFromIDToken(successResponse));
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNull(successResponse.getAuthorizationCode());
+        Assert.assertNull(successResponse.getIssuer());
+    }
+
     @Test
     public void testWithImplicitTokenFlow() throws IOException, SessionException, DataSealerException, ParseException {
         request.setMethod("GET");
@@ -496,6 +548,37 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNotNull(token.getStringClaim("eduPersonScopedAffiliation"));
     }
 
+    @Test
+    public void testWithImplicitTokenFlowAndNonUriResource() throws IOException, SessionException, ParseException {
+        request.setMethod("GET");
+        setRequestParameters(List.of(new Pair<>("client_id", "mockClientId"),
+                new Pair<>("response_type", "id_token token"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("redirect_uri", redirectUri),
+                new Pair<>("resource", resourceNonUri),
+                new Pair<>("nonce", "idhas3h23hi13h1o2i32")));
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+        storeMetadata(storageService, resourceNonUri, null, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNotNull(successResponse.getIDToken());
+        final String sid1 = getSidFromIDToken(successResponse);
+        Assert.assertNotNull(sid1);
+        Assert.assertNotNull(successResponse.getAccessToken());
+        final String sid2 = getSidFromJWTAccessTokenClaimsSet(successResponse);
+        Assert.assertEquals(sid1, sid2);
+        Assert.assertNull(successResponse.getAuthorizationCode());
+        Assert.assertNull(successResponse.getIssuer());
+
+        final JWTClaimsSet token =  SignedJWT.parse(successResponse.getAccessToken().getValue()).getJWTClaimsSet();
+        Assert.assertEquals(token.getAudience(), List.of(resourceNonUri, issuer));
+    }
+
     @Test
     public void testWithImplicitTokenFlowNoNonce() throws IOException, SessionException {
         request.setMethod("GET");
@@ -655,6 +738,40 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         final String sid2 = getSidFromAuthorizeCodeClaimsSet(successResponse);
         Assert.assertEquals(sid1, sid2);
         Assert.assertNull(successResponse.getIssuer());
+        final List<String> audience = getAudienceFromAuthorizeCodeClaimsSet(successResponse);
+        Assert.assertNotNull(audience);
+        Assert.assertEquals(audience.size(), 1);
+        Assert.assertTrue(audience.contains(resource));
+    }
+
+    @Test
+    public void testWithHybridIdTokenFlowAndNonUriResource() throws IOException, SessionException {
+        request.setMethod("GET");
+        setRequestParameters(List.of(new Pair<>("client_id", "mockClientId"),
+                new Pair<>("response_type", "code id_token"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("resource", resourceNonUri),
+                new Pair<>("redirect_uri", redirectUri),
+                new Pair<>("nonce", "idhas3h23hi13h1o2i32")));
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        final String sid1 = getSidFromIDToken(successResponse);
+        Assert.assertNotNull(sid1);
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        final String sid2 = getSidFromAuthorizeCodeClaimsSet(successResponse);
+        Assert.assertEquals(sid1, sid2);
+        Assert.assertNull(successResponse.getIssuer());
+        final List<String> audience = getAudienceFromAuthorizeCodeClaimsSet(successResponse);
+        Assert.assertNotNull(audience);
+        Assert.assertEquals(audience.size(), 1);
+        Assert.assertTrue(audience.contains(resourceNonUri));
     }
 
     @Test
@@ -783,6 +900,37 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNotNull(token.getStringClaim("eduPersonScopedAffiliation"));
     }
 
+    @Test
+    public void testWithHybridTokenFlowAndNonUriResource() throws IOException, SessionException, ParseException {
+        request.setMethod("GET");
+        setRequestParameters(List.of(new Pair<>("client_id", "mockClientId"),
+                new Pair<>("response_type", "code token"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("resource", resourceNonUri),
+                new Pair<>("redirect_uri", redirectUri)));
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+        storeMetadata(storageService, resourceNonUri, null, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        // success response as id_token is not involved and thus nonce is not required
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNotNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertNull(successResponse.getIssuer());
+
+        final String sid1 = getSidFromJWTAccessTokenClaimsSet(successResponse);
+        final String sid2 = getSidFromAuthorizeCodeClaimsSet(successResponse);
+        Assert.assertEquals(sid1, sid2);
+
+        final JWTClaimsSet token =  SignedJWT.parse(successResponse.getAccessToken().getValue()).getJWTClaimsSet();
+        Assert.assertEquals(token.getAudience(), List.of(resourceNonUri, issuer));
+    }
+
     @Test
     public void testWithHybridIdTokenTokenFlow() throws IOException, SessionException, ParseException, DataSealerException {
         request.setMethod("GET");
@@ -942,6 +1090,39 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNotNull(token.getStringClaim("eduPersonScopedAffiliation"));
     }
 
+    @Test
+    public void testWithHybridIdTokenTokenFlowAndNonUriResource() throws IOException, SessionException, ParseException {
+        request.setMethod("GET");
+        setRequestParameters(List.of(new Pair<>("client_id", "mockClientId"),
+                new Pair<>("response_type", "code id_token token"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("resource", resourceNonUri),
+                new Pair<>("redirect_uri", redirectUri),
+                new Pair<>("nonce", "idhas3h23hi13h1o2i32")));
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+        storeMetadata(storageService, resourceNonUri, null, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNotNull(successResponse.getIDToken());
+        Assert.assertNotNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertNull(successResponse.getIssuer());
+
+        final String sid1 = getSidFromJWTAccessTokenClaimsSet(successResponse);
+        final String sid2 = getSidFromAuthorizeCodeClaimsSet(successResponse);
+        final String sid3 = getSidFromIDToken(successResponse);
+        Assert.assertEquals(sid1, sid2);
+        Assert.assertEquals(sid2, sid3);
+
+        final JWTClaimsSet token =  SignedJWT.parse(successResponse.getAccessToken().getValue()).getJWTClaimsSet();
+        Assert.assertEquals(token.getAudience(), List.of(resourceNonUri, issuer));
+    }
+
     @Test
     public void testWithHybridIdTokenTokenFlowNoNonce() throws IOException, SessionException {
         request.setMethod("GET");
@@ -1648,6 +1829,18 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         }
     }
 
+    protected List<String> getAudienceFromAuthorizeCodeClaimsSet(final AuthenticationSuccessResponse successResponse) {
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        final AuthorizeCodeClaimsSet claims;
+        try {
+            claims = AuthorizeCodeClaimsSet.parse(successResponse.getAuthorizationCode().getValue(), getDataSealer());
+            Assert.assertNotNull(claims.getAudience());
+            return claims.getAudience();
+        } catch (ParseException | DataSealerException e) {
+            return null;
+        }
+    }
+
     protected String getSidFromOpaqueAccessTokenClaimsSet(final AuthenticationSuccessResponse successResponse) {
         Assert.assertNotNull(successResponse.getAccessToken());
         final AccessTokenClaimsSet claims;
@@ -1709,6 +1902,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         removeMetadata(storageService, "mockClientIdCustomTokens");
         removeMetadata(storageService, "mockClientIdRequestObjectEnforced");
         removeMetadata(storageService, clientIdIssInResponse);
+        removeMetadata(storageService, resourceNonUri);
     }
 
 }
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index 90ce2644..b71cf236 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
@@ -43,6 +43,8 @@ import com.nimbusds.jose.EncryptionMethod;
 import com.nimbusds.jose.JWEAlgorithm;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.AccessTokenResponse;
 import com.nimbusds.oauth2.sdk.AuthorizationCode;
 import com.nimbusds.oauth2.sdk.OAuth2Error;
@@ -93,6 +95,9 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     String clientIdNoIdTokenViaRefreshToken = "mockClientIdNoIdTokenViaRefreshToken";
     String codeVerifier = "9234567812345678123456781234567812345678123456781234567812345678";
 
+    String resourceUri = "https://rp.example.org";
+    String resourceNonUri = "resource.example.org";
+    
     Scope scope = Scope.parse("openid profile email offline_access");
     
     @Autowired
@@ -117,6 +122,8 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         removeMetadata(storageService, clientIdRefreshTokenRotation);
         removeMetadata(storageService, clientIdJwtAccessToken);
         removeMetadata(storageService, clientIdNoIdTokenViaRefreshToken);
+        removeMetadata(storageService, resourceUri);
+        removeMetadata(storageService, resourceNonUri);
     }
 
     @Test
@@ -233,6 +240,54 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         }
     }
 
+    @Test
+    public void testValidWithUriResource() throws Exception {
+        final Map<String, String> requestParameters = createRequestParameters(redirectUri, "authorization_code",
+                buildAuthorizationCode(clientId, null, "openid profile email offline_access",
+                        List.of(resourceUri)), clientId);
+        requestParameters.put("resource", resourceUri);
+        initializeGrantAndRequest(clientId, requestParameters);
+        storeConsent(storageService, "jdoe", clientId, "mail");
+        final OIDCClientMetadata resourceMetadata = buildMetadataSkeleton();
+        resourceMetadata.setScope(Scope.parse("openid profile email offline_access"));
+        storeMetadataObject(storageService, resourceUri, clientSecret, resourceMetadata);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+        Assert.assertNotNull(response.getTokens().getRefreshToken());
+        Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
+        Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+        final SignedJWT jwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
+        final List<String> audience = jwt.getJWTClaimsSet().getAudience();
+        Assert.assertEquals(audience.size(), 2);
+        Assert.assertTrue(audience.contains(resourceUri));
+        Assert.assertTrue(audience.contains(issuer));
+    }
+
+    @Test
+    public void testValidGrantWithNonUriResource() throws Exception {
+        final Map<String, String> requestParameters = createRequestParameters(redirectUri, "authorization_code",
+                buildAuthorizationCode(clientId, null, "openid profile email offline_access",
+                        List.of(resourceNonUri)), clientId);
+        requestParameters.put("resource", resourceNonUri);
+        initializeGrantAndRequest(clientId, requestParameters);
+        storeConsent(storageService, "jdoe", clientId, "mail");
+        final OIDCClientMetadata resourceMetadata = buildMetadataSkeleton();
+        resourceMetadata.setScope(Scope.parse("openid profile email offline_access"));
+        storeMetadataObject(storageService, resourceNonUri, clientSecret, resourceMetadata);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+        Assert.assertNotNull(response.getTokens().getRefreshToken());
+        Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
+        Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+        final SignedJWT jwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
+        final List<String> audience = jwt.getJWTClaimsSet().getAudience();
+        Assert.assertEquals(audience.size(), 2);
+        Assert.assertTrue(audience.contains(resourceNonUri));
+        Assert.assertTrue(audience.contains(issuer));
+    }
+
     @Test
     public void testValidGrant_unregisterdClient_policyCompliant() throws Exception {
         final String clientId = "policyAcceptedClient1";
@@ -304,6 +359,52 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
     }
 
+    @Test
+    public void testValidGrantNoOpenIdWithUriResource() throws Exception {
+        final Map<String, String> requestParameters = createRequestParameters(redirectUri, "authorization_code",
+                buildAuthorizationCode(clientId, null, "profile email offline_access",
+                        List.of(resourceUri)), clientId);
+        requestParameters.put("resource", resourceUri);
+        initializeGrantAndRequest(clientId, requestParameters);
+        storeConsent(storageService, "jdoe", clientId, "mail");
+        final OIDCClientMetadata resourceMetadata = buildMetadataSkeleton();
+        resourceMetadata.setScope(Scope.parse("profile email offline_access"));
+        storeMetadataObject(storageService, resourceUri, clientSecret, resourceMetadata);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+        Assert.assertNotNull(response.getTokens().getRefreshToken());
+        Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
+        Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+        final SignedJWT jwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
+        final List<String> audience = jwt.getJWTClaimsSet().getAudience();
+        Assert.assertEquals(audience.size(), 1);
+        Assert.assertTrue(audience.contains(resourceUri));
+    }
+
+    @Test
+    public void testValidGrantNoOpenIdWithNonUriResource() throws Exception {
+        final Map<String, String> requestParameters = createRequestParameters(redirectUri, "authorization_code",
+                buildAuthorizationCode(clientId, null, "profile email offline_access",
+                        List.of(resourceNonUri)), clientId);
+        requestParameters.put("resource", resourceNonUri);
+        initializeGrantAndRequest(clientId, requestParameters);
+        storeConsent(storageService, "jdoe", clientId, "mail");
+        final OIDCClientMetadata resourceMetadata = buildMetadataSkeleton();
+        resourceMetadata.setScope(Scope.parse("profile email offline_access"));
+        storeMetadataObject(storageService, resourceNonUri, clientSecret, resourceMetadata);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+        Assert.assertNotNull(response.getTokens().getRefreshToken());
+        Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
+        Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+        final SignedJWT jwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
+        final List<String> audience = jwt.getJWTClaimsSet().getAudience();
+        Assert.assertEquals(audience.size(), 1);
+        Assert.assertTrue(audience.contains(resourceNonUri));
+    }
+
     @Test
     public void testValidGrantNoOpenIdRefreshTokensDisabledInSSOProfile() throws Exception {
         final String clientId = "mockClientIdNoRefreshTokensInSSOProfile";
@@ -567,6 +668,11 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         return buildAuthorizationCode(clientId, verifier, null, null, null, scope);
     }
 
+    protected String buildAuthorizationCode(final String clientId, final String verifier, final String scope,
+            final Collection<String> aud) throws Exception {
+        return buildAuthorizationCodeWithSid(clientId, verifier, null, null, null, scope, null, aud);
+    }
+
     protected String buildAuthorizationCode(final String clientId, final String verifier,
             final JSONObject deliveryClaims, final JSONObject deliveryClaimsIDToken,
             final JSONObject deliveryClaimsUserInfo) throws Exception {
@@ -581,9 +687,25 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
                 deliveryClaimsUserInfo, scope, null);
     }
 
+    protected String buildAuthorizationCode(final String clientId, final String verifier,
+            final JSONObject deliveryClaims, final JSONObject deliveryClaimsIDToken,
+            final JSONObject deliveryClaimsUserInfo, final String scope, final Collection<String> aud)
+                    throws Exception {
+        return buildAuthorizationCodeWithSid(clientId, verifier, deliveryClaims, deliveryClaimsIDToken,
+                deliveryClaimsUserInfo, scope, null, aud);
+    }
+
     protected String buildAuthorizationCodeWithSid(final String clientId, final String verifier,
             final JSONObject deliveryClaims, final JSONObject deliveryClaimsIDToken,
             final JSONObject deliveryClaimsUserInfo, final String scope, final String sid) throws Exception {
+        return buildAuthorizationCodeWithSid(clientId, verifier, deliveryClaims, deliveryClaimsIDToken,
+                deliveryClaimsUserInfo, scope, sid, null);
+    }
+
+    protected String buildAuthorizationCodeWithSid(final String clientId, final String verifier,
+            final JSONObject deliveryClaims, final JSONObject deliveryClaimsIDToken,
+            final JSONObject deliveryClaimsUserInfo, final String scope, final String sid, final Collection<String> aud)
+                    throws Exception {
         final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
         builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
             .setClientID(new ClientID(clientId))
@@ -606,6 +728,9 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         if (deliveryClaimsUserInfo != null) {
             builder.setDlClaimsUI(new ClaimsSet(deliveryClaimsUserInfo));
         }
+        if (aud != null) {
+            builder.setAudience(aud);
+        }
 
         return builder.build().serialize(getDataSealer());
     }
@@ -1095,7 +1220,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
                 };
     }
 
-    private AccessTokenClaimsSet unwrapAccessToken(final OIDCTokenResponse tokenResponse) {
+    private AccessTokenClaimsSet unwrapAccessToken(final AccessTokenResponse tokenResponse) {
         final AccessToken accessToken = tokenResponse.getTokens().getAccessToken();
         Assert.assertNotNull(accessToken);
         try {
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 2992cdc2..bdddb0ea 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -262,7 +262,7 @@
             </property>
         </bean>
         <bean parent="RelyingPartyByName"
-                c:relyingPartyIds="#{{'https://rp.example.org', 'https://resource.example.org', 'https://mock.example.org'}}">
+                c:relyingPartyIds="#{{'https://rp.example.org', 'https://resource.example.org', 'https://mock.example.org', 'resource.example.org'}}">
             <property name="profileConfigurations">
                  <list>
                      <bean parent="OAUTH2.TokenAudience.MDDriven" p:accessTokenType="JWT" p:encryptionOptional="true" />

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


More information about the commits mailing list