[java-idp-oidc] branch main updated: JOIDC-128 - Support OAuth authorization requests

Henri Mikkonen henri.mikkonen at iki.fi
Fri Oct 7 10:24:09 UTC 2022


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=d8d8158a3f57c90dfba111647968f72116607c31

The following commit(s) were added to refs/heads/main by this push:
     new d8d8158a JOIDC-128 - Support OAuth authorization requests
d8d8158a is described below

commit d8d8158a3f57c90dfba111647968f72116607c31
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Oct 7 13:22:49 2022 +0300

    JOIDC-128 - Support OAuth authorization requests
    
    https://shibboleth.atlassian.net/browse/JOIDC-128
    
    UserInfo flow now requires openid -scope from the access token.
---
 .../oidc/op/oauth2/profile/impl/ValidateScope.java | 35 ++++++++-
 .../idp/flows/oidc/userinfo/userinfo-beans.xml     | 11 +++
 .../op/oauth2/profile/impl/ValidateScopeTest.java  | 86 +++++++++++++++++++++-
 .../plugin/oidc/op/profile/flow/UserInfoTest.java  | 31 ++++++++
 4 files changed, 157 insertions(+), 6 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateScope.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateScope.java
index 5b860f85..60505d59 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateScope.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateScope.java
@@ -77,6 +77,9 @@ public class ValidateScope extends AbstractOAuthAuthorizationResponseAction {
     /** Strategy used to obtain the scope allowed for the client. */
     @Nonnull private Function<ProfileRequestContext,Scope> allowedScopeLookupStrategy;
 
+    /** Strategy used to obtain the mandatory scope value value. */
+    @Nonnull private Function<ProfileRequestContext,Scope> mandatoryScopeLookupStrategy;
+
     /** Strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext}. */
     @Nonnull
     private Function<ProfileRequestContext,OIDCAuthenticationResponseTokenClaimsContext>
@@ -88,6 +91,7 @@ public class ValidateScope extends AbstractOAuthAuthorizationResponseAction {
         relyingPartyIdLookupStrategy = new RelyingPartyIdLookupFunction();
         allowedScopeLookupStrategy = new ClientInfoScopeLookupFunction().compose(
                 new DefaultOIDCMetadataContextLookupFunction());
+        mandatoryScopeLookupStrategy = prc -> null;
         tokenClaimsContextLookupStrategy =
                 new ChildContextLookup<>(OIDCAuthenticationResponseTokenClaimsContext.class).compose(
                         new OIDCAuthenticationResponseContextLookupFunction());
@@ -126,9 +130,21 @@ public class ValidateScope extends AbstractOAuthAuthorizationResponseAction {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
         allowedScopeLookupStrategy = Constraint.isNotNull(strategy,
-                "Allowed scope lookyp strategy cannot be null");
+                "Allowed scope lookup strategy cannot be null");
     }
-    
+
+    /**
+     * Set the strategy used to locate the mandatory scope value.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setMandatoryScopeLookupStrategy(@Nonnull final Function<ProfileRequestContext,Scope> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        mandatoryScopeLookupStrategy = Constraint.isNotNull(strategy,
+                "Mandatory scope lookup strategy cannot be null");
+    }
+
     /**
      * Set the strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext} associated with a given
      * {@link ProfileRequestContext}.
@@ -179,6 +195,21 @@ public class ValidateScope extends AbstractOAuthAuthorizationResponseAction {
             return;
         }
 
+        final Scope mandatoryScopes = mandatoryScopeLookupStrategy.apply(profileRequestContext);
+        if (mandatoryScopes != null && !mandatoryScopes.isEmpty()) {
+            if (requestedScopes == null || requestedScopes.isEmpty()) {
+                log.warn("{} Mendatory scope set to {} but none requested", getLogPrefix(), mandatoryScopes.toString());
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+                return;
+            }
+            for (final Scope.Value value : mandatoryScopes) {
+                if (!requestedScopes.contains(value.getValue())) {
+                    log.warn("{} Mandatory scope {} is not requested", getLogPrefix(), value.getValue());
+                    ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+                    return;
+                }
+            }
+        }
         if (allowedScopes == null || allowedScopes.isEmpty()) {
             log.debug("{} No allowed scope for client {}, nothing to do", getLogPrefix(), clientId);
             return;
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
index 4af5b42f..4b0d4eb5 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
@@ -46,6 +46,17 @@
         <property name="requestedScopeLookupStrategy">
             <null/>
         </property>
+        <property name="mandatoryScopeLookupStrategy">
+            <bean parent="shibboleth.Functions.Scripted">
+                <constructor-arg>
+                    <value>
+                    <![CDATA[
+                        new com.nimbusds.oauth2.sdk.Scope(com.nimbusds.openid.connect.sdk.OIDCScopeValue.OPENID);
+                    ]]>
+                    </value>
+                </constructor-arg>
+            </bean>
+        </property>
     </bean>
     
     <bean id="SetRequestedClaimsToResponseContext"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateScopeTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateScopeTest.java
index eb36d64d..392e8041 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateScopeTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateScopeTest.java
@@ -62,15 +62,19 @@ public class ValidateScopeTest extends BaseOIDCResponseActionTest {
 
     @BeforeMethod
     private void init() throws ComponentInitializationException, URISyntaxException {
+        init(OIDCScopeValue.OPENID, OIDCScopeValue.EMAIL, OIDCScopeValue.OFFLINE_ACCESS);
+    }
+    
+    private void init(Scope.Value... values) throws URISyntaxException, ComponentInitializationException {
         action = new ValidateScope();
         action.initialize();
         final OIDCMetadataContext oidcCtx =
                 profileRequestCtx.getInboundMessageContext().getSubcontext(OIDCMetadataContext.class, true);
         metaData = new OIDCClientMetadata();
         final Scope scope = new Scope();
-        scope.add(OIDCScopeValue.OPENID);
-        scope.add(OIDCScopeValue.EMAIL);
-        scope.add(OIDCScopeValue.OFFLINE_ACCESS);
+        for (final Scope.Value value : values) {
+            scope.add(value);
+        }
         metaData.setScope(scope);
         metaData.setRedirectionURI(new URI("https://notmatching.org"));
         final OIDCClientInformation information =
@@ -82,7 +86,7 @@ public class ValidateScopeTest extends BaseOIDCResponseActionTest {
                 respCtx.getSubcontext(OIDCAuthenticationResponseTokenClaimsContext.class, true);
         tokenClaimsCtx.getClaims().setClaim("gen", "value1");
         tokenClaimsCtx.getIdtokenClaims().setClaim("idtoken", "value2");
-        tokenClaimsCtx.getUserinfoClaims().setClaim("userinfo", "value3");
+        tokenClaimsCtx.getUserinfoClaims().setClaim("userinfo", "value3");        
     }
 
     /**
@@ -292,4 +296,78 @@ public class ValidateScopeTest extends BaseOIDCResponseActionTest {
       Assert.assertNotNull(tokenClaimsCtx);
   }
 
+  /**
+   * Test that action notices missing mandatory scope.
+   * 
+   * @throws ComponentInitializationException
+   * @throws URISyntaxException
+   */
+  @Test
+  public void testMandatoryScopeMissing() throws ComponentInitializationException, URISyntaxException {
+
+      action = new ValidateScope();
+      action.setRequestedScopeLookupStrategy(null);
+      action.setMandatoryScopeLookupStrategy(prc -> Scope.parse("openid"));
+      action.initialize();
+      
+      final UserInfoRequest req =
+              new UserInfoRequest(new URI("http://localhost"), Method.POST, new BearerAccessToken());
+      setUserInfoRequest(req);
+      
+      final AuthorizeCodeClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
+              .setJWTID(idGenerator)
+              .setClientID(new ClientID("s6BhdRkqt3"))
+              .setIssuer("issuer")
+              .setPrincipal("userPrin")
+              .setSubject("subject")
+              .setIssuedAt(Instant.now())
+              .setExpiresAt(Instant.now())
+              .setAuthenticationTime(Instant.now())
+              .setRedirectURI(new URI("http://localhost"))
+              .setScope(Scope.parse("email"))
+              .setACR(new ACR("0"))
+              .build();
+      respCtx.setAuthorizationGrantClaimsSet(claims);
+      
+      final Event event = action.execute(requestCtx);
+      ActionTestingSupport.assertEvent(event, "InvalidMessage");
+  }
+
+  /**
+   * Test that action deals requested openid scope correctly when it's not registered.
+   * 
+   * @throws ComponentInitializationException
+   * @throws URISyntaxException
+   */
+  @Test
+  public void testOpenidScopeRequestedNotRegistered() throws ComponentInitializationException, URISyntaxException {
+      init(OIDCScopeValue.EMAIL);
+      
+      action = new ValidateScope();
+      action.setRequestedScopeLookupStrategy(null);
+      action.initialize();
+      
+      final UserInfoRequest req =
+              new UserInfoRequest(new URI("http://localhost"), Method.POST, new BearerAccessToken());
+      setUserInfoRequest(req);
+      
+      final AuthorizeCodeClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
+              .setJWTID(idGenerator)
+              .setClientID(new ClientID("s6BhdRkqt3"))
+              .setIssuer("issuer")
+              .setPrincipal("userPrin")
+              .setSubject("subject")
+              .setIssuedAt(Instant.now())
+              .setExpiresAt(Instant.now())
+              .setAuthenticationTime(Instant.now())
+              .setRedirectURI(new URI("http://localhost"))
+              .setScope(Scope.parse("openid email"))
+              .setACR(new ACR("0"))
+              .build();
+      respCtx.setAuthorizationGrantClaimsSet(claims);
+      
+      final Event event = action.execute(requestCtx);
+      ActionTestingSupport.assertEvent(event, "InvalidMessage");
+  }
+
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java
index 37efb494..26718392 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java
@@ -22,6 +22,7 @@ import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.text.ParseException;
 
+import org.opensaml.profile.action.EventIds;
 import org.opensaml.storage.RevocationCache;
 import org.opensaml.storage.StorageService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -124,6 +125,36 @@ public class UserInfoTest extends AbstractOidcApiFlowTest {
         Assert.assertNull(response.getUserInfoJWT());
     }
 
+    @Test
+    public void testFailsWhenNoOpenidScopeRequested() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+        ComponentInitializationException, IOException {
+        final BearerAccessToken token = buildToken(clientId, subject, new Scope("profile"));
+        storeMetadata(storageService, clientId, "mockSecret", scope);
+        request.addHeader("Authorization", token.toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+    }    
+
+    @Test
+    public void testFailsWhenNoOpenidScopeRegistered() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+        ComponentInitializationException, IOException {
+        final BearerAccessToken token = buildToken(clientId, subject, new Scope("openid profile"));
+        storeMetadata(storageService, clientId, "mockSecret", new Scope("profile"));
+        request.addHeader("Authorization", token.toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+    }    
+
+    @Test
+    public void testFailsWhenNoAnyScopeRegistered() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+        ComponentInitializationException, IOException {
+        final BearerAccessToken token = buildToken(clientId, subject, new Scope("openid profile"));
+        storeMetadata(storageService, clientId, "mockSecret", null);
+        request.addHeader("Authorization", token.toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+    }    
+
     @Test
     public void testSuccessOnlySubjectWithLegacyToken() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
         ComponentInitializationException, IOException, com.nimbusds.oauth2.sdk.ParseException {

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


More information about the commits mailing list