Allow customization of aud claim with JWT Authentication

Closes #21445
This commit is contained in:
Justin Tay 2023-08-14 14:30:40 +08:00 committed by Pedro Igor
parent 511fc76d50
commit 3ff0476cc3
8 changed files with 130 additions and 11 deletions

View file

@ -46,6 +46,9 @@ image:images/oidc-add-identity-provider.png[Add Identity Provider]
|Signature algorithm to create JWT assertion as client authentication.
In the case of JWT signed with private key or Client secret as jwt, it is required. If no algorithm is specified, the following algorithm is adapted. `RS256` is adapted in the case of JWT signed with private key. `HS256` is adapted in the case of Client secret as jwt.
|Client Assertion Audience
|The audience to use for the client assertion. The default value is the IDP's token endpoint URL.
|Issuer
|{project_name} validates issuer claims, in responses from the IDP, against this value.

View file

@ -83,19 +83,26 @@ describe("OIDC identity provider test", () => {
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
ClientAuthentication.basicAuth,
);
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
ClientAuthentication.post,
);
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
ClientAuthentication.jwt,
);
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
ClientAuthentication.jwtPrivKey,
);
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
ClientAuthentication.post,
);
//Client assertion signature algorithm
Object.entries(ClientAssertionSigningAlg).forEach(([, value]) => {
providerBaseAdvancedSettingsPage.assertOIDCClientAuthSignAlg(value);
});
//Client assertion audience
providerBaseAdvancedSettingsPage.typeClientAssertionAudience(
"http://localhost:8180",
);
providerBaseAdvancedSettingsPage.assertClientAssertionAudienceInputEqual(
"http://localhost:8180",
);
//OIDC Advanced Settings
providerBaseAdvancedSettingsPage.assertOIDCSettingsAdvancedSwitches();
providerBaseAdvancedSettingsPage.selectPromptOption(PromptSelect.none);

View file

@ -29,10 +29,10 @@ export enum PromptSelect {
}
export enum ClientAuthentication {
post = "Client secret sent as basic auth",
basicAuth = "Client secret as jwt",
jwt = "JWT signed with private key",
jwtPrivKey = "Client secret sent as post",
post = "Client secret sent as post",
basicAuth = "Client secret sent as basic auth",
jwt = "JWT signed with client secret",
jwtPrivKey = "JWT signed with private key",
}
export enum ClientAssertionSigningAlg {
@ -84,6 +84,7 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
#pkceMethod = "#pkceMethod";
#clientAuth = "#clientAuthentication";
#clientAssertionSigningAlg = "#clientAssertionSigningAlg";
#clientAssertionAudienceInput = "#clientAssertionAudience";
public clickSaveBtn() {
cy.findByTestId(this.#saveBtn).click();
@ -187,6 +188,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
return this;
}
public typeClientAssertionAudience(text: string) {
cy.get(this.#clientAssertionAudienceInput).type(text).blur();
return this;
}
public selectSyncModeOption(syncModeOption: SyncModeOption) {
cy.get(this.#syncModeSelect).click();
super.clickSelectMenuItem(
@ -314,6 +320,13 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
return this;
}
public assertClientAssertionAudienceInputEqual(text: string) {
cy.get(this.#clientAssertionAudienceInput)
.should("have.value", text)
.parent();
return this;
}
public assertOIDCUrl(url: string) {
cy.findByTestId("jump-link-openid-connect-settings").click();
cy.findByTestId(url + "Url")

View file

@ -2894,9 +2894,10 @@
"clientAuthentications": {
"client_secret_post": "Client secret sent as post",
"client_secret_basic": "Client secret sent as basic auth",
"client_secret_jwt": "Client secret as jwt",
"client_secret_jwt": "JWT signed with client secret",
"private_key_jwt": "JWT signed with private key"
},
"clientAssertionAudience": "Client assertion audience",
"clientAssertionSigningAlg": "Client assertion signature algorithm",
"algorithmNotSpecified": "Algorithm not specified",
"acceptsPromptNone": "Accepts prompt=none forward from client",
@ -2979,7 +2980,8 @@
"attributeConsumingServiceNameHelp": "Name of the Attribute Consuming Service profile to advertise in the SP metadata.",
"forwardParametersHelp": "Non OpenID Connect/OAuth standard query parameters to be forwarded to external IDP from the initial application request to Authorization Endpoint. Multiple parameters can be entered, separated by comma (,).",
"clientAuthenticationHelp": "The client authentication method (cfr. https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication). In case of JWT signed with private key, the realm private key is used.",
"clientAssertionSigningAlgHelp": "Signature algorithm to create JWT assertion as client authentication. In the case of JWT signed with private key or Client secret as jwt, it is required. If no algorithm is specified, the following algorithm is adapted. RS256 is adapted in the case of JWT signed with private key. HS256 is adapted in the case of Client secret as jwt.",
"clientAssertionAudienceHelp": "The audience to use for the client assertion. The default value is the IDP's token endpoint URL.",
"clientAssertionSigningAlgHelp": "Signature algorithm to create JWT assertion as client authentication. In the case of JWT signed with private key or JWT signed with client secret, it is required. If no algorithm is specified, the following algorithm is adapted. RS256 is adapted in the case of JWT signed with private key. HS256 is adapted in the case of JWT signed with client secret.",
"storeTokensHelp": "Enable/disable if tokens must be stored after authenticating users.",
"storedTokensReadableHelp": "Enable/disable if new users can read any stored tokens. This assigns the broker.read-token role.",
"accountLinkingOnlyHelp": "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",

View file

@ -12,6 +12,7 @@ import { HelpItem } from "ui-shared";
import { ClientIdSecret } from "../component/ClientIdSecret";
import { sortProviders } from "../../util";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { TextField } from "../component/TextField";
const clientAuthentications = [
"client_secret_post",
@ -123,6 +124,13 @@ export const OIDCAuthentication = ({ create = true }: { create?: boolean }) => {
)}
/>
</FormGroup>
{(clientAuthMethod === "private_key_jwt" ||
clientAuthMethod === "client_secret_jwt") && (
<TextField
field="config.clientAssertionAudience"
label="clientAssertionAudience"
/>
)}
</>
);
};

View file

@ -62,6 +62,7 @@ import org.keycloak.services.Urls;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.utils.StringUtil;
import org.keycloak.vault.VaultStringSecret;
import javax.crypto.SecretKey;
@ -427,7 +428,11 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
jwt.type(OAuth2Constants.JWT);
jwt.issuer(getConfig().getClientId());
jwt.subject(getConfig().getClientId());
jwt.audience(getConfig().getTokenUrl());
String audience = getConfig().getClientAssertionAudience();
if (StringUtil.isBlank(audience)) {
audience = getConfig().getTokenUrl();
}
jwt.audience(audience);
int expirationDelay = session.getContext().getRealm().getAccessCodeLifespan();
jwt.expiration(Time.currentTime() + expirationDelay);
jwt.issuedNow();

View file

@ -154,7 +154,15 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel {
public void setClientAssertionSigningAlg(String signingAlg) {
getConfig().put("clientAssertionSigningAlg", signingAlg);
}
public String getClientAssertionAudience() {
return getConfig().get("clientAssertionAudience");
}
public void setClientAssertionAudience(String audience) {
getConfig().put("clientAssertionAudience", audience);
}
@Override
public void validate(RealmModel realm) {
SslRequired sslRequired = realm.getSslRequired();

View file

@ -0,0 +1,73 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.broker;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.crypto.Algorithm;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation.KeyMetadataRepresentation;
import org.keycloak.testsuite.util.KeyUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_PROVIDER_ID;
import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider;
public class KcOidcBrokerPrivateKeyJwtCustomAudienceTest extends AbstractBrokerTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerConfigurationWithJWTAuthentication();
}
private class KcOidcBrokerConfigurationWithJWTAuthentication extends KcOidcBrokerConfiguration {
@Override
public List<ClientRepresentation> createProviderClients() {
List<ClientRepresentation> clientsRepList = super.createProviderClients();
log.info("Update provider clients to accept JWT authentication");
KeyMetadataRepresentation keyRep = KeyUtils.findActiveSigningKey(adminClient.realm(consumerRealmName()), Algorithm.RS256);
for (ClientRepresentation client: clientsRepList) {
client.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
if (client.getAttributes() == null) {
client.setAttributes(new HashMap<String, String>());
}
client.getAttributes().put(JWTClientAuthenticator.CERTIFICATE_ATTR, keyRep.getCertificate());
}
return clientsRepList;
}
@Override
public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) {
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
Map<String, String> config = idp.getConfig();
applyDefaultConfiguration(config, syncMode);
config.put("clientSecret", null);
config.put("clientAuthMethod", OIDCLoginProtocol.PRIVATE_KEY_JWT);
config.put("clientAssertionAudience", "https://localhost:8543/auth/realms/provider");
return idp;
}
}
}