Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fe3cf3a
Initial support for Shared Signals Framework
thomasdarimont Oct 20, 2025
4dbd134
Next iteration of SSF support
thomasdarimont Nov 4, 2025
254dc42
WIP Next iteration of SSF support
thomasdarimont Nov 7, 2025
1b93bda
WIP Next iteration of SSF support
thomasdarimont Nov 9, 2025
b8d6428
WIP Refactoring
thomasdarimont Nov 9, 2025
2c24196
WIP Refactoring
thomasdarimont Nov 9, 2025
15a2093
Make spotless happy
thomasdarimont Nov 24, 2025
8bedd02
Revise SSF Receiver support for review
thomasdarimont Nov 27, 2025
d285038
Make it more explicit that this is only about SSF Receivers
thomasdarimont Nov 27, 2025
f5443a3
Use password controls for push auth header and transmitter access token
thomasdarimont Jan 30, 2026
ed36e6a
Add HttpServerUtil helper to ease return HttpServer responses
thomasdarimont Jan 31, 2026
75bdfcf
Refactor SSF Receiver support
thomasdarimont Jan 31, 2026
7b17474
Add initial SsfReceiverTests
thomasdarimont Jan 31, 2026
18c0067
Revise DefaultSsfEventListener
thomasdarimont Feb 1, 2026
920b043
Next iteration of SSF Receiver support
thomasdarimont Feb 12, 2026
c0dc513
Revise SSF Receiver Provider configuration UI
thomasdarimont Feb 12, 2026
dda6865
Split SSF Receiver Provider configuration UI in general / stream sett…
thomasdarimont Feb 12, 2026
2011f85
Split SSF Receiver Provider configuration UI in general / stream sett…
thomasdarimont Feb 12, 2026
17361be
Add Trigger Verification to SSF Receiver Provider toolbar
thomasdarimont Feb 12, 2026
901d8dc
Polishing
thomasdarimont Feb 12, 2026
26a7bf6
Fix code review issues
thomasdarimont Feb 12, 2026
fab58ea
Fix code review issues
thomasdarimont Feb 12, 2026
2bc5aa2
Fix code review issues
thomasdarimont Feb 12, 2026
8c6da1a
Fix code review issues
thomasdarimont Feb 12, 2026
842d67b
Fix code review issues
thomasdarimont Feb 12, 2026
a453c25
Revise transmitterMetadataUrlHelp
thomasdarimont Feb 12, 2026
6a682ab
Add client authentication support to SSF Receiver
thomasdarimont Feb 18, 2026
cd00311
Revise DefaultSsfEventListener
thomasdarimont Feb 18, 2026
29f4549
Allow idp lookups by SubjectUserLookup
thomasdarimont Feb 18, 2026
81ac8b4
Make SsfAdminRealmResourceProviderFactory environment dependent
thomasdarimont Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/src/main/java/org/keycloak/common/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ public enum Feature {

DB_TIDB("TiDB database type", Type.EXPERIMENTAL),

SSF("Shared Signals Framework", Type.EXPERIMENTAL),

HTTP_OPTIMIZED_SERIALIZERS("Optimized JSON serializers for better performance of the HTTP layer", Type.PREVIEW),

OPENAPI("OpenAPI specification served at runtime", Type.EXPERIMENTAL, CLIENT_ADMIN_API_V2),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,7 @@ keyTab=Key tab
addSamlProvider=Add SAML provider
addSpiffeProvider=Add SPIFFE provider
addKubernetesProvider=Add Kubernetes provider
addSsfReceiverProvider=Add SSF receiver provider
spiffeTrustDomain=SPIFFE Trust Domain
spiffeTrustDomainHelp=Use a URL starting with 'spiffe://' followed by a domain name. For example, 'spiffe://acme.com'.
spiffeBundleEndpoint=SPIFFE Bundle or OIDC JWKs endpoint
Expand Down Expand Up @@ -2915,6 +2916,36 @@ fullName={{givenName}} {{familyName}}
deleteConfirm=Are you sure you want to permanently delete the provider '{{provider}}'?
compositesRemovedAlertDescription=All the associated roles have been removed
aliasHelp=The alias uniquely identifies an identity provider and it is also used to build the redirect uri.
ssfTransmitterIssuerHelp=The issuer URL of the SSF Transmitter. This is used to derive the transmitter metadata endpoint.
ssfTransmitterAccessToken=Access Token
ssfTransmitterAccessTokenHelp=The Transmitter Access Token to perform SSF stream verification.
ssfTransmitterToken=Transmitter Token
ssfTransmitterTokenHelp=The token used to authenticate with the SSF Transmitter.
ssfTransmitterTokenType=Token Type
ssfTransmitterTokenTypeHelp=The type of token to use for authenticating with the SSF Transmitter.
ssfTransmitterTokenType.accessToken=Access Token
ssfTransmitterAuthMethod=Transmitter Authentication
ssfTransmitterAuthMethodHelp=How to authenticate with the SSF Transmitter. Use 'Static Token' to provide a pre-configured bearer token, or 'Client Credentials' to obtain tokens dynamically using the OAuth2 client_credentials grant.
ssfTransmitterAuthMethod.staticToken=Static Token
ssfTransmitterAuthMethod.clientCredentials=Client Credentials
ssfTokenUrlHelp=The token endpoint URL of the authorization server associated with the SSF Transmitter, used to obtain access tokens via client_credentials grant.
ssfScope=Scope
ssfScopeHelp=Space-separated list of OAuth2 scopes to request when obtaining access tokens via client_credentials grant. Leave blank to use the transmitter's default scopes.
transmitterMetadataUrl=Metadata URL
transmitterMetadataUrlHelp=The SSF Transmitter metadata url, e.g. /.well-known/ssf-configuration. Leave blank to derive from issuer URL.
ssfStreamId=Stream ID
ssfStreamIdHelp=ID of the SSF stream registered with the Transmitter.
ssfStreamSettings=Stream Settings
ssfStreamAudience=Audience
ssfStreamAudienceHelp=Audience URI configured for the Stream registered with the Transmitter. If empty the current realm issuer URI is used as audience. Multiple audience URIs can be provided as comma separated list.
ssfDeliveryMethod=Delivery Method
ssfDeliveryMethodHelp=The delivery method used to receive SSF events from the Transmitter.
ssfDeliveryMethod.push=Push
ssfPushAuthorizationHeader=Push Authorization Header
ssfPushAuthorizationHeaderHelp='Authorization' header value expected to be sent by SSF Transmitters when Push delivery via HTTP is used.
ssfTriggerVerification=Trigger Verification
ssfTriggerVerificationSuccess=Stream verification triggered successfully.
ssfTriggerVerificationError=Failed to trigger stream verification: {{error}}
selectRealm=Select realm
roleNameLdapAttribute=Role name LDAP attribute
javaKeystore=java-keystore
Expand Down
105 changes: 105 additions & 0 deletions js/apps/admin-ui/src/identity-providers/add/AddSsfReceiver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import {
ActionGroup,
AlertVariant,
Button,
PageSection,
Title,
} from "@patternfly/react-core";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { useAdminClient } from "../../admin-client";
import { useAlerts } from "@keycloak/keycloak-ui-shared";
import { FormAccess } from "../../components/form/FormAccess";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { useRealm } from "../../context/realm-context/RealmContext";
import { toIdentityProvider } from "../routes/IdentityProvider";
import { toIdentityProviders } from "../routes/IdentityProviders";
import { SsfReceiverSettings } from "./SsfReceiverSettings";
import { SsfReceiverStreamSettings } from "./SsfReceiverStreamSettings";

type DiscoveryIdentityProvider = IdentityProviderRepresentation & {
discoveryEndpoint?: string;
};

export default function AddSsfReceiver() {
const { adminClient } = useAdminClient();

const { t } = useTranslation();
const navigate = useNavigate();
const id = "ssf-receiver";

const form = useForm<DiscoveryIdentityProvider>({
defaultValues: { alias: id, config: { allowCreate: "true" } },
mode: "onChange",
});
const {
handleSubmit,
formState: { isDirty },
} = form;

const { addAlert, addError } = useAlerts();
const { realm } = useRealm();

const onSubmit = async (provider: DiscoveryIdentityProvider) => {
delete provider.discoveryEndpoint;
try {
await adminClient.identityProviders.create({
...provider,
providerId: id,
});
addAlert(t("createIdentityProviderSuccess"), AlertVariant.success);
navigate(
toIdentityProvider({
realm,
providerId: id,
alias: provider.alias!,
tab: "settings",
}),
);
} catch (error: any) {
addError("createIdentityProviderError", error);
}
};

return (
<>
<ViewHeader titleKey={t("addSsfReceiverProvider")} />
<PageSection variant="light">
<FormProvider {...form}>
<FormAccess
role="manage-identity-providers"
isHorizontal
onSubmit={handleSubmit(onSubmit)}
>
<SsfReceiverSettings />
<Title headingLevel="h2" size="xl" className="kc-form-panel__title">
{t("ssfStreamSettings")}
</Title>
<SsfReceiverStreamSettings />
<ActionGroup>
<Button
isDisabled={!isDirty}
variant="primary"
type="submit"
data-testid="createProvider"
>
{t("add")}
</Button>
<Button
variant="link"
data-testid="cancel"
component={(props) => (
<Link {...props} to={toIdentityProviders({ realm })} />
)}
>
{t("cancel")}
</Button>
</ActionGroup>
</FormAccess>
</FormProvider>
</PageSection>
</>
);
}
79 changes: 76 additions & 3 deletions js/apps/admin-ui/src/identity-providers/add/DetailSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fetchWithError } from "@keycloak/keycloak-admin-client";
import type IdentityProviderMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderMapperRepresentation";
import IdentityProviderRepresentation, {
IdentityProviderType,
Expand Down Expand Up @@ -49,6 +50,8 @@ import { useAccess } from "../../context/access/Access";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { toUpperCase } from "../../util";
import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders";
import { joinPath } from "../../utils/joinPath";
import useIsFeatureEnabled, { Feature } from "../../utils/useIsFeatureEnabled";
import { useParams } from "../../utils/useParams";
import { toIdentityProviderAddMapper } from "../routes/AddMapper";
Expand All @@ -70,6 +73,8 @@ import { OIDCGeneralSettings } from "./OIDCGeneralSettings";
import { ReqAuthnConstraints } from "./ReqAuthnConstraintsSettings";
import { SamlGeneralSettings } from "./SamlGeneralSettings";
import { SpiffeSettings } from "./SpiffeSettings";
import { SsfReceiverSettings } from "./SsfReceiverSettings";
import { SsfReceiverStreamSettings } from "./SsfReceiverStreamSettings";
import { AdminEvents } from "../../events/AdminEvents";
import { UserProfileClaimsSettings } from "./OAuth2UserProfileClaimsSettings";
import { KubernetesSettings } from "./KubernetesSettings";
Expand Down Expand Up @@ -173,6 +178,28 @@ const Header = ({ onChange, value, save, toggleDeleteDialog }: HeaderProps) => {
}
};

const triggerSsfVerification = async (alias: string) => {
try {
await fetchWithError(
joinPath(
adminClient.baseUrl,
"admin/realms",
encodeURIComponent(adminClient.realmName),
"ssf/receivers",
encodeURIComponent(alias),
"verify",
),
{
method: "POST",
headers: getAuthorizationHeaders(await adminClient.getAccessToken()),
},
);
addAlert(t("ssfTriggerVerificationSuccess"), AlertVariant.success);
} catch (error) {
addError("ssfTriggerVerificationError", error);
}
};

return (
<>
<DisableConfirm />
Expand Down Expand Up @@ -219,6 +246,16 @@ const Header = ({ onChange, value, save, toggleDeleteDialog }: HeaderProps) => {
</DropdownItem>,
]
: []),
...(provider?.providerId?.includes("ssf-receiver")
? [
<DropdownItem
key="triggerVerification"
onClick={() => triggerSsfVerification(provider.alias!)}
>
{t("ssfTriggerVerification")}
</DropdownItem>,
]
: []),
<Divider key="separator" />,
<DropdownItem key="delete" onClick={() => toggleDeleteDialog()}>
{t("delete")}
Expand Down Expand Up @@ -426,6 +463,7 @@ export default function DetailSettings() {
const isSAML = provider.providerId!.includes("saml");
const isOAuth2 = provider.providerId!.includes("oauth2");
const isSPIFFE = provider.providerId!.includes("spiffe");
const isSsfReceiver = provider.providerId!.includes("ssf-receiver");
const isKubernetes = provider.providerId!.includes("kubernetes");
const isJWTAuthorizationGrant = provider.providerId!.includes(
"jwt-authorization-grant",
Expand Down Expand Up @@ -467,7 +505,8 @@ export default function DetailSettings() {
const sections = [
{
title: t("generalSettings"),
isHidden: isSPIFFE || isKubernetes || isJWTAuthorizationGrant,
isHidden:
isSPIFFE || isKubernetes || isJWTAuthorizationGrant || isSsfReceiver,
panel: (
<FormAccess
role="manage-identity-providers"
Expand Down Expand Up @@ -553,6 +592,34 @@ export default function DetailSettings() {
</Form>
),
},
{
title: t("generalSettings"),
isHidden: !isSsfReceiver,
panel: (
<Form
isHorizontal
className="pf-v5-u-py-lg"
onSubmit={handleSubmit(save)}
>
<SsfReceiverSettings />
<FixedButtonsGroup name="idp-details" isSubmit reset={reset} />
</Form>
),
},
{
title: t("ssfStreamSettings"),
isHidden: !isSsfReceiver,
panel: (
<Form
isHorizontal
className="pf-v5-u-py-lg"
onSubmit={handleSubmit(save)}
>
<SsfReceiverStreamSettings />
<FixedButtonsGroup name="idp-details" isSubmit reset={reset} />
</Form>
),
},
{
title: t("generalSettings"),
isHidden: !isJWTAuthorizationGrant,
Expand Down Expand Up @@ -601,7 +668,8 @@ export default function DetailSettings() {
},
{
title: t("advancedSettings"),
isHidden: isSPIFFE || isKubernetes || isJWTAuthorizationGrant,
isHidden:
isSPIFFE || isKubernetes || isJWTAuthorizationGrant || isSsfReceiver,
panel: (
<FormAccess
role="manage-identity-providers"
Expand Down Expand Up @@ -653,7 +721,12 @@ export default function DetailSettings() {
</Tab>
<Tab
id="mappers"
isHidden={isSPIFFE || isKubernetes || isJWTAuthorizationGrant}
isHidden={
isSPIFFE ||
isKubernetes ||
isJWTAuthorizationGrant ||
isSsfReceiver
}
data-testid="mappers-tab"
title={<TabTitleText>{t("mappers")}</TabTitleText>}
{...mappersTab}
Expand Down
Loading