mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-18 18:37:54 -05:00
Make sure registration tokens are verified before processing registration (#46155)
Closes #46145 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
64dee82f9f
commit
8fc9a98026
6 changed files with 98 additions and 23 deletions
|
|
@ -84,15 +84,24 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
|
|||
return invalidOrganizationResponse(tokenContext, token);
|
||||
}
|
||||
|
||||
session.getContext().setOrganization(organization);
|
||||
|
||||
InvitationManager invitationManager = orgProvider.getInvitationManager();
|
||||
OrganizationInvitationModel invitation = invitationManager.getById(token.getId());
|
||||
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
|
||||
|
||||
if (invitation == null || invitation.isExpired()) {
|
||||
return invalidTokenResponse(tokenContext, token);
|
||||
String orgId = authSession == null ? null : authSession.getAuthNote(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||
|
||||
if (orgId == null || !orgId.equals(token.getOrgId())) {
|
||||
return invalidTokenResponse(tokenContext, token);
|
||||
}
|
||||
}
|
||||
|
||||
if (authSession != null) {
|
||||
authSession.setAuthNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId());
|
||||
}
|
||||
|
||||
session.getContext().setOrganization(organization);
|
||||
|
||||
return super.preHandleToken(token, tokenContext);
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +182,7 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
|
|||
.detail(Details.ORG_ID, token.getOrgId())
|
||||
.error(Errors.INVALID_TOKEN);
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
.setStatus(Status.BAD_REQUEST)
|
||||
.setAuthenticationSession(authSession)
|
||||
.setAttribute("messageHeader", Messages.EXPIRED_ACTION)
|
||||
.setInfo(Messages.STALE_INVITE_ORG_LINK)
|
||||
|
|
@ -189,6 +199,7 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
|
|||
.detail(Details.ORG_ID, token.getOrgId())
|
||||
.error(Errors.ORG_NOT_FOUND);
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
.setStatus(Status.BAD_REQUEST)
|
||||
.setAuthenticationSession(authSession)
|
||||
.setAttribute("messageHeader", Messages.EXPIRED_ACTION)
|
||||
.setInfo(Messages.ORG_NOT_FOUND, token.getOrgId())
|
||||
|
|
@ -205,6 +216,7 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
|
|||
.detail(Details.ORG_ID, token.getOrgId())
|
||||
.error(Errors.USER_ORG_MEMBER_ALREADY);
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
.setStatus(Status.BAD_REQUEST)
|
||||
.setAuthenticationSession(authSession)
|
||||
.setAttribute("messageHeader", Messages.EXPIRED_ACTION)
|
||||
.setInfo(Messages.ORG_MEMBER_ALREADY, user.getUsername(), organization.getName())
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ public class RegistrationPage implements FormAuthenticator, FormAuthenticatorFac
|
|||
public Response render(FormContext context, LoginFormsProvider form) {
|
||||
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||
try {
|
||||
InviteOrgActionToken token = Organizations.parseInvitationToken(context.getHttpRequest());
|
||||
InviteOrgActionToken token = Organizations.parseInvitationToken(context.getSession(), context.getHttpRequest());
|
||||
|
||||
if (token != null) {
|
||||
KeycloakSession session = context.getSession();
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
|
|||
InviteOrgActionToken token;
|
||||
|
||||
try {
|
||||
token = Organizations.parseInvitationToken(context.getHttpRequest());
|
||||
token = Organizations.parseInvitationToken(context.getSession(), context.getHttpRequest());
|
||||
} catch (VerificationException e) {
|
||||
error.accept(List.of(new FormMessage("Unexpected error parsing the invitation token")));
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ import org.keycloak.authentication.actiontoken.inviteorg.InviteOrgActionToken;
|
|||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.crypto.SignatureProvider;
|
||||
import org.keycloak.crypto.SignatureVerifierContext;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
|
|
@ -47,6 +49,7 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.organization.OrganizationProvider;
|
||||
import org.keycloak.organization.protocol.mappers.oidc.OrganizationScope;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
import static java.util.Optional.of;
|
||||
|
|
@ -153,7 +156,7 @@ public class Organizations {
|
|||
}
|
||||
}
|
||||
|
||||
public static InviteOrgActionToken parseInvitationToken(HttpRequest request) throws VerificationException {
|
||||
public static InviteOrgActionToken parseInvitationToken(KeycloakSession session, HttpRequest request) throws VerificationException {
|
||||
MultivaluedMap<String, String> queryParameters = request.getUri().getQueryParameters();
|
||||
String tokenFromQuery = queryParameters.getFirst(Constants.TOKEN);
|
||||
|
||||
|
|
@ -161,7 +164,16 @@ public class Organizations {
|
|||
return null;
|
||||
}
|
||||
|
||||
return TokenVerifier.create(tokenFromQuery, InviteOrgActionToken.class).getToken();
|
||||
KeycloakContext context = session.getContext();
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
TokenVerifier<InviteOrgActionToken> verifier = TokenVerifier.create(tokenFromQuery, InviteOrgActionToken.class)
|
||||
.withChecks(TokenVerifier.IS_ACTIVE,
|
||||
new TokenVerifier.RealmUrlCheck(Urls.realmIssuer(context.getUri().getBaseUri(), realm.getName())));
|
||||
|
||||
SignatureVerifierContext verifierContext = session.getProvider(SignatureProvider.class, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId());
|
||||
verifier.verifierContext(verifierContext);
|
||||
|
||||
return verifier.verify().getToken();
|
||||
}
|
||||
|
||||
public static String getEmailDomain(String email) {
|
||||
|
|
|
|||
|
|
@ -657,6 +657,11 @@ public class LoginActionsService {
|
|||
tokenContext = new ActionTokenContext<>(session, realm, sessionContext.getUri(), clientConnection, request, event, handler, execution, clientData, this::processFlow, this::brokerLoginFlow);
|
||||
|
||||
if (preHandleToken != null) {
|
||||
KeycloakContext context = session.getContext();
|
||||
authSession = context.getAuthenticationSession();
|
||||
if (authSession != null) {
|
||||
tokenContext.setAuthenticationSession(authSession, false);
|
||||
}
|
||||
return preHandleToken.apply(handler, token, tokenContext);
|
||||
}
|
||||
|
||||
|
|
@ -778,11 +783,7 @@ public class LoginActionsService {
|
|||
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||
@QueryParam(Constants.TAB_ID) String tabId,
|
||||
@QueryParam(Constants.TOKEN) String tokenString) {
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION) && tokenString != null) {
|
||||
//this call should extract orgId from token and set the organization to the session context
|
||||
preHandleActionToken(tokenString);
|
||||
}
|
||||
return registerRequest(authSessionId, code, execution, clientId, tabId,clientData);
|
||||
return registerRequest(authSessionId, code, execution, clientId, tabId,clientData, tokenString);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -801,21 +802,12 @@ public class LoginActionsService {
|
|||
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||
@QueryParam(Constants.TAB_ID) String tabId,
|
||||
@QueryParam(Constants.TOKEN) String tokenString) {
|
||||
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION) && tokenString != null) {
|
||||
//this call should extract orgId from token and set the organization to the session context
|
||||
preHandleActionToken(tokenString);
|
||||
}
|
||||
return registerRequest(authSessionId, code, execution, clientId, tabId, clientData);
|
||||
return registerRequest(authSessionId, code, execution, clientId, tabId, clientData, tokenString);
|
||||
}
|
||||
|
||||
|
||||
private Response registerRequest(String authSessionId, String code, String execution, String clientId, String tabId, String clientData) {
|
||||
private Response registerRequest(String authSessionId, String code, String execution, String clientId, String tabId, String clientData, String tokenString) {
|
||||
event.event(EventType.REGISTER);
|
||||
if (!Organizations.isRegistrationAllowed(session, realm)) {
|
||||
event.error(Errors.REGISTRATION_DISABLED);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REGISTRATION_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, clientData, REGISTRATION_PATH);
|
||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
|
|
@ -824,10 +816,25 @@ public class LoginActionsService {
|
|||
|
||||
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
|
||||
|
||||
session.getContext().setAuthenticationSession(authSession);
|
||||
|
||||
processLocaleParam(authSession);
|
||||
|
||||
AuthenticationManager.expireIdentityCookie(session);
|
||||
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION) && tokenString != null) {
|
||||
//this call should extract orgId from token and set the organization to the session context
|
||||
Response response = preHandleActionToken(tokenString);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Organizations.isRegistrationAllowed(session, realm)) {
|
||||
event.error(Errors.REGISTRATION_DISABLED);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REGISTRATION_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
return processRegistration(checks.isActionRequest(), execution, authSession, null);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
package org.keycloak.testsuite.organization.admin;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
|
@ -30,10 +32,13 @@ import java.util.function.Predicate;
|
|||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||
import org.keycloak.common.util.UriUtils;
|
||||
import org.keycloak.cookie.CookieType;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||
|
|
@ -57,7 +62,10 @@ import org.keycloak.testsuite.util.MailUtils;
|
|||
import org.keycloak.testsuite.util.MailUtils.EmailBody;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.OAuthClient;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.utils.URLEncodedUtils;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Before;
|
||||
|
|
@ -74,6 +82,7 @@ import static org.hamcrest.Matchers.empty;
|
|||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
|
||||
public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||
|
||||
|
|
@ -259,6 +268,41 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
|||
Assert.assertNotNull(driver.manage().getCookieNamed(CookieType.IDENTITY.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegistrationUsingRegistrationEndpoint() throws Exception {
|
||||
String email = "inviteduser@email";
|
||||
String firstName = "Homer";
|
||||
String lastName = "Simpson";
|
||||
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
organization.members().inviteUser(email, firstName, lastName).close();
|
||||
|
||||
URI link = URI.create(getInvitationLinkFromEmail());
|
||||
NameValuePair tokenParam = URLEncodedUtils.parse(link, StandardCharsets.UTF_8).stream()
|
||||
.filter((np) -> "token".equals(np.getName()))
|
||||
.findAny().orElse(null);
|
||||
assertThat(tokenParam, notNullValue());
|
||||
JWSInput token = new JWSInput(tokenParam.getValue());
|
||||
Map<String, String> tokenClaims = token.readJsonContent(Map.class);
|
||||
tokenClaims.put("email", "myemail@some.org");
|
||||
String modifiedToken = new JWSBuilder().content(JsonSerialization.writeValueAsString(tokenClaims).getBytes(StandardCharsets.UTF_8)).none();
|
||||
modifiedToken = token.getEncodedHeader() + modifiedToken.substring(modifiedToken.indexOf('.'));
|
||||
modifiedToken = modifiedToken + token.getEncodedSignature();
|
||||
|
||||
RealmRepresentation realm = testRealm().toRepresentation();
|
||||
realm.setRegistrationAllowed(true);
|
||||
testRealm().update(realm);
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(realm.getRealm());
|
||||
loginPage.clickRegister();
|
||||
registerPage.assertCurrent();
|
||||
String registerUrl = UriBuilder.fromUri(driver.getCurrentUrl())
|
||||
.queryParam("token", modifiedToken)
|
||||
.build().toString();
|
||||
driver.navigate().to(registerUrl);
|
||||
errorPage.assertCurrent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInviteNewUserRegistrationCustomRegistrationFlow() throws IOException, MessagingException {
|
||||
String registrationFlowAlias = "custom-registration-flow";
|
||||
|
|
|
|||
Loading…
Reference in a new issue