Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
06e9874
This is to close [KEYCLOAK-19956](https://issues.redhat.com/browse/KE…
dmartinol May 5, 2023
83cb863
integrated suggested changes
dmartinol May 9, 2023
b2fdf9b
Merge branch 'main' into main
dmartinol May 10, 2023
16a63f2
fix: integrated changes from PR
dmartinol May 16, 2023
32c43be
Merge branch 'keycloak:main' into main
dmartinol May 16, 2023
a156b2d
fix: added logic to manage token claim values of type List and integr…
dmartinol May 19, 2023
f9d734c
Merge branch 'main' into main
dmartinol May 22, 2023
5df84b6
Merge branch 'main' into main
dmartinol May 22, 2023
6f2b091
Merge branch 'main' into main
dmartinol May 23, 2023
9dfa350
Merge branch 'keycloak:main' into main
dmartinol May 23, 2023
d4ab80e
Merge branch 'keycloak:main' into main
dmartinol May 25, 2023
112c47b
Updated field names as per PR comments
dmartinol May 25, 2023
ad0a015
Merge branch 'main' into main
dmartinol May 29, 2023
d0b17bb
Merge branch 'keycloak:main' into main
dmartinol May 30, 2023
16dacd3
Update js/apps/admin-ui/src/identity-providers/add/AdvancedSettings.tsx
dmartinol May 30, 2023
23a03f7
Update js/apps/admin-ui/src/identity-providers/add/AdvancedSettings.tsx
dmartinol May 30, 2023
1943950
Integrated recommended changes
dmartinol May 30, 2023
c60164b
linting js
dmartinol May 30, 2023
4fad71d
Merge branch 'keycloak:main' into main
dmartinol May 31, 2023
0a1b167
Update js/apps/admin-ui/src/identity-providers/add/AdvancedSettings.tsx
dmartinol May 31, 2023
434383a
Removed redundant aria-label field
dmartinol May 31, 2023
d86e1fc
Merge branch 'main' into main
dmartinol May 31, 2023
64a66c9
Merge branch 'main' into main
dmartinol Jun 1, 2023
ecda9a0
Merge branch 'main' into main
dmartinol Jun 6, 2023
6dfd539
Merge branch 'main' into main
dmartinol Jun 13, 2023
623aa07
Merge pull request #1 from keycloak/main
dmartinol Jun 13, 2023
0d89877
Merge branch 'main' into main
dmartinol Jun 13, 2023
c3f5de0
Fixed broken tests
dmartinol Jun 13, 2023
ffc59c0
Merge branch 'main' into main
dmartinol Jun 13, 2023
d974f05
Merge branch 'main' into main
dmartinol Jun 20, 2023
f01d445
Merge branch 'main' into main
dmartinol Jun 20, 2023
4f0856f
Fixed UI test for IDP
dmartinol Jun 20, 2023
562d735
Fixed linting issues
dmartinol Jun 20, 2023
2c07e04
fixing lint
dmartinol Jun 20, 2023
66be92f
linting fixes
dmartinol Jun 20, 2023
576d71b
linting
dmartinol Jun 20, 2023
0e2d305
added extra call to ensureAdvancedSettingsAreVisible()
dmartinol Jun 20, 2023
ae8a29a
fixed should revert and save options test
dmartinol Jun 20, 2023
c7c16a7
Merge branch 'keycloak:main' into main
dmartinol Jun 27, 2023
4024813
Updated release notes
dmartinol Jun 27, 2023
5dafb3a
Merge branch 'main' into main
dmartinol Jun 27, 2023
3401bef
Merge branch 'main' into main
dmartinol Jun 28, 2023
7e6a0e5
Update docs/documentation/server_admin/topics/identity-broker/configu…
dmartinol Jun 28, 2023
4abc09c
Update docs/documentation/release_notes/topics/22_0_0.adoc
dmartinol Jun 28, 2023
ae32ea5
Merge branch 'main' into main
dmartinol Jun 28, 2023
bd0b54b
Merge branch 'main' into main
dmartinol Jun 28, 2023
0fbd562
Merge branch 'main' into main
dmartinol Jun 29, 2023
9e52ab6
Merge branch 'main' into main
mposolda Jun 29, 2023
c1f86f5
Merge branch 'main' into main
mposolda Jun 29, 2023
57a4e4d
Merge branch 'main' into main
mposolda Jun 29, 2023
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
10 changes: 9 additions & 1 deletion docs/documentation/release_notes/topics/22_0_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,19 @@ q=<name>:<value> <name>:<value> ...

Where `<name>` and `<value>` represent the attribute name and value, respectively.

= Essential claim configuration in OpenID Connect identity providers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andymunro could you give this piece of text a look?


OpenID Connect identity providers support a new configuration to specify that the ID tokens issued by the identity provider must have a specific claim,
otherwise the user can not authenticate through this broker.

The option is disabled by default; when it is enabled, you can specify the name of the JWT token claim to filter and the value to match
(supports regular expression format).

= LDAPS-only Truststore option removed

LDAP option to use truststore SPI `Only for ldaps` has been removed. This parameter is used to
select truststore for TLS-secured LDAP connection: either internal Keycloak truststore is
picked (`Always`), or the global JVM one (`Never`).

Deployments where `Only for ldaps` was used will automatically behave as if `Always` option was
selected for TLS-secured LDAP connections.
selected for TLS-secured LDAP connections.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ Although each type of identity provider has its configuration options, all share
|GUI Order
|The sort order of the available identity providers on the login page.


|Verify essential claim
|When *ON*, ID tokens issued by the identity provider must have a specific claim, otherwise, the user can not authenticate through this broker

|Essential claim
|When *Verify essential claim* is *ON*, the name of the JWT token claim to filter (match is case sensitive)

|Essential claim value
|When *Verify essential claim* is *ON*, the value of the JWT token claim to match (supports regular expression format)

|First Login Flow
|The authentication flow {project_name} triggers when users use this identity provider to log into {project_name} for the first time.

Expand Down
10 changes: 9 additions & 1 deletion js/apps/admin-ui/cypress/e2e/identity_providers_test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ describe("Identity provider test", () => {
listingPage.goToItemDetails("github");

advancedSettings.typeScopesInput("openid");
//advancedSettings.assertScopesInputEqual("openid"); //this line doesn't work
advancedSettings.assertScopesInputEqual("openid");

advancedSettings.assertStoreTokensSwitchTurnedOn(false);
advancedSettings.assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(
Expand All @@ -331,7 +331,11 @@ describe("Identity provider test", () => {
advancedSettings.clickTrustEmailSwitch();
advancedSettings.clickAccountLinkingOnlySwitch();
advancedSettings.clickHideOnLoginPageSwitch();
advancedSettings.clickEssentialClaimSwitch();
advancedSettings.typeClaimNameInput("claim-name");
advancedSettings.typeClaimValueInput("claim-value");

advancedSettings.ensureAdvancedSettingsAreVisible();
advancedSettings.assertStoreTokensSwitchTurnedOn(true);
advancedSettings.assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(
true
Expand All @@ -340,6 +344,9 @@ describe("Identity provider test", () => {
advancedSettings.assertTrustEmailSwitchTurnedOn(true);
advancedSettings.assertAccountLinkingOnlySwitchTurnedOn(true);
advancedSettings.assertHideOnLoginPageSwitchTurnedOn(true);
advancedSettings.assertEssentialClaimSwitchTurnedOn(true);
advancedSettings.assertClaimInputEqual("claim-name");
advancedSettings.assertClaimValueInputEqual("claim-value");

cy.findByTestId("idp-details-save").click();
});
Expand All @@ -355,6 +362,7 @@ describe("Identity provider test", () => {
);
advancedSettings.clickStoreTokensSwitch();
advancedSettings.clickAcceptsPromptNoneForwardFromClientSwitch();
advancedSettings.ensureAdvancedSettingsAreVisible();
advancedSettings.assertStoreTokensSwitchTurnedOn(false);
advancedSettings.assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(
false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
private firstLoginFlowSelect = "#firstBrokerLoginFlowAlias";
private postLoginFlowSelect = "#postBrokerLoginFlowAlias";
private syncModeSelect = "#syncMode";
private essentialClaimSwitch = "#filteredByClaim";
private claimNameInput = "#kc-claim-filter-name";
private claimValueInput = "#kc-claim-filter-value";
private addBtn = "createProvider";
private saveBtn = "idp-details-save";
private revertBtn = "idp-details-revert";
Expand All @@ -94,6 +97,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
return this;
}

public ensureAdvancedSettingsAreVisible() {
cy.findByTestId("jump-link-general-settings").click();
cy.findByTestId("jump-link-advanced-settings").click();
}

public clickStoreTokensSwitch() {
cy.get(this.storeTokensSwitch).parent().click();
return this;
Expand Down Expand Up @@ -129,6 +137,21 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
return this;
}

public clickEssentialClaimSwitch() {
cy.get(this.essentialClaimSwitch).parent().click();
return this;
}

public typeClaimNameInput(text: string) {
cy.get(this.claimNameInput).type(text).blur();
return this;
}

public typeClaimValueInput(text: string) {
cy.get(this.claimValueInput).type(text).blur();
return this;
}

public selectFirstLoginFlowOption(loginFlowOption: LoginFlowOption) {
cy.get(this.firstLoginFlowSelect).click();
super.clickSelectMenuItem(
Expand Down Expand Up @@ -182,7 +205,7 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
}

public assertScopesInputEqual(text: string) {
cy.get(this.scopesInput).should("have.text", text).parent();
cy.get(this.scopesInput).should("have.value", text).parent();
return this;
}

Expand Down Expand Up @@ -230,6 +253,21 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
return this;
}

public assertEssentialClaimSwitchTurnedOn(isOn: boolean) {
super.assertSwitchStateOn(cy.get(this.essentialClaimSwitch).parent(), isOn);
return this;
}

public assertClaimInputEqual(text: string) {
cy.get(this.claimNameInput).should("have.value", text).parent();
return this;
}

public assertClaimValueInputEqual(text: string) {
cy.get(this.claimValueInput).should("have.value", text).parent();
return this;
}

public assertFirstLoginFlowSelectOptionEqual(
loginFlowOption: LoginFlowOption
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"trustEmail": "If enabled, email provided by this provider is not verified even if verification is enabled for the realm.",
"accountLinkingOnly": "If true, users cannot log in through this provider. They can only link to this provider. This is useful if you don't want to allow login from the provider, but want to integrate with a provider",
"hideOnLoginPage": "If hidden, login with this provider is possible only if requested explicitly, for example using the 'kc_idp_hint' parameter.",
"filteredByClaim": "If true, ID tokens issued by the identity provider must have a specific claim. Otherwise, the user can not authenticate through this broker.",
"claimFilterName": "Name of the essential claim",
"claimFilterValue": "Value of the essential claim (with regex support)",
"firstBrokerLoginFlowAlias": "Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that no Keycloak account is currently linked to the authenticated identity provider account.",
"postBrokerLoginFlowAlias": "Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this to \"None\" if you need no any additional authenticators to be triggered after login with this identity provider. Also note that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it.",
"syncMode": "Default sync mode for all mappers. The sync mode determines when user data will be synced using the mappers. Possible values are: 'legacy' to keep the behaviour before this option was introduced, 'import' to only import the user once during first login of the user with this identity provider, 'force' to always update the user during every login with this identity provider.",
Expand Down
3 changes: 3 additions & 0 deletions js/apps/admin-ui/public/locales/en/identity-providers.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@
"trustEmail": "Trust Email",
"accountLinkingOnly": "Account linking only",
"hideOnLoginPage": "Hide on login page",
"filteredByClaim": "Verify essential claim",
"claimFilterName": "Essential claim",
"claimFilterValue": "Essential claim value",
"firstBrokerLoginFlowAlias": "First login flow",
"postBrokerLoginFlowAlias": "Post login flow",
"syncMode": "Sync mode",
Expand Down
101 changes: 99 additions & 2 deletions js/apps/admin-ui/src/identity-providers/add/AdvancedSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation";
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import {
FormGroup,
Select,
SelectOption,
SelectVariant,
Switch,
ValidatedOptions,
} from "@patternfly/react-core";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared";

import { adminClient } from "../../admin-client";
import { useFetch } from "../../utils/useFetch";
import type { FieldProps } from "../component/FormGroupField";
import { FormGroupField } from "../component/FormGroupField";
import { SwitchField } from "../component/SwitchField";
import { TextField } from "../component/TextField";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";

const LoginFlow = ({
field,
Expand Down Expand Up @@ -93,8 +98,18 @@ type AdvancedSettingsProps = { isOIDC: boolean; isSAML: boolean };

export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
const { t } = useTranslation("identity-providers");
const { control } = useFormContext();
const {
control,
register,
formState: { errors },
} = useFormContext<IdentityProviderRepresentation>();
const [syncModeOpen, setSyncModeOpen] = useState(false);
const filteredByClaim = useWatch({
control,
name: "config.filteredByClaim",
defaultValue: "false",
});
const claimFilterRequired = filteredByClaim === "true";
return (
<>
{!isOIDC && !isSAML && (
Expand Down Expand Up @@ -125,6 +140,88 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
/>
<SwitchField field="config.hideOnLoginPage" label="hideOnLoginPage" />

{(!isSAML || isOIDC) && (
<FormGroupField label="filteredByClaim">
<Controller
name="config.filteredByClaim"
defaultValue="false"
control={control}
render={({ field }) => (
<Switch
id="filteredByClaim"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={field.value === "true"}
onChange={(value) => {
field.onChange(value.toString());
}}
/>
)}
/>
</FormGroupField>
)}
{(!isSAML || isOIDC) && claimFilterRequired && (
<>
<FormGroup
label={t("identity-providers:claimFilterName")}
labelIcon={
<HelpItem
helpText={t("identity-providers-help:claimFilterName")}
fieldLabelId="identity-providers:claimFilterName"
/>
}
fieldId="kc-claim-filter-name"
isRequired
validated={
errors.config?.claimFilterName
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<KeycloakTextInput
isRequired
id="kc-claim-filter-name"
data-testid="claimFilterName"
validated={
errors.config?.claimFilterName
? ValidatedOptions.error
: ValidatedOptions.default
}
{...register("config.claimFilterName", { required: true })}
/>
</FormGroup>
<FormGroup
label={t("identity-providers:claimFilterValue")}
labelIcon={
<HelpItem
helpText={t("identity-providers-help:claimFilterValue")}
fieldLabelId="identity-providers:claimFilterName"
/>
}
fieldId="kc-claim-filter-value"
isRequired
validated={
errors.config?.claimFilterValue
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<KeycloakTextInput
isRequired
id="kc-claim-filter-value"
data-testid="claimFilterValue"
validated={
errors.config?.claimFilterValue
? ValidatedOptions.error
: ValidatedOptions.default
}
{...register("config.claimFilterValue", { required: true })}
/>
</FormGroup>
</>
)}
<LoginFlow
field="firstBrokerLoginFlowAlias"
label="firstBrokerLoginFlowAlias"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ public class IdentityProviderModel implements Serializable {

public static final String HIDE_ON_LOGIN = "hideOnLoginPage";

public static final String FILTERED_BY_CLAIMS = "filteredByClaim";
public static final String CLAIM_FILTER_NAME = "claimFilterName";
public static final String CLAIM_FILTER_VALUE = "claimFilterValue";

private String internalId;

/**
Expand Down Expand Up @@ -254,4 +258,28 @@ public boolean isHideOnLogin() {
public void setHideOnLogin(boolean hideOnLogin) {
getConfig().put(HIDE_ON_LOGIN, String.valueOf(hideOnLogin));
}

public boolean isFilteredByClaims() {
return Boolean.valueOf(getConfig().getOrDefault(FILTERED_BY_CLAIMS, Boolean.toString(false)));
}

public void setFilteredByClaims(boolean filteredByClaims) {
getConfig().put(FILTERED_BY_CLAIMS, String.valueOf(filteredByClaims));
}

public String getClaimFilterName() {
return String.valueOf(getConfig().getOrDefault(CLAIM_FILTER_NAME, ""));
}

public void setClaimFilterName(String claimFilterName) {
getConfig().put(CLAIM_FILTER_NAME, claimFilterName);
}

public String getClaimFilterValue() {
return String.valueOf(getConfig().getOrDefault(CLAIM_FILTER_VALUE, ""));
}

public void setClaimFilterValue(String claimFilterValue) {
getConfig().put(CLAIM_FILTER_VALUE, claimFilterValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForModification;

Expand Down Expand Up @@ -389,6 +392,30 @@ public BrokeredIdentityContext getFederatedIdentity(String response) {
throw new IdentityBrokerException("Mismatch between the subject in the id_token and the subject from the user_info endpoint");
}

if (getConfig().isFilteredByClaims()) {
String filterName = getConfig().getClaimFilterName();
String filterValue = getConfig().getClaimFilterValue();

logger.tracef("Filtering user %s by %s=%s", idToken.getOtherClaims().get(getusernameClaimNameForIdToken()), filterName, filterValue);
if (idToken.getOtherClaims().containsKey(filterName)) {
Object claimObject = idToken.getOtherClaims().get(filterName);
List<String> claimValues = new ArrayList<>();
if (claimObject instanceof List) {
((List<?>)claimObject).forEach(v->claimValues.add(Objects.toString(v)));
} else {
claimValues.add(Objects.toString(claimObject));
}
logger.tracef("Found claim %s with values %s", filterName, claimValues);
if (!claimValues.stream().anyMatch(v->v.matches(filterValue))) {
logger.warnf("Claim %s has values \"%s\" that does not match the expected filter \"%s\"", filterName, claimValues, filterValue);
throw new IdentityBrokerException(String.format("Unmatched claim value for %s.", filterName));
}
} else {
logger.debugf("Claim %s was not found", filterName);
throw new IdentityBrokerException(String.format("Claim %s not found", filterName));
}
}

identity.getContextData().put(BROKER_NONCE_PARAM, idToken.getOtherClaims().get(OIDCLoginProtocol.NONCE_PARAM));

if (getConfig().isStoreToken()) {
Expand Down
Loading