Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -155,6 +155,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 @@ -2887,6 +2887,15 @@ 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=Transmitter Access Token
ssfTransmitterAccessTokenHelp=The Transmitter Access Token to perform SSF stream verification.
ssfStreamId=Stream ID
ssfStreamIdHelp=ID of the SSF stream registered with the Transmitter.
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.
ssfPushAuthorizationHeader=Push Authorization Header
ssfPushAuthorizationHeaderHelp='Authorization' header value expected to be sent by SSF Transmitters when Push delivery via HTTP is used.
selectRealm=Select realm
roleNameLdapAttribute=Role name LDAP attribute
javaKeystore=java-keystore
Expand Down
99 changes: 99 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,99 @@
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import {
ActionGroup,
AlertVariant,
Button,
PageSection,
} 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";

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 />
<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>
</>
);
}
29 changes: 26 additions & 3 deletions js/apps/admin-ui/src/identity-providers/add/DetailSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import { OIDCGeneralSettings } from "./OIDCGeneralSettings";
import { ReqAuthnConstraints } from "./ReqAuthnConstraintsSettings";
import { SamlGeneralSettings } from "./SamlGeneralSettings";
import { SpiffeSettings } from "./SpiffeSettings";
import { SsfReceiverSettings } from "./SsfReceiverSettings";
import { AdminEvents } from "../../events/AdminEvents";
import { UserProfileClaimsSettings } from "./OAuth2UserProfileClaimsSettings";
import { KubernetesSettings } from "./KubernetesSettings";
Expand Down Expand Up @@ -425,6 +426,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 @@ -463,7 +465,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 @@ -549,6 +552,20 @@ 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("generalSettings"),
isHidden: !isJWTAuthorizationGrant,
Expand Down Expand Up @@ -597,7 +614,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 @@ -649,7 +667,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { TextControl } from "@keycloak/keycloak-ui-shared";
import { useTranslation } from "react-i18next";

export const SsfReceiverSettings = () => {
const { t } = useTranslation();

return (
<>
<TextControl
name="alias"
label={t("alias")}
labelIcon={t("aliasHelp")}
rules={{
required: t("required"),
}}
/>

<TextControl
name="config.description"
label={t("description")}
labelIcon={t("descriptionHelp")}
rules={{
required: t("required"),
}}
/>

<TextControl
name="config.issuer"
label={t("issuer")}
labelIcon={t("ssfTransmitterIssuerHelp")}
rules={{
required: t("required"),
}}
/>

<TextControl
name="config.transmitterAccessToken"
label={t("ssfTransmitterAccessToken")}
labelIcon={t("ssfTransmitterAccessTokenHelp")}
rules={{
required: t("required"),
}}
/>

<TextControl
name="config.streamAudience"
label={t("ssfStreamAudience")}
labelIcon={t("ssfStreamAudienceHelp")}
/>

<TextControl
name="config.streamId"
label={t("ssfStreamId")}
labelIcon={t("ssfStreamIdHelp")}
rules={{
required: t("required"),
}}
/>

<TextControl
name="config.pushAuthorizationHeader"
label={t("ssfPushAuthorizationHeader")}
labelIcon={t("ssfPushAuthorizationHeaderHelp")}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { lazy } from "react";
import type { Path } from "react-router-dom";
import { generateEncodedPath } from "../../utils/generateEncodedPath";
import type { AppRouteObject } from "../../routes";

export type IdentityProviderSsfReceiverParams = { realm: string };

const AddSsfReceiver = lazy(() => import("../add/AddSsfReceiver"));

export const IdentityProviderSsfReceiverRoute: AppRouteObject = {
path: "/:realm/identity-providers/ssf-receiver/add",
element: <AddSsfReceiver />,
breadcrumb: (t) => t("addSsfReceiverProvider"),
handle: {
access: "manage-identity-providers",
},
};

export const toIdentityProviderSsfReceiver = (
params: IdentityProviderSsfReceiverParams,
): Partial<Path> => ({
pathname: generateEncodedPath(IdentityProviderSsfReceiverRoute.path, params),
});
17 changes: 17 additions & 0 deletions services/src/main/java/org/keycloak/protocol/ssf/Ssf.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.keycloak.protocol.ssf;

import org.keycloak.protocol.ssf.receiver.spi.SsfReceiverProvider;

import static org.keycloak.utils.KeycloakSessionUtil.getKeycloakSession;

/**
* Entry-point to lookup the SsfProvider.
*/
public class Ssf {

private Ssf() {}

public static SsfReceiverProvider receiverProvider() {
return getKeycloakSession().getProvider(SsfReceiverProvider.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.keycloak.protocol.ssf;

public class SsfException extends RuntimeException {

public SsfException() {
}

public SsfException(String message) {
super(message);
}

public SsfException(String message, Throwable cause) {
super(message, cause);
}
}
Loading
Loading