forked from DSpace/DSpace
-
Notifications
You must be signed in to change notification settings - Fork 1
ZCU-PUB/fix: shibboleth special groups lost in short-lived and refreshed tokens - 403 on download (backport #1347) #1374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
milanmajchrak
merged 4 commits into
customer/zcu-pub
from
zcu-pub/backport-1347-shib-special-groups
Jul 20, 2026
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ed471e0
ZCU-PUB/test: shibboleth special groups must survive short-lived and …
milanmajchrak 9bb81d6
UFAL/Obtain special groups from user context when new token is genera…
kosarko 57ba86a
ZCU-PUB/test: fail cleanly on a missing Authorization header, avoid a…
milanmajchrak b7d3c78
ZCU-PUB/test: resolve Copilot review comments
milanmajchrak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
178 changes: 178 additions & 0 deletions
178
...er-webapp/src/test/java/org/dspace/app/rest/security/ClarinShibbolethSpecialGroupsIT.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| /** | ||
| * The contents of this file are subject to the license and copyright | ||
| * detailed in the LICENSE and NOTICE files at the root of the source | ||
| * tree and available online at | ||
| * | ||
| * http://www.dspace.org/license/ | ||
| */ | ||
| package org.dspace.app.rest.security; | ||
|
|
||
| import static org.junit.Assert.assertNotNull; | ||
| import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | ||
| import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | ||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||
|
|
||
| import java.io.InputStream; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import org.apache.commons.codec.CharEncoding; | ||
| import org.apache.commons.io.IOUtils; | ||
| import org.dspace.app.rest.test.AbstractControllerIntegrationTest; | ||
| import org.dspace.app.util.Util; | ||
| import org.dspace.builder.BitstreamBuilder; | ||
| import org.dspace.builder.CollectionBuilder; | ||
| import org.dspace.builder.CommunityBuilder; | ||
| import org.dspace.builder.EPersonBuilder; | ||
| import org.dspace.builder.GroupBuilder; | ||
| import org.dspace.builder.ItemBuilder; | ||
| import org.dspace.content.Bitstream; | ||
| import org.dspace.content.Collection; | ||
| import org.dspace.content.Community; | ||
| import org.dspace.content.Item; | ||
| import org.dspace.core.I18nUtil; | ||
| import org.dspace.eperson.EPerson; | ||
| import org.dspace.eperson.Group; | ||
| import org.dspace.services.ConfigurationService; | ||
| import org.junit.Before; | ||
| import org.junit.Test; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.test.web.servlet.MvcResult; | ||
|
|
||
| /** | ||
| * Integration test verifying that the Shibboleth special groups (e.g. the default `Authenticated` group) | ||
| * survive into tokens which are minted on stateless REST requests after the login: | ||
| * the short-lived token used for bitstream downloads and the refreshed login token. | ||
| * | ||
| * Replicates https://github.com/dataquest-dev/DSpace/issues/900 - a bitstream restricted to the | ||
| * `Authenticated` group is visible after the Shibboleth login, but its download returns 403, | ||
| * because the special groups are lost when the short-lived token is generated | ||
| * (see ufal/clarin-dspace#1373). | ||
| * | ||
| * @author Milan Majchrak (milan.majchrak at dataquest.sk) | ||
| */ | ||
| public class ClarinShibbolethSpecialGroupsIT extends AbstractControllerIntegrationTest { | ||
|
|
||
| public static final String[] SHIB_ONLY = {"org.dspace.authenticate.clarin.ClarinShibAuthentication"}; | ||
| private static final String NET_ID_TEST_EPERSON = "123456789"; | ||
| private static final String IDP_TEST_EPERSON = "Test Idp"; | ||
| private static final String AUTHORIZATION_TYPE = "Bearer "; | ||
|
|
||
| private EPerson clarinEperson; | ||
| private Bitstream restrictedBitstream; | ||
|
|
||
| @Autowired | ||
| ConfigurationService configurationService; | ||
|
|
||
| @Before | ||
| public void setup() throws Exception { | ||
| super.setUp(); | ||
|
|
||
| // Enable Shibboleth login for all tests | ||
| configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); | ||
|
|
||
| context.turnOffAuthorisationSystem(); | ||
|
|
||
| // Create an eperson with netID - that means the user already exists in the database | ||
| clarinEperson = EPersonBuilder.createEPerson(context) | ||
| .withCanLogin(false) | ||
| .withEmail("clarin@email.com") | ||
| .withNameInMetadata("first", "last") | ||
| .withLanguage(I18nUtil.getDefaultLocale().getLanguage()) | ||
| .withNetId(Util.formatNetId(NET_ID_TEST_EPERSON, IDP_TEST_EPERSON)) | ||
| .build(); | ||
|
|
||
| // The group every shibboleth-authenticated user is implicitly added to (as a special group) | ||
| String defaultGroupName = configurationService.getProperty("authentication-shibboleth.default.auth.group"); | ||
| Group authenticatedGroup = GroupBuilder.createGroup(context) | ||
| .withName(defaultGroupName) | ||
| .build(); | ||
|
|
||
| // A bitstream readable only by the shibboleth default special group | ||
| Community community = CommunityBuilder.createCommunity(context) | ||
| .withName("Community") | ||
| .build(); | ||
| Collection collection = CollectionBuilder.createCollection(context, community) | ||
| .withName("Collection") | ||
| .build(); | ||
| Item item = ItemBuilder.createItem(context, collection) | ||
| .withTitle("Item with a restricted bitstream") | ||
| .build(); | ||
| try (InputStream is = IOUtils.toInputStream("Restricted content", CharEncoding.UTF_8)) { | ||
| restrictedBitstream = BitstreamBuilder.createBitstream(context, item, is) | ||
| .withName("restricted.txt") | ||
| .withMimeType("text/plain") | ||
| .withReaderGroup(authenticatedGroup) | ||
| .build(); | ||
| } | ||
|
|
||
| context.restoreAuthSystemState(); | ||
| } | ||
|
|
||
| /** | ||
| * Replication of the issue #900: | ||
| * 1. Sign in via Shibboleth - the user is implicitly added into the `Authenticated` special group. | ||
| * 2. The bitstream restricted to the `Authenticated` group is readable with the login token. | ||
| * 3. The UI downloads the bitstream with a short-lived token minted on a separate stateless request | ||
| * - the download must succeed too. | ||
| */ | ||
| @Test | ||
| public void shouldDownloadRestrictedBitstreamWithShortLivedTokenAfterShibLogin() throws Exception { | ||
| String loginToken = shibLogin(); | ||
|
|
||
| // Sanity check: the login token keeps the special groups (its `sg` claim was computed | ||
| // during the shibboleth login request), so the restricted bitstream is readable. | ||
| getClient(loginToken).perform(get("/api/core/bitstreams/" + restrictedBitstream.getID() + "/content")) | ||
| .andExpect(status().isOk()); | ||
|
|
||
| // The short-lived token is minted on a stateless request - the special groups must be | ||
| // obtained from the user context (restored from the login token), not from the session. | ||
| String shortLivedToken = getShortLivedToken(loginToken); | ||
| getClient().perform(get("/api/core/bitstreams/" + restrictedBitstream.getID() | ||
| + "/content?authentication-token=" + shortLivedToken)) | ||
| .andExpect(status().isOk()); | ||
| } | ||
|
|
||
| /** | ||
| * The refreshed login token (POST /api/authn/login with the Bearer token, no shibboleth headers) | ||
| * must keep the special groups too, otherwise the user loses the access after the first token refresh | ||
| * (see ufal/clarin-dspace#1373). | ||
| */ | ||
| @Test | ||
| public void shouldKeepSpecialGroupsAfterLoginTokenRefresh() throws Exception { | ||
| String loginToken = shibLogin(); | ||
|
|
||
| // Sanity check: the restricted bitstream is readable with the login token | ||
| getClient(loginToken).perform(get("/api/core/bitstreams/" + restrictedBitstream.getID() + "/content")) | ||
| .andExpect(status().isOk()); | ||
|
|
||
| // Refresh the login token on a stateless request (no shibboleth session/headers) | ||
| String refreshedToken = getClient(loginToken).perform(post("/api/authn/login")) | ||
| .andExpect(status().isOk()) | ||
| .andReturn().getResponse().getHeader("Authorization") | ||
| .replace(AUTHORIZATION_TYPE, ""); | ||
|
milanmajchrak marked this conversation as resolved.
Outdated
|
||
|
|
||
| // The restricted bitstream must still be readable with the refreshed token | ||
| getClient(refreshedToken).perform(get("/api/core/bitstreams/" + restrictedBitstream.getID() + "/content")) | ||
| .andExpect(status().isOk()); | ||
| } | ||
|
|
||
| private String shibLogin() throws Exception { | ||
| String authHeader = getClient().perform(get("/api/authn/shibboleth") | ||
| .header("SHIB-MAIL", clarinEperson.getEmail()) | ||
| .header("Shib-Identity-Provider", IDP_TEST_EPERSON) | ||
| .header("SHIB-NETID", NET_ID_TEST_EPERSON)) | ||
| .andExpect(status().is3xxRedirection()) | ||
| .andReturn().getResponse().getHeader("Authorization"); | ||
| assertNotNull("The shibboleth login must return the Authorization header", authHeader); | ||
| return authHeader.replace(AUTHORIZATION_TYPE, ""); | ||
| } | ||
|
|
||
| private String getShortLivedToken(String loginToken) throws Exception { | ||
| ObjectMapper mapper = new ObjectMapper(); | ||
| MvcResult mvcResult = getClient(loginToken).perform(post("/api/authn/shortlivedtokens")) | ||
| .andExpect(status().isOk()) | ||
| .andReturn(); | ||
| String content = mvcResult.getResponse().getContentAsString(); | ||
| return mapper.readTree(content).get("token").asText(); | ||
|
milanmajchrak marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.