Skip to content

Commit ec81091

Browse files
stianstpedroigor
andauthored
Make sure the code is bound to the user session (#18) (#17380)
Co-authored-by: Pedro Igor <pigor.craveiro@gmail.com>
1 parent 008b73d commit ec81091

File tree

4 files changed

+51
-5
lines changed

4 files changed

+51
-5
lines changed

services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ public Response authenticated(AuthenticationSessionModel authSession, UserSessio
241241
authSession.getClientNote(OAuth2Constants.SCOPE),
242242
authSession.getClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM),
243243
authSession.getClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM),
244-
authSession.getClientNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM));
244+
authSession.getClientNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM),
245+
userSession.getId());
245246

246247
code = OAuth2CodeParser.persistCode(session, clientSession, codeData);
247248
redirectUri.addParam(OAuth2Constants.CODE, code);

services/src/main/java/org/keycloak/protocol/oidc/utils/OAuth2Code.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class OAuth2Code {
3838
private static final String REDIRECT_URI_PARAM_NOTE = "redirectUri";
3939
private static final String CODE_CHALLENGE_NOTE = "code_challenge";
4040
private static final String CODE_CHALLENGE_METHOD_NOTE = "code_challenge_method";
41+
private static final String USER_SESSION_ID_NOTE = "user_session_id";
4142

4243
private final String id;
4344

@@ -52,17 +53,18 @@ public class OAuth2Code {
5253
private final String codeChallenge;
5354

5455
private final String codeChallengeMethod;
55-
56+
private final String userSessionId;
5657

5758
public OAuth2Code(String id, int expiration, String nonce, String scope, String redirectUriParam,
58-
String codeChallenge, String codeChallengeMethod) {
59+
String codeChallenge, String codeChallengeMethod, String userSessionId) {
5960
this.id = id;
6061
this.expiration = expiration;
6162
this.nonce = nonce;
6263
this.scope = scope;
6364
this.redirectUriParam = redirectUriParam;
6465
this.codeChallenge = codeChallenge;
6566
this.codeChallengeMethod = codeChallengeMethod;
67+
this.userSessionId = userSessionId;
6668
}
6769

6870

@@ -74,6 +76,7 @@ private OAuth2Code(Map<String, String> data) {
7476
redirectUriParam = data.get(REDIRECT_URI_PARAM_NOTE);
7577
codeChallenge = data.get(CODE_CHALLENGE_NOTE);
7678
codeChallengeMethod = data.get(CODE_CHALLENGE_METHOD_NOTE);
79+
userSessionId = data.get(USER_SESSION_ID_NOTE);
7780
}
7881

7982

@@ -92,6 +95,7 @@ public Map<String, String> serializeCode() {
9295
result.put(REDIRECT_URI_PARAM_NOTE, redirectUriParam);
9396
result.put(CODE_CHALLENGE_NOTE, codeChallenge);
9497
result.put(CODE_CHALLENGE_METHOD_NOTE, codeChallengeMethod);
98+
result.put(USER_SESSION_ID_NOTE, userSessionId);
9599

96100
return result;
97101
}
@@ -124,4 +128,8 @@ public String getCodeChallenge() {
124128
public String getCodeChallengeMethod() {
125129
return codeChallengeMethod;
126130
}
131+
132+
public String getUserSessionId() {
133+
return userSessionId;
134+
}
127135
}

services/src/main/java/org/keycloak/protocol/oidc/utils/OAuth2CodeParser.java

+9-2
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,23 @@ public static ParseResult parseCode(KeycloakSession session, String code, RealmM
121121
return result.illegalCode();
122122
}
123123

124-
logger.tracef("Successfully verified code '%s'. User session: '%s', client: '%s'", codeUUID, userSessionId, clientUUID);
125-
126124
result.codeData = OAuth2Code.deserializeCode(codeData);
127125

126+
String persistedUserSessionId = result.codeData.getUserSessionId();
127+
128+
if (!userSessionId.equals(persistedUserSessionId)) {
129+
logger.warnf("Code '%s' is bound to a different session", codeUUID);
130+
return result.illegalCode();
131+
}
132+
128133
// Finally doublecheck if code is not expired
129134
int currentTime = Time.currentTime();
130135
if (currentTime > result.codeData.getExpiration()) {
131136
return result.expiredCode();
132137
}
133138

139+
logger.tracef("Successfully verified code '%s'. User session: '%s', client: '%s'", codeUUID, userSessionId, clientUUID);
140+
134141
return result;
135142
}
136143

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java

+30
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.keycloak.testsuite.forms;
1818

19+
import org.apache.http.impl.client.CloseableHttpClient;
1920
import org.jboss.arquillian.drone.api.annotation.Drone;
2021
import org.jboss.arquillian.graphene.page.Page;
2122
import org.junit.Assert;
@@ -39,13 +40,17 @@
3940
import org.keycloak.testsuite.pages.AppPage.RequestType;
4041
import org.keycloak.testsuite.pages.LoginPage;
4142
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
43+
import org.keycloak.testsuite.util.MutualTLSUtils;
4244
import org.keycloak.testsuite.util.OAuthClient;
4345
import org.openqa.selenium.WebDriver;
4446

4547
import static org.junit.Assert.assertEquals;
4648
import static org.junit.Assert.assertNotEquals;
4749
import static org.junit.Assert.assertTrue;
4850

51+
import java.io.IOException;
52+
import javax.ws.rs.core.Response;
53+
4954
/**
5055
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
5156
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
@@ -209,4 +214,29 @@ public void loginWithRequiredActionAddedInTheMeantime() {
209214

210215
}
211216

217+
@Test
218+
public void failIfUsingCodeFromADifferentSession() throws IOException {
219+
// first client user login
220+
oauth.openLoginForm();
221+
oauth.doLogin("test-user@localhost", "password");
222+
String firstCode = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
223+
224+
// second client user login
225+
OAuthClient oauth2 = new OAuthClient();
226+
oauth2.init(driver2);
227+
oauth2.doLogin("john-doh@localhost", "password");
228+
String secondCode = oauth2.getCurrentQuery().get(OAuth2Constants.CODE);
229+
String[] firstCodeParts = firstCode.split("\\.");
230+
String[] secondCodeParts = secondCode.split("\\.");
231+
secondCodeParts[1] = firstCodeParts[1];
232+
secondCode = String.join(".", secondCodeParts);
233+
234+
OAuthClient.AccessTokenResponse tokenResponse;
235+
236+
try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithOtherKeyStoreAndTrustStore()) {
237+
tokenResponse = oauth2.doAccessTokenRequest(secondCode, "password", client);
238+
}
239+
240+
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), tokenResponse.getStatusCode());
241+
}
212242
}

0 commit comments

Comments
 (0)