VYPR
High severity8.1NVD Advisory· Published Feb 9, 2026· Updated Apr 15, 2026

CVE-2026-1529

CVE-2026-1529

Description

A flaw was found in Keycloak. An attacker can exploit this vulnerability by modifying the organization ID and target email within a legitimate invitation token's JSON Web Token (JWT) payload. This lack of cryptographic signature verification allows the attacker to successfully self-register into an unauthorized organization, leading to unauthorized access.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.keycloak:keycloak-servicesMaven
>= 26.5.0, < 26.5.326.5.3
org.keycloak:keycloak-servicesMaven
< 26.2.1326.2.13
org.keycloak:keycloak-servicesMaven
>= 26.3.0, < 26.4.926.4.9

Patches

3
8fc9a9802610

Make sure registration tokens are verified before processing registration (#46155)

https://github.com/keycloak/keycloakPedro IgorFeb 10, 2026via ghsa
6 files changed · +98 23
  • services/src/main/java/org/keycloak/authentication/actiontoken/inviteorg/InviteOrgActionTokenHandler.java+15 3 modified
    @@ -84,15 +84,24 @@ public Response preHandleToken(InviteOrgActionToken token, ActionTokenContext<In
                 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 @@ private Response invalidTokenResponse(ActionTokenContext<InviteOrgActionToken> t
                     .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 @@ private Response invalidOrganizationResponse(ActionTokenContext<InviteOrgActionT
                     .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 @@ private Response alreadyMemberResponse(OrganizationModel organization, UserModel
                     .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())
    
  • services/src/main/java/org/keycloak/authentication/forms/RegistrationPage.java+1 1 modified
    @@ -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();
    
  • services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java+1 1 modified
    @@ -300,7 +300,7 @@ private boolean validateOrganizationInvitation(ValidationContext context, Multiv
                 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;
    
  • services/src/main/java/org/keycloak/organization/utils/Organizations.java+14 2 modified
    @@ -32,6 +32,8 @@
     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.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,15 +156,24 @@ public static void checkEnabled(OrganizationProvider provider) {
             }
         }
     
    -    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);
     
             if (tokenFromQuery == null) {
                 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) {
    
  • services/src/main/java/org/keycloak/services/resources/LoginActionsService.java+23 16 modified
    @@ -657,6 +657,11 @@ protected <T extends JsonWebToken & SingleUseObjectKeyModel> Response handleActi
             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 Response registerPage(@QueryParam(AUTH_SESSION_ID) String authSessionId,
                                      @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 Response processRegister(@QueryParam(AUTH_SESSION_ID) String authSessionI
                                         @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 @@ private Response registerRequest(String authSessionId, String code, String execu
     
             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);
         }
     
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java+44 0 modified
    @@ -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 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.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.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 void testInviteNewUserRegistration() throws IOException, MessagingExcepti
             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";
    
82cd7941d1dd

Make sure registration tokens are verified before processing registration

https://github.com/keycloak/keycloakStian ThorgersenJan 29, 2026via ghsa
6 files changed · +144 47
  • services/src/main/java/org/keycloak/authentication/actiontoken/inviteorg/InviteOrgActionTokenHandler.java+60 27 modified
    @@ -104,40 +104,18 @@ public Response handleToken(InviteOrgActionToken token, ActionTokenContext<Invit
             OrganizationModel organization = orgProvider.getById(token.getOrgId());
     
             if (organization == null) {
    -            event.user(user).error(Errors.ORG_NOT_FOUND);
    -            return session.getProvider(LoginFormsProvider.class)
    -                    .setAuthenticationSession(authSession)
    -                    .setInfo(Messages.ORG_NOT_FOUND, token.getOrgId())
    -                    .createInfoPage();
    +            return invalidOrganizationResponse(tokenContext, token);
             }
     
             if (organization.isMember(user)) {
    -            event.user(user).error(Errors.USER_ORG_MEMBER_ALREADY);
    -            return session.getProvider(LoginFormsProvider.class)
    -                    .setAuthenticationSession(authSession)
    -                    .setInfo(Messages.ORG_MEMBER_ALREADY, user.getUsername())
    -                    .setAttribute("pageRedirectUri", organization.getRedirectUrl())
    -                    .createInfoPage();
    +            return alreadyMemberResponse(organization, user, tokenContext, token);
             }
     
    -        final UriInfo uriInfo = tokenContext.getUriInfo();
    -        final RealmModel realm = tokenContext.getRealm();
    +        UriInfo uriInfo = tokenContext.getUriInfo();
    +        RealmModel realm = tokenContext.getRealm();
     
             if (tokenContext.isAuthenticationSessionFresh()) {
    -            // Update the authentication session in the token
    -            String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
    -            token.setCompoundAuthenticationSessionId(authSessionEncodedId);
    -            UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
    -                    authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession));
    -            String confirmUri = builder.build(realm.getName()).toString();
    -
    -            return session.getProvider(LoginFormsProvider.class)
    -                    .setAuthenticationSession(authSession)
    -                    .setSuccess(Messages.CONFIRM_ORGANIZATION_MEMBERSHIP, organization.getName())
    -                    .setAttribute("messageHeader", Messages.CONFIRM_ORGANIZATION_MEMBERSHIP_TITLE)
    -                    .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
    -                    .setAttribute(OrganizationModel.ORGANIZATION_NAME_ATTRIBUTE, organization.getName())
    -                    .createInfoPage();
    +            return confirmMembershipResponse(organization, user, tokenContext, token);
             }
     
             // if we made it this far then go ahead and add the user to the organization
    @@ -169,4 +147,59 @@ public Response handleToken(InviteOrgActionToken token, ActionTokenContext<Invit
     
             return AuthenticationManager.redirectToRequiredActions(session, realm, authSession, uriInfo, nextAction);
         }
    +
    +    private Response invalidOrganizationResponse(ActionTokenContext<InviteOrgActionToken> tokenContext, InviteOrgActionToken token) {
    +        EventBuilder event = tokenContext.getEvent();
    +        KeycloakSession session = tokenContext.getSession();
    +        AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
    +
    +        event.detail(Details.TOKEN_ID, token.getId())
    +                .detail(Details.EMAIL, token.getEmail())
    +                .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())
    +                .createInfoPage();
    +    }
    +
    +    private Response alreadyMemberResponse(OrganizationModel organization, UserModel user, ActionTokenContext<InviteOrgActionToken> tokenContext, InviteOrgActionToken token) {
    +        EventBuilder event = tokenContext.getEvent();
    +        KeycloakSession session = tokenContext.getSession();
    +        AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
    +
    +        event.detail(Details.TOKEN_ID, token.getId())
    +                .detail(Details.EMAIL, token.getEmail())
    +                .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())
    +                .setAttribute("pageRedirectUri", organization.getRedirectUrl())
    +                .createInfoPage();
    +    }
    +
    +    private Response confirmMembershipResponse(OrganizationModel organization, UserModel user, ActionTokenContext<InviteOrgActionToken> tokenContext, InviteOrgActionToken token) {
    +        KeycloakSession session = tokenContext.getSession();
    +        AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
    +        UriInfo uriInfo = tokenContext.getUriInfo();
    +        String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
    +        token.setCompoundAuthenticationSessionId(authSessionEncodedId);
    +        RealmModel realm = tokenContext.getRealm();
    +        UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
    +                authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession));
    +        String confirmUri = builder.build(realm.getName()).toString();
    +
    +        return session.getProvider(LoginFormsProvider.class)
    +                .setAuthenticationSession(authSession)
    +                .setSuccess(Messages.CONFIRM_ORGANIZATION_MEMBERSHIP, organization.getName())
    +                .setAttribute("messageHeader", Messages.CONFIRM_ORGANIZATION_MEMBERSHIP_TITLE)
    +                .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
    +                .setAttribute(OrganizationModel.ORGANIZATION_NAME_ATTRIBUTE, organization.getName())
    +                .createInfoPage();
    +    }
     }
    
  • services/src/main/java/org/keycloak/authentication/forms/RegistrationPage.java+1 1 modified
    @@ -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();
    
  • services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java+1 1 modified
    @@ -297,7 +297,7 @@ private boolean validateOrganizationInvitation(ValidationContext context, Multiv
                 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;
    
  • services/src/main/java/org/keycloak/organization/utils/Organizations.java+14 2 modified
    @@ -35,6 +35,8 @@
     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;
    @@ -50,6 +52,7 @@
     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;
     
     public class Organizations {
    @@ -148,15 +151,24 @@ public static void checkEnabled(OrganizationProvider provider) {
             }
         }
     
    -    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);
     
             if (tokenFromQuery == null) {
                 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) {
    
  • services/src/main/java/org/keycloak/services/resources/LoginActionsService.java+23 16 modified
    @@ -655,6 +655,11 @@ protected <T extends JsonWebToken & SingleUseObjectKeyModel> Response handleActi
             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);
             }
     
    @@ -776,11 +781,7 @@ public Response registerPage(@QueryParam(AUTH_SESSION_ID) String authSessionId,
                                      @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);
         }
     
     
    @@ -799,21 +800,12 @@ public Response processRegister(@QueryParam(AUTH_SESSION_ID) String authSessionI
                                         @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)) {
    @@ -822,10 +814,25 @@ private Response registerRequest(String authSessionId, String code, String execu
     
             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);
         }
     
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java+45 0 modified
    @@ -22,9 +22,12 @@
     import static org.hamcrest.Matchers.empty;
     import static org.hamcrest.Matchers.equalTo;
     import static org.hamcrest.Matchers.is;
    +import static org.hamcrest.Matchers.notNullValue;
     import static org.keycloak.services.messages.Messages.ORG_MEMBER_ALREADY;
     
     import java.io.IOException;
    +import java.net.URI;
    +import java.nio.charset.StandardCharsets;
     import java.util.Arrays;
     import java.util.HashMap;
     import java.util.List;
    @@ -37,6 +40,10 @@
     import jakarta.mail.internet.MimeMessage;
     import jakarta.ws.rs.core.Response;
     import java.time.Duration;
    +
    +import jakarta.ws.rs.core.UriBuilder;
    +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;
    @@ -45,6 +52,8 @@
     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.utils.DefaultAuthenticationFlows;
     import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
    @@ -66,6 +75,7 @@
     import org.keycloak.testsuite.util.MailUtils.EmailBody;
     import org.keycloak.testsuite.util.oauth.OAuthClient;
     import org.keycloak.testsuite.util.UserBuilder;
    +import org.keycloak.util.JsonSerialization;
     import org.openqa.selenium.By;
     
     public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
    @@ -205,6 +215,41 @@ public void testInviteNewUserRegistration() throws IOException, MessagingExcepti
             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";
    
b2519756487b

Make sure registration tokens are verified before processing registration

https://github.com/keycloak/keycloakStian ThorgersenJan 29, 2026via ghsa
6 files changed · +145 47
  • services/src/main/java/org/keycloak/authentication/actiontoken/inviteorg/InviteOrgActionTokenHandler.java+60 27 modified
    @@ -104,40 +104,18 @@ public Response handleToken(InviteOrgActionToken token, ActionTokenContext<Invit
             OrganizationModel organization = orgProvider.getById(token.getOrgId());
     
             if (organization == null) {
    -            event.user(user).error(Errors.ORG_NOT_FOUND);
    -            return session.getProvider(LoginFormsProvider.class)
    -                    .setAuthenticationSession(authSession)
    -                    .setInfo(Messages.ORG_NOT_FOUND, token.getOrgId())
    -                    .createInfoPage();
    +            return invalidOrganizationResponse(tokenContext, token);
             }
     
             if (organization.isMember(user)) {
    -            event.user(user).error(Errors.USER_ORG_MEMBER_ALREADY);
    -            return session.getProvider(LoginFormsProvider.class)
    -                    .setAuthenticationSession(authSession)
    -                    .setInfo(Messages.ORG_MEMBER_ALREADY, user.getUsername())
    -                    .setAttribute("pageRedirectUri", organization.getRedirectUrl())
    -                    .createInfoPage();
    +            return alreadyMemberResponse(organization, user, tokenContext, token);
             }
     
    -        final UriInfo uriInfo = tokenContext.getUriInfo();
    -        final RealmModel realm = tokenContext.getRealm();
    +        UriInfo uriInfo = tokenContext.getUriInfo();
    +        RealmModel realm = tokenContext.getRealm();
     
             if (tokenContext.isAuthenticationSessionFresh()) {
    -            // Update the authentication session in the token
    -            String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
    -            token.setCompoundAuthenticationSessionId(authSessionEncodedId);
    -            UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
    -                    authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession));
    -            String confirmUri = builder.build(realm.getName()).toString();
    -
    -            return session.getProvider(LoginFormsProvider.class)
    -                    .setAuthenticationSession(authSession)
    -                    .setSuccess(Messages.CONFIRM_ORGANIZATION_MEMBERSHIP, organization.getName())
    -                    .setAttribute("messageHeader", Messages.CONFIRM_ORGANIZATION_MEMBERSHIP_TITLE)
    -                    .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
    -                    .setAttribute(OrganizationModel.ORGANIZATION_NAME_ATTRIBUTE, organization.getName())
    -                    .createInfoPage();
    +            return confirmMembershipResponse(organization, user, tokenContext, token);
             }
     
             // if we made it this far then go ahead and add the user to the organization
    @@ -169,4 +147,59 @@ public Response handleToken(InviteOrgActionToken token, ActionTokenContext<Invit
     
             return AuthenticationManager.redirectToRequiredActions(session, realm, authSession, uriInfo, nextAction);
         }
    +
    +    private Response invalidOrganizationResponse(ActionTokenContext<InviteOrgActionToken> tokenContext, InviteOrgActionToken token) {
    +        EventBuilder event = tokenContext.getEvent();
    +        KeycloakSession session = tokenContext.getSession();
    +        AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
    +
    +        event.detail(Details.TOKEN_ID, token.getId())
    +                .detail(Details.EMAIL, token.getEmail())
    +                .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())
    +                .createInfoPage();
    +    }
    +
    +    private Response alreadyMemberResponse(OrganizationModel organization, UserModel user, ActionTokenContext<InviteOrgActionToken> tokenContext, InviteOrgActionToken token) {
    +        EventBuilder event = tokenContext.getEvent();
    +        KeycloakSession session = tokenContext.getSession();
    +        AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
    +
    +        event.detail(Details.TOKEN_ID, token.getId())
    +                .detail(Details.EMAIL, token.getEmail())
    +                .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())
    +                .setAttribute("pageRedirectUri", organization.getRedirectUrl())
    +                .createInfoPage();
    +    }
    +
    +    private Response confirmMembershipResponse(OrganizationModel organization, UserModel user, ActionTokenContext<InviteOrgActionToken> tokenContext, InviteOrgActionToken token) {
    +        KeycloakSession session = tokenContext.getSession();
    +        AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
    +        UriInfo uriInfo = tokenContext.getUriInfo();
    +        String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
    +        token.setCompoundAuthenticationSessionId(authSessionEncodedId);
    +        RealmModel realm = tokenContext.getRealm();
    +        UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
    +                authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession));
    +        String confirmUri = builder.build(realm.getName()).toString();
    +
    +        return session.getProvider(LoginFormsProvider.class)
    +                .setAuthenticationSession(authSession)
    +                .setSuccess(Messages.CONFIRM_ORGANIZATION_MEMBERSHIP, organization.getName())
    +                .setAttribute("messageHeader", Messages.CONFIRM_ORGANIZATION_MEMBERSHIP_TITLE)
    +                .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
    +                .setAttribute(OrganizationModel.ORGANIZATION_NAME_ATTRIBUTE, organization.getName())
    +                .createInfoPage();
    +    }
     }
    
  • services/src/main/java/org/keycloak/authentication/forms/RegistrationPage.java+1 1 modified
    @@ -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();
    
  • services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java+1 1 modified
    @@ -297,7 +297,7 @@ private boolean validateOrganizationInvitation(ValidationContext context, Multiv
                 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;
    
  • services/src/main/java/org/keycloak/organization/utils/Organizations.java+15 2 modified
    @@ -35,12 +35,15 @@
     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;
     import org.keycloak.models.GroupModel;
     import org.keycloak.models.GroupModel.Type;
     import org.keycloak.models.IdentityProviderModel;
    +import org.keycloak.models.KeycloakContext;
     import org.keycloak.models.KeycloakSession;
     import org.keycloak.models.OrganizationDomainModel;
     import org.keycloak.models.OrganizationModel;
    @@ -49,6 +52,7 @@
     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;
     
     public class Organizations {
    @@ -147,15 +151,24 @@ public static void checkEnabled(OrganizationProvider provider) {
             }
         }
     
    -    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);
     
             if (tokenFromQuery == null) {
                 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) {
    
  • services/src/main/java/org/keycloak/services/resources/LoginActionsService.java+23 16 modified
    @@ -641,6 +641,11 @@ protected <T extends JsonWebToken & SingleUseObjectKeyModel> Response handleActi
             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);
             }
     
    @@ -762,11 +767,7 @@ public Response registerPage(@QueryParam(AUTH_SESSION_ID) String authSessionId,
                                      @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);
         }
     
     
    @@ -785,21 +786,12 @@ public Response processRegister(@QueryParam(AUTH_SESSION_ID) String authSessionI
                                         @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)) {
    @@ -808,10 +800,25 @@ private Response registerRequest(String authSessionId, String code, String execu
     
             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);
         }
     
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java+45 0 modified
    @@ -22,9 +22,12 @@
     import static org.hamcrest.Matchers.empty;
     import static org.hamcrest.Matchers.equalTo;
     import static org.hamcrest.Matchers.is;
    +import static org.hamcrest.Matchers.notNullValue;
     import static org.keycloak.services.messages.Messages.ORG_MEMBER_ALREADY;
     
     import java.io.IOException;
    +import java.net.URI;
    +import java.nio.charset.StandardCharsets;
     import java.util.Arrays;
     import java.util.HashMap;
     import java.util.List;
    @@ -37,6 +40,10 @@
     import jakarta.mail.internet.MimeMessage;
     import jakarta.ws.rs.core.Response;
     import java.time.Duration;
    +
    +import jakarta.ws.rs.core.UriBuilder;
    +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;
    @@ -45,6 +52,8 @@
     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.utils.DefaultAuthenticationFlows;
     import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
    @@ -66,6 +75,7 @@
     import org.keycloak.testsuite.util.MailUtils.EmailBody;
     import org.keycloak.testsuite.util.oauth.OAuthClient;
     import org.keycloak.testsuite.util.UserBuilder;
    +import org.keycloak.util.JsonSerialization;
     import org.openqa.selenium.By;
     
     public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
    @@ -205,6 +215,41 @@ public void testInviteNewUserRegistration() throws IOException, MessagingExcepti
             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";
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

13

News mentions

0

No linked articles in our index yet.