mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Add switch to enable token-exchange to requester clients
Closes #37110 Signed-off-by: Douglas Palmer <dpalmer@redhat.com>
This commit is contained in:
parent
fe090c1635
commit
3687aae436
@ -448,6 +448,7 @@ nameIdFormatHelp=The name ID format to use for the subject.
|
||||
detailsHelp=This is information about the details.
|
||||
adminEvents=Admin events
|
||||
serviceAccountHelp=Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. In terms of OAuth2 specification, this enables support of 'Client Credentials Grant' for this client.
|
||||
standardTokenExchangeEnabledHelp=Enable Standard Token Exchange V2 for this client.
|
||||
urisHelp=Set of URIs which are protected by resource.
|
||||
eventTypes.IDENTITY_PROVIDER_RESPONSE.name=Identity provider response
|
||||
confirmClientSecretTitle=Regenerate secret for this client?
|
||||
@ -1112,6 +1113,7 @@ client-updater-source-groups.tooltip=Name of groups to check. The condition eval
|
||||
webAuthnPolicyRpId=Relying party ID
|
||||
ldapRolesDnHelp=LDAP DN where roles of this tree are saved. For example, 'ou\=finance,dc\=example,dc\=org'.
|
||||
serviceAccount=Service accounts roles
|
||||
standardTokenExchangeEnabled=Standard Token Exchange
|
||||
providerUpdatedSuccess=Client policy updated successfully
|
||||
assertionConsumerServiceRedirectBindingURL=Assertion Consumer Service Redirect Binding URL
|
||||
createClientScopeError=Could not create client scope\: '{{error}}'
|
||||
|
||||
@ -76,6 +76,12 @@ export const CapabilityConfig = ({
|
||||
),
|
||||
false,
|
||||
);
|
||||
setValue(
|
||||
convertAttributeNameToForm<FormFields>(
|
||||
"attributes.standard.token.exchange.enabled",
|
||||
),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}}
|
||||
aria-label={t("clientAuthentication")}
|
||||
@ -234,6 +240,41 @@ export const CapabilityConfig = ({
|
||||
)}
|
||||
/>
|
||||
</GridItem>
|
||||
{isFeatureEnabled(Feature.StandardTokenExchangeV2) && (
|
||||
<GridItem lg={8} sm={6}>
|
||||
<Controller
|
||||
name={convertAttributeNameToForm<
|
||||
Required<ClientRepresentation["attributes"]>
|
||||
>("attributes.standard.token.exchange.enabled")}
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<InputGroup>
|
||||
<InputGroupItem>
|
||||
<Checkbox
|
||||
data-testid="standard-token-exchange-enabled"
|
||||
label={t("standardTokenExchangeEnabled")}
|
||||
id="kc-standard-token-exchange-enabled"
|
||||
name="standard-token-exchange-enabled"
|
||||
isChecked={
|
||||
field.value.toString() === "true" &&
|
||||
!clientAuthentication
|
||||
}
|
||||
onChange={field.onChange}
|
||||
isDisabled={clientAuthentication}
|
||||
/>
|
||||
</InputGroupItem>
|
||||
<InputGroupItem>
|
||||
<HelpItem
|
||||
helpText={t("standardTokenExchangeEnabledHelp")}
|
||||
fieldLabelId="standardTokenExchangeEnabled"
|
||||
/>
|
||||
</InputGroupItem>
|
||||
</InputGroup>
|
||||
)}
|
||||
/>
|
||||
</GridItem>
|
||||
)}
|
||||
{isFeatureEnabled(Feature.DeviceFlow) && (
|
||||
<GridItem lg={8} sm={6}>
|
||||
<Controller
|
||||
|
||||
@ -14,6 +14,7 @@ export enum Feature {
|
||||
Organizations = "ORGANIZATION",
|
||||
OpenId4VCI = "OID4VC_VCI",
|
||||
QuickTheme = "QUICK_THEME",
|
||||
StandardTokenExchangeV2 = "TOKEN_EXCHANGE_STANDARD_V2",
|
||||
}
|
||||
|
||||
export default function useIsFeatureEnabled() {
|
||||
|
||||
@ -86,6 +86,8 @@ public final class OIDCConfigAttributes {
|
||||
public static final String FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED = "frontchannel.logout.session.required";
|
||||
|
||||
public static final String POST_LOGOUT_REDIRECT_URIS = "post.logout.redirect.uris";
|
||||
|
||||
public static final String STANDARD_TOKEN_EXCHANGE_ENABLED = "standard.token.exchange.enabled";
|
||||
|
||||
private OIDCConfigAttributes() {
|
||||
}
|
||||
|
||||
@ -230,6 +230,16 @@ public class OIDCAdvancedConfigWrapper extends AbstractClientConfigWrapper {
|
||||
setAttribute(OIDCConfigAttributes.USE_REFRESH_TOKEN_FOR_CLIENT_CREDENTIALS_GRANT, val);
|
||||
}
|
||||
|
||||
public boolean isStandardTokenExchangeEnabled() {
|
||||
String val = getAttribute(OIDCConfigAttributes.STANDARD_TOKEN_EXCHANGE_ENABLED, "false");
|
||||
return Boolean.parseBoolean(val);
|
||||
}
|
||||
|
||||
public void setStandardTokenExchangeEnabled(boolean enable) {
|
||||
String val = String.valueOf(enable);
|
||||
setAttribute(OIDCConfigAttributes.STANDARD_TOKEN_EXCHANGE_ENABLED, val);
|
||||
}
|
||||
|
||||
public String getTlsClientAuthSubjectDn() {
|
||||
return getAttribute(X509ClientAuthenticator.ATTR_SUBJECT_DN);
|
||||
}
|
||||
|
||||
@ -79,6 +79,12 @@ public class StandardTokenExchangeProvider extends AbstractTokenExchangeProvider
|
||||
Cors cors = context.getCors();
|
||||
EventBuilder event = context.getEvent();
|
||||
|
||||
if(!OIDCAdvancedConfigWrapper.fromClientModel(context.getClient()).isStandardTokenExchangeEnabled()) {
|
||||
event.detail(Details.REASON, "Standard token exchange is not enabled for the requested client");
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Standard token exchange is not enabled for the requested client", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
String subjectToken = context.getParams().getSubjectToken();
|
||||
if (subjectToken == null) {
|
||||
event.detail(Details.REASON, "subject_token parameter not provided");
|
||||
|
||||
@ -454,6 +454,19 @@ public class StandardTokenExchangeV2Test extends AbstractKeycloakTest {
|
||||
testExchange();
|
||||
testingClient.disableFeature(Profile.Feature.DYNAMIC_SCOPES);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UncaughtServerErrorExpected
|
||||
public void testExchangeDisabledOnClient() throws Exception {
|
||||
oauth.realm(TEST);
|
||||
String accessToken = resourceOwnerLogin("john", "password", "subject-client", "secret");
|
||||
{
|
||||
AccessTokenResponse response = tokenExchange(accessToken, "disabled-requester-client", "secret", null, null);
|
||||
org.junit.Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
|
||||
org.junit.Assert.assertEquals(OAuthErrorException.INVALID_REQUEST, response.getError());
|
||||
org.junit.Assert.assertEquals("Standard token exchange is not enabled for the requested client", response.getErrorDescription());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsents() throws Exception {
|
||||
|
||||
@ -707,6 +707,7 @@
|
||||
"oidc.ciba.grant.enabled" : "false",
|
||||
"client.secret.creation.time" : "1739807936",
|
||||
"backchannel.logout.session.required" : "true",
|
||||
"standard.token.exchange.enabled":"true",
|
||||
"frontchannel.logout.session.required" : "true",
|
||||
"post.logout.redirect.uris" : "+",
|
||||
"oauth2.device.authorization.grant.enabled" : "false",
|
||||
@ -718,6 +719,49 @@
|
||||
"nodeReRegistrationTimeout" : -1,
|
||||
"defaultClientScopes" : [ "service_account", "acr", "roles", "basic" ],
|
||||
"optionalClientScopes" : [ ]
|
||||
}, {
|
||||
"id" : "54b8e1b4-e912-4821-9335-3f0c0d2f4a2d",
|
||||
"clientId" : "disabled-requester-client",
|
||||
"name" : "",
|
||||
"description" : "",
|
||||
"rootUrl" : "",
|
||||
"adminUrl" : "",
|
||||
"baseUrl" : "",
|
||||
"surrogateAuthRequired" : false,
|
||||
"enabled" : true,
|
||||
"alwaysDisplayInConsole" : false,
|
||||
"clientAuthenticatorType" : "client-secret",
|
||||
"secret" : "secret",
|
||||
"redirectUris" : [ "/*" ],
|
||||
"webOrigins" : [ "/*" ],
|
||||
"notBefore" : 0,
|
||||
"bearerOnly" : false,
|
||||
"consentRequired" : false,
|
||||
"standardFlowEnabled" : true,
|
||||
"implicitFlowEnabled" : false,
|
||||
"directAccessGrantsEnabled" : true,
|
||||
"serviceAccountsEnabled" : true,
|
||||
"publicClient" : false,
|
||||
"frontchannelLogout" : true,
|
||||
"protocol" : "openid-connect",
|
||||
"attributes" : {
|
||||
"realm_client" : "false",
|
||||
"oidc.ciba.grant.enabled" : "false",
|
||||
"client.secret.creation.time" : "1732884723",
|
||||
"backchannel.logout.session.required" : "true",
|
||||
"standard.token.exchange.enabled":"false",
|
||||
"post.logout.redirect.uris" : "+",
|
||||
"frontchannel.logout.session.required" : "true",
|
||||
"oauth2.device.authorization.grant.enabled" : "false",
|
||||
"display.on.consent.screen" : "false",
|
||||
"use.jwks.url" : "false",
|
||||
"backchannel.logout.revoke.offline.tokens" : "false"
|
||||
},
|
||||
"authenticationFlowBindingOverrides" : { },
|
||||
"fullScopeAllowed" : false,
|
||||
"nodeReRegistrationTimeout" : -1,
|
||||
"defaultClientScopes" : [ "service_account", "acr", "default-scope1", "roles", "basic" ],
|
||||
"optionalClientScopes" : [ "optional-scope2" ]
|
||||
}, {
|
||||
"id" : "a11ebbaf-c0fd-46df-9fe7-64e94ac8d945",
|
||||
"clientId" : "realm-management",
|
||||
@ -780,6 +824,7 @@
|
||||
"oidc.ciba.grant.enabled" : "false",
|
||||
"client.secret.creation.time" : "1732884723",
|
||||
"backchannel.logout.session.required" : "true",
|
||||
"standard.token.exchange.enabled":"true",
|
||||
"post.logout.redirect.uris" : "+",
|
||||
"frontchannel.logout.session.required" : "true",
|
||||
"oauth2.device.authorization.grant.enabled" : "false",
|
||||
@ -820,6 +865,7 @@
|
||||
"realm_client" : "false",
|
||||
"oidc.ciba.grant.enabled" : "false",
|
||||
"backchannel.logout.session.required" : "true",
|
||||
"standard.token.exchange.enabled":"true",
|
||||
"frontchannel.logout.session.required" : "true",
|
||||
"oauth2.device.authorization.grant.enabled" : "false",
|
||||
"display.on.consent.screen" : "false",
|
||||
@ -909,6 +955,7 @@
|
||||
"oidc.ciba.grant.enabled" : "false",
|
||||
"client.secret.creation.time" : "1739806499",
|
||||
"backchannel.logout.session.required" : "true",
|
||||
"standard.token.exchange.enabled":"true",
|
||||
"post.logout.redirect.uris" : "+",
|
||||
"frontchannel.logout.session.required" : "true",
|
||||
"oauth2.device.authorization.grant.enabled" : "false",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user