Skip to content

Commit

Permalink
Allow customization of aud claim with JWT Authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
justin-tay authored and pedroigor committed Oct 31, 2023
1 parent 511fc76 commit 3ff0476
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
13 changes: 10 additions & 3 deletions js/apps/admin-ui/cypress/e2e/identity_providers_oidc_test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,25 @@ describe("OIDC identity provider test", () => {
ClientAuthentication.basicAuth,
);
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
ClientAuthentication.jwt,
ClientAuthentication.post,
);
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
ClientAuthentication.jwtPrivKey,
ClientAuthentication.jwt,
);
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
ClientAuthentication.post,
ClientAuthentication.jwtPrivKey,
);
//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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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")
Expand Down
6 changes: 4 additions & 2 deletions js/apps/admin-ui/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
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;
Expand Down Expand Up @@ -427,7 +428,11 @@ protected JsonWebToken generateToken() {
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,15 @@ public String getClientAssertionSigningAlg() {
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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}

}

0 comments on commit 3ff0476

Please sign in to comment.