Skip to content

Commit

Permalink
Fix CREST action authorization
Browse files Browse the repository at this point in the history
The token ID can be retrieved from HTTP header or cookie for
authorization verification of CREST action.

This fixes an issue when requesting of the session info without token ID
in URL was forbidden - requests ended with HTTP status 403.
  • Loading branch information
fyrbach committed Jun 6, 2024
1 parent 0ec6976 commit 0990980
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
* Portions copyright 2024 Wren Security.
*/

package org.forgerock.openam.core.rest.session;
Expand Down Expand Up @@ -39,7 +40,7 @@ public class SessionResourceAuthzModule extends TokenOwnerAuthzModule {

@Inject
public SessionResourceAuthzModule(SSOTokenManager ssoTokenManager) {
super("tokenId", ssoTokenManager,
super(ssoTokenManager,
SessionResource.DELETE_PROPERTY_ACTION_ID, SessionResource.GET_PROPERTY_ACTION_ID,
SessionResource.GET_PROPERTY_NAMES_ACTION_ID, SessionResource.SET_PROPERTY_ACTION_ID,
SessionResource.GET_TIME_LEFT_ACTION_ID, SessionResource.GET_MAX_IDLE_ACTION_ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,35 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
* Portions copyright 2024 Wren Security.
*/

package org.forgerock.openam.core.rest.session;

import static org.forgerock.json.JsonValue.*;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.forgerock.json.JsonValue.field;
import static org.forgerock.json.JsonValue.json;
import static org.forgerock.json.JsonValue.object;

import com.google.inject.Inject;
import com.iplanet.am.util.SystemProperties;
import com.iplanet.dpro.session.share.SessionInfo;
import com.iplanet.services.naming.WebtopNamingQuery;
import com.iplanet.sso.SSOException;
import com.iplanet.sso.SSOToken;
import com.iplanet.sso.SSOTokenManager;
import com.sun.identity.idm.AMIdentity;
import com.sun.identity.idm.IdRepoException;
import com.sun.identity.shared.Constants;
import com.sun.identity.shared.debug.Debug;
import com.sun.identity.sm.DNMapper;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.forgerock.http.header.CookieHeader;
import org.forgerock.http.protocol.Cookie;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.Request;
import org.forgerock.json.resource.http.HttpContext;
import org.forgerock.openam.core.rest.session.query.SessionQueryManager;
import org.forgerock.openam.session.SessionConstants;
import org.forgerock.openam.utils.StringUtils;
Expand Down Expand Up @@ -76,6 +83,67 @@ public SessionResourceUtil(final SSOTokenManager ssoTokenManager,
this.webtopNamingQuery = webtopNamingQuery;
}

/**
* Retrieves the token ID from the given context and request. The method attempts to extract the token ID
* from various sources in the following order:
* <ol>
* <li>Path of the request</li>
* <li>URL parameters of the request</li>
* <li>Cookies</li>
* <li>HTTP headers</li>
* </ol>
* If the token ID is not found in any of these sources, the method returns {@code null}.
*
* @return The token ID if found; {@code null} otherwise.
*/
static String getTokenId(HttpContext context, Request request) {
String cookieName = SystemProperties.get(Constants.AM_COOKIE_NAME, "iPlanetDirectoryPro");

String tokenId = getTokenIdFromPath(request);

if (StringUtils.isEmpty(tokenId)) {
tokenId = getTokenIdFromUrlParam(request);
}

if (StringUtils.isEmpty(tokenId)) {
tokenId = getTokenIdFromCookie(context, cookieName);
}

if (StringUtils.isEmpty(tokenId)) {
tokenId = getTokenIdFromHeader(context, cookieName);
}

return StringUtils.isEmpty(tokenId) ? null : tokenId;
}

private static String getTokenIdFromPath(Request request) {
return request.getResourcePath();
}

private static String getTokenIdFromUrlParam(Request request) {
return request.getAdditionalParameter("tokenId");
}

private static String getTokenIdFromCookie(HttpContext context, String cookieName) {
final List<String> headers = context.getHeader("cookie");
for (String header : headers) {
for (Cookie cookie : CookieHeader.valueOf(header).getCookies()) {
if (cookie.getName().equalsIgnoreCase(cookieName)) {
return cookie.getValue();
}
}
}
return null;
}

private static String getTokenIdFromHeader(HttpContext context, String headerName) {
final List<String> header = context.getHeader(headerName);
if (!header.isEmpty()) {
return header.get(0);
}
return null;
}

/**
* tokenId may, or may not, specify a valid token. If it does, retrieve it and the carefully refresh it so
* as not to alter its idle time setting. If it does not exist, or is invalid, throw an SSOException.
Expand Down Expand Up @@ -124,7 +192,7 @@ public Collection<String> getAllServerIds() {
* @return A non null collection of SessionInfos from the named server.
*/
public Collection<SessionInfo> generateNamedServerSession(String serverId) {
List<String> serverList = Arrays.asList(new String[]{serverId});
List<String> serverList = List.of(serverId);
Collection<SessionInfo> sessions = queryManager.getAllSessions(serverList);
if (LOGGER.messageEnabled()) {
LOGGER.message("SessionResource.generateNmaedServerSession :: retrieved session list for server, " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2015-2016 ForgeRock AS.
* Portions copyright 2024 Wren Security.
*/

package org.forgerock.openam.core.rest.session;
Expand All @@ -24,7 +25,6 @@
import com.iplanet.sso.SSOToken;
import com.iplanet.sso.SSOTokenManager;
import com.sun.identity.shared.Constants;
import org.apache.commons.lang.StringUtils;
import org.forgerock.authz.filter.api.AuthorizationResult;
import org.forgerock.authz.filter.crest.api.CrestAuthorizationModule;
import org.forgerock.json.resource.ActionRequest;
Expand All @@ -37,6 +37,7 @@
import org.forgerock.json.resource.Request;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.json.resource.http.HttpContext;
import org.forgerock.openam.rest.resource.SSOTokenContext;
import org.forgerock.services.context.Context;
import org.forgerock.util.Reject;
Expand Down Expand Up @@ -64,23 +65,19 @@ public class TokenOwnerAuthzModule implements CrestAuthorizationModule {

private final SSOTokenManager ssoTokenManager;
private final Set<String> allowedActions;
private final String tokenIdParameter;

/**
* Creates an authz module that will verify that a tokenId provided by the user (via query params)
* is the same user (via universal identifier) as the user requesting the action.
*
* @param tokenIdParameter The tokenId query parameter. May not be null.
* @param ssoTokenManager An instance of the SSOTokenManager.
* @param allowedActions A list of allowed actions. Will be matched ignoring case.
*/
public TokenOwnerAuthzModule(String tokenIdParameter, SSOTokenManager ssoTokenManager, String... allowedActions) {
public TokenOwnerAuthzModule(SSOTokenManager ssoTokenManager, String... allowedActions) {
Reject.ifNull(allowedActions);
Reject.ifTrue(StringUtils.isEmpty(tokenIdParameter));

this.ssoTokenManager = ssoTokenManager;
this.allowedActions = new HashSet<>(Arrays.asList(allowedActions));
this.tokenIdParameter = tokenIdParameter;
}

@Override
Expand Down Expand Up @@ -136,12 +133,7 @@ public Promise<AuthorizationResult, ResourceException> authorizeQuery(Context co

boolean isTokenOwner(Context context, Request request) throws ResourceException, SSOException {
String loggedInUserId = getUserId(context);

String tokenId = request.getResourcePath(); // infer from the token from resource path
if (StringUtils.isEmpty(tokenId)) {
//if there's no tokenId then is it supplied as additional parameter
tokenId = request.getAdditionalParameter(tokenIdParameter);
}
String tokenId = SessionResourceUtil.getTokenId(context.asContext(HttpContext.class), request);

final String queryUsername = ssoTokenManager.createSSOToken(tokenId).getProperty(Constants.UNIVERSAL_IDENTIFIER);
return queryUsername.equalsIgnoreCase(loggedInUserId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2024 Wren Security.
*/

package org.forgerock.openam.core.rest.session;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;

import java.util.Collections;
import java.util.List;
import org.forgerock.json.resource.Request;
import org.forgerock.json.resource.http.HttpContext;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class SessionResourceUtilTest {

private HttpContext context;
private Request request;

@BeforeMethod
public void setUp() {
context = mock(HttpContext.class);
request = mock(Request.class);
}

@Test
public void testGetTokenIdFromPath() {
when(request.getResourcePath()).thenReturn("tokenIdFromPath");

String tokenId = SessionResourceUtil.getTokenId(context, request);

assertEquals(tokenId, "tokenIdFromPath");
}

@Test
public void testGetTokenIdFromUrlParam() {
when(request.getAdditionalParameter("tokenId")).thenReturn("tokenIdFromUrlParam");

String tokenId = SessionResourceUtil.getTokenId(context, request);

assertEquals(tokenId, "tokenIdFromUrlParam");
}

@Test
public void testGetTokenIdFromCookie() {
when(context.getHeader("cookie")).thenReturn(List.of("iPlanetDirectoryPro=tokenIdFromCookie"));

String tokenId = SessionResourceUtil.getTokenId(context, request);

assertEquals(tokenId, "tokenIdFromCookie");
}

@Test
public void testGetTokenIdFromHeader() {
when(context.getHeader("iPlanetDirectoryPro")).thenReturn(Collections.singletonList("tokenIdFromHeader"));

String tokenId = SessionResourceUtil.getTokenId(context, request);

assertEquals(tokenId, "tokenIdFromHeader");
}

@Test
public void testGetTokenIdNotFound() {
String tokenId = SessionResourceUtil.getTokenId(context, request);

assertNull(tokenId);
}

@Test(dataProvider = "tokenDataProvider")
public void testGetTokenIdPreferredSource(String pathToken, String urlParamToken, String cookieToken, String headerToken, String expectedToken) {
when(request.getResourcePath()).thenReturn(pathToken);
when(request.getAdditionalParameter("tokenId")).thenReturn(urlParamToken);
when(context.getHeader("cookie")).thenReturn(Collections.singletonList("iPlanetDirectoryPro=" + cookieToken));
when(context.getHeader("iPlanetDirectoryPro")).thenReturn(Collections.singletonList(headerToken));

String tokenId = SessionResourceUtil.getTokenId(context, request);

assertEquals(tokenId, expectedToken);
}

@DataProvider(name = "tokenDataProvider")
public String[][] tokenDataProvider() {
return new String[][] {
{"tokenIdFromPath", "tokenIdFromUrlParam", "tokenIdFromCookie", "tokenIdFromHeader", "tokenIdFromPath"},
{"", "tokenIdFromUrlParam", "tokenIdFromCookie", "tokenIdFromHeader", "tokenIdFromUrlParam"},
{"", "", "tokenIdFromCookie", "tokenIdFromHeader", "tokenIdFromCookie"},
{"", "", "", "tokenIdFromHeader", "tokenIdFromHeader"}
};
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2015-2016 ForgeRock AS.
*/
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2015-2016 ForgeRock AS.
* Portions copyright 2024 Wren Security.
*/
package org.forgerock.openam.core.rest.session;

import static org.forgerock.util.test.assertj.AssertJPromiseAssert.*;
Expand Down Expand Up @@ -64,7 +65,7 @@ public void theSetUp() throws SSOException {
given(mockConfig.get()).willReturn(mockService);
mockContext = setupUser("universal_id");

testModule = new TokenOwnerAuthzModule("tokenId", mockTokenManager, "deleteProperty");
testModule = new TokenOwnerAuthzModule(mockTokenManager, "deleteProperty");
}

@Test
Expand Down

0 comments on commit 0990980

Please sign in to comment.