Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
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 @@ -69,6 +69,8 @@ public enum Feature {

TOKEN_EXCHANGE("Token Exchange Service", Type.PREVIEW),

ASSERTION_GRANT("AssertionGrantService",Type.PREVIEW),

WEB_AUTHN("W3C Web Authentication (WebAuthn)", Type.DEFAULT),

CLIENT_POLICIES("Client configuration policies", Type.DEFAULT),
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/java/org/keycloak/OAuth2Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ public interface OAuth2Constants {

String UMA_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";

// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-jwt-bearer
String JWT_BEARER_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
String ASSERTION = "assertion";

// https://tools.ietf.org/html/draft-ietf-oauth-device-flow-15#section-3.4
String DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
String DEVICE_CODE = "device_code";
Expand Down
30 changes: 30 additions & 0 deletions core/src/main/java/org/keycloak/TokenVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,36 @@ public boolean test(JsonWebToken t) throws VerificationException {
}
};

/**
* Verify the 'iss' claim in OIDC tokens
*/
public static class IssuerCheck implements Predicate<JsonWebToken> {

private final String expectedIssuer;

public IssuerCheck(String expectedIssuer) {
this.expectedIssuer = expectedIssuer;
}

@Override
public boolean test(JsonWebToken t) throws VerificationException {
if (expectedIssuer == null) {
throw new VerificationException("Missing expectedIssuer");
}

String issuer = t.getIssuer();
if (issuer == null) {
throw new VerificationException("No issuer in the token");
}

if (issuer.equals(expectedIssuer)) {
return true;
}

throw new VerificationException("Expected issuer not available in the token");
}
};


public static class IssuedForCheck implements Predicate<JsonWebToken> {

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion docs/documentation/server_admin/topics/admin-console.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ include::login-settings/forgot-password.adoc[leveloffset=2]
include::login-settings/remember-me.adoc[leveloffset=2]
include::login-settings/acr-to-loa-mapping.adoc[leveloffset=2]
include::login-settings/update-email-workflow.adoc[leveloffset=2]
include::realms/keys.adoc[]
include::realms/keys.adoc[]
include::realms/cross-domain-trust.adoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,20 @@ really requires level 2, the client is encouraged to check the presence of the `
image:images/client-oidc-map-acr-to-loa.png[alt="ACR to LoA mapping"]

For further details see <<_step-up-flow,Step-up Authentication>> and https://openid.net/specs/openid-connect-core-1_0.html#acrSemantics[the official OIDC specification].

[[_assertion_grant_configuration]]
*JWT Bearer Authorization Grant Configuration*

In the advanced settings of a client, you can define a set of cross-domain trusts for the assertion grant protocol. This configuration is used when a client submits an
assertion to the OIDC token endpoint with _grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer_ (see the <<_assertion_grant,JWTs as Authorization Grants>> chapter for more information).

[NOTE]
====
Cross-domain trusts are configured at the realm level. See the <<cross_domain_trust,Configuring Cross-Domain Trusts>> chapter for more information.
====

When verifying an assertion, Keycloak will iterate through each cross-domain trust configured on the client and attempt to verify the assertion signature against it. If valid, Keycloak will then
verify that the _iss_ and _aud_ claim in the assertion match the issuer and audience of the cross-domain trust. If the assertion is valid, Keycloak will generate an access token for the user specified
in the _sub_ claim of the assertion and return it to the client.

image:images/assertion-grant-config.png[alt="Assertion Grant Configuration"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[[cross_domain_trust]]
=== Configuring Cross-Domain Trusts
Copy link
Contributor

Choose a reason for hiding this comment

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

What about identity brokers, would it make sense to somehow link this concept of trusted domains with identity broker providers?


Cross-domain trusts can be defined at the realm level to specify trusted security domains external to Keycloak. This set of trusted
domains is used by other features, such as the <<_assertion_grant,JWTs as Authorization Grants>> feature which allows external entities
to request access tokens for users.

To configure a trusted domain:
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd imagine there would be a need to be able to lock down things further to have checks on claims within the assertions. For example if a different Keycloak server is the trusted domain, then since Keycloak uses JWTs for refresh, ID, and access tokens, there would need to be some mechanism to for example only permit ID tokens. For other providers there could be other claims that would need to be checked.


. Go to `Realm settings`
. Select the `Cross-Domain Trust` tab
. Create a trusted domain entry
. Save the changes

Each cross-domain trust configuration contains 3 elements:

* Issuer: The URI identifying this trusted domain. This value will be used to verify the issuer on assertions signed by this trusted domain.
* Audience: The URI identifying the audience of assertions submitted by this trusted domain. This value will be used to verify the audience on assertions signed by this trusted domain.
Copy link
Contributor

Choose a reason for hiding this comment

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

What if there are multiple audiences, and different client use different audiences?

As an example let's imagine we're setting up a cross-domain trust with Google as the assertion provider. In a realm there are two clients (client-a and client-b), where each client has their own client configured with Google.

* Certificate: The base64 encoded X509 signing certificate of the trusted domain. This will be used to verify assertions signed by this trusted domain.
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be natural to support jwks urls here

Copy link
Contributor

Choose a reason for hiding this comment

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

Does it have to be ceritificate?
Can it be also public key?

It would be useful in the case when the issuer of assertion is Okta, because Okta looks provide only public key, not certificate for signature verification of the assertion.


image:images/cross-domain-trust-table.png[alt="Cross-Domain Trust Configuration Table"]
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,23 @@ This is used by clients running on internet-connected devices that have limited
. The application provides the user with the user code and the verification URI. The user accesses a verification URI to be authenticated by using another browser. You could define a short verification_uri that will be redirected to {project_name} verification URI (/realms/realm_name/device)outside {project_name} - fe in a proxy.
. The application repeatedly polls {project_name} to find out if the user completed the user authorization. If user authentication is complete, the application exchanges the device code for an _identity_, _access_ and _refresh_ token.

[[_assertion_grant]]
===== JWTs as Authorization Grants

The JWTs as authorization grants flow described in https://www.rfc-editor.org/rfc/rfc7523#section-2.1[RFC 7523 Section 2.1] enables trusted entities to authenticate users by submitting a signed JWT to the OIDC token endpoint.

The protocol works as follows:

. A trusted entity generates a JWT, specifying the username of the user in the _sub_ claim of the JWT
Copy link
Contributor

Choose a reason for hiding this comment

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

The username from the assertion does not necessarily map directly to a username in Keycloak.

. The trusted entity signs the JWT with the private key assocated with a pre-registered certificate (see the <<_assertion_grant_configuration,Assertion Grant Trusted Issuer Configuration>> chapter for configuring trusted certificates)
. The trusted entity submits the JWT to the oidc _token_ endpoint with _grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer_
. Keycloak validates the submitted assertion against cross-domain trusts in the client configuration
. If the assertion is valid, Keycloak creates an authentication session for the requested user and returns the generated OIDC tokens
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense to create a user session and return a refresh token? Or it is safer to just rely on transient sessions and no refresh tokens as a regular client credentials grant?

Copy link
Author

Choose a reason for hiding this comment

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

I agree, a transient session would be safer. I double checked the RFC and it doesn't have a refresh token requirement, it says its optional so implementing it without the refresh token seems like a safer bet.

Copy link
Contributor

Choose a reason for hiding this comment

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

From RFC-7521:

An assertion used in this context is generally a short-lived representation of the authorization grant, and authorization servers SHOULD NOT issue access tokens with a lifetime that exceeds the validity period of the assertion by a significant period. In practice, that will usually mean that refresh tokens are not issued in response to assertion grant requests, and access tokens will be issued with a reasonably short lifetime. Clients can refresh an expired access token by requesting a new one using the same assertion, if it is still valid, or with a new assertion.

So, no refresh token (or session), and make sure the access token has an expiration equal to or shorter than the assertion expiration.


To perform assertion grant requests, the client submitting the assertion grant request must be authorized to impersonate users.

For details on fine grain permissions and the _impersonate_ role, see the <<full-list-of-permissions,Fine grain admin permissions>> chapter.
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume token exchange (and fine grained admin perissions) allows limiting what users can be impersonated? For example a specific group of users.


[[_client_initiated_backchannel_authentication_grant]]
===== Client initiated backchannel authentication grant

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3139,6 +3139,25 @@ parRequestUriLifespanHelp=Number that represents the lifetime of the request URI
identityBrokeringLink=Identity brokering link
searchClientRegistration=Search for policy
importFileHelp=File to import a key
oidcClientJWTBearerEnabled=JWT Bearer Authorization Grant
oidcClientJWTBearerEnabledHelp=This enables the 'jwt-bearer' grant type as described in RFC 7523
oidcClientJWTBearerConfigTitle=JWT Bearer Authorization Grant Configuration
oidcClientJWTBearerConfigHelp=This section is used to configure cross-tenant trust configurations for this client. These trusted issuers will be used to verify assertions sent by this client in 'jwt-bearer' token requests.
oidcClientJWTBearerCrossDomainTitle=Cross-Domain Trusts
oidcClientJWTBearerCrossDomainHelp=Keycloak will use the selected cross-domain trusts to verify assertions sent to the token endpoint by this client.
crossDomainTrustTab=Cross-Domain Trust
crossDomainTrustConfigDropdownTitle=Trusted Domains
crossDomainTrustConfigIssuer=Issuer
crossDomainTrustConfigIssuerHelp=The URI identifying this trusted domain. This value will be used to verify the issuer on assertions signed by this trusted domain.
crossDomainTrustConfigAudience=Audience
crossDomainTrustConfigAudienceHelp=The URI identifying the audience of assertions submitted by this trusted domain. This value will be used to verify the audience on assertions signed by this trusted domain.
crossDomainTrustConfigCert=Certificate
crossDomainTrustConfigCertHelp=The base64 encoded X509 signing certificate of the trusted domain. This will be used to verify assertions sent by this trusted domain
crossDomainTrustConfigAddTitle=Add Trusted Domain
crossDomainTrustConfigAddFail=Failed to add trusted domain: {{error}}
crossDomainTrustConfigDeleteTitle=Remove Trusted Domain
crossDomainTrustConfigNoneMessage=No cross-domain trusts configured
crossDomainTrustConfigNoneInstructions=Click below to add a new trusted domain
logo=Logo
avatarImage=Avatar image
organizationsEnabled=Organizations
Expand Down
18 changes: 18 additions & 0 deletions js/apps/admin-ui/src/clients/AdvancedTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { ScrollForm } from "@keycloak/keycloak-ui-shared";
import type { AddAlertFunction } from "../components/alert/Alerts";
import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled";
import { convertAttributeNameToForm, toUpperCase } from "../util";
import type { FormFields, SaveOptions } from "./ClientDetails";
import { AdvancedSettings } from "./advanced/AdvancedSettings";
Expand All @@ -14,6 +15,7 @@ import { ClusteringPanel } from "./advanced/ClusteringPanel";
import { FineGrainOpenIdConnect } from "./advanced/FineGrainOpenIdConnect";
import { FineGrainSamlEndpointConfig } from "./advanced/FineGrainSamlEndpointConfig";
import { OpenIdConnectCompatibilityModes } from "./advanced/OpenIdConnectCompatibilityModes";
import { AssertionGrantConfigPanel } from "./advanced/AssertionGrantConfigPanel";

export const parseResult = (
result: GlobalRequestResult,
Expand Down Expand Up @@ -51,6 +53,7 @@ export type AdvancedProps = {
export const AdvancedTab = ({ save, client }: AdvancedProps) => {
const { t } = useTranslation();
const openIdConnect = "openid-connect";
const isFeatureEnabled = useIsFeatureEnabled();

const { setValue } = useFormContext();
const {
Expand Down Expand Up @@ -187,6 +190,21 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => {
</>
),
},
{
title: t("oidcClientJWTBearerConfigTitle"),
isHidden:
!isFeatureEnabled(Feature.AssertionGrant) ||
attributes?.["oidc.grants.assertion.enabled"]?.toString() !==
"true",
panel: (
<>
<Text className="pf-u-pb-lg">
{t("oidcClientJWTBearerConfigHelp")}
</Text>
<AssertionGrantConfigPanel client={client} save={save} />
</>
),
},
{
title: t("authenticationOverrides"),
panel: (
Expand Down
26 changes: 26 additions & 0 deletions js/apps/admin-ui/src/clients/add/CapabilityConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,32 @@ export const CapabilityConfig = ({
)}
/>
</GridItem>
{isFeatureEnabled(Feature.AssertionGrant) && (
<GridItem lg={8} sm={6}>
<Controller
name={convertAttributeNameToForm<FormFields>(
"attributes.oidc.grants.assertion.enabled",
)}
defaultValue={false}
control={control}
render={({ field }) => (
<InputGroup>
<Checkbox
data-testid="oidc-assertion-grant"
label={t("oidcClientJWTBearerEnabled")}
id="kc-oidc-assertion-grant"
isChecked={field.value.toString() === "true"}
onChange={field.onChange}
/>
<HelpItem
helpText={t("oidcClientJWTBearerEnabledHelp")}
fieldLabelId="oidcClientJWTBearerEnabled"
/>
</InputGroup>
)}
/>
</GridItem>
)}
<GridItem lg={8} sm={6}>
<Controller
name="directAccessGrantsEnabled"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useTranslation } from "react-i18next";
import { ActionGroup, Button } from "@patternfly/react-core";

import { FormAccess } from "../../components/form/FormAccess";
import { convertAttributeNameToForm } from "../../util";
import type { AdvancedProps } from "../AdvancedTab";
import { CrossDomainSelect } from "../../components/cross-domain/crossDomainSelect";

export const AssertionGrantConfigPanel = ({
save,
client: { access, protocol },
}: AdvancedProps) => {
const { t } = useTranslation();

return (
<FormAccess
role="manage-clients"
fineGrainedAccess={access?.configure}
isHorizontal
>
{protocol === "openid-connect" && (
<>
<CrossDomainSelect
name={convertAttributeNameToForm(
"attributes.oidc.grants.assertion.config",
)}
label={t("oidcClientJWTBearerCrossDomainTitle")}
helpText={t("oidcClientJWTBearerCrossDomainHelp")}
onChange={(d) => JSON.stringify(d.map((c) => c.issuer))}
onLoad={(trustedDomains, data) => {
const loaded = JSON.parse(data || "[]");
return trustedDomains.filter((c) => loaded.includes(c.issuer));
}}
/>
<ActionGroup>
<Button variant="secondary" onClick={() => save()}>
{t("save")}
</Button>
</ActionGroup>
</>
)}
</FormAccess>
);
};
Loading