parent
511fc76d50
commit
3ff0476cc3
8 changed files with 130 additions and 11 deletions
|
@ -46,6 +46,9 @@ image:images/oidc-add-identity-provider.png[Add Identity Provider]
|
||||||
|Signature algorithm to create JWT assertion as client authentication.
|
|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.
|
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
|
|Issuer
|
||||||
|{project_name} validates issuer claims, in responses from the IDP, against this value.
|
|{project_name} validates issuer claims, in responses from the IDP, against this value.
|
||||||
|
|
||||||
|
|
|
@ -83,19 +83,26 @@ describe("OIDC identity provider test", () => {
|
||||||
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
|
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
|
||||||
ClientAuthentication.basicAuth,
|
ClientAuthentication.basicAuth,
|
||||||
);
|
);
|
||||||
|
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
|
||||||
|
ClientAuthentication.post,
|
||||||
|
);
|
||||||
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
|
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
|
||||||
ClientAuthentication.jwt,
|
ClientAuthentication.jwt,
|
||||||
);
|
);
|
||||||
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
|
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
|
||||||
ClientAuthentication.jwtPrivKey,
|
ClientAuthentication.jwtPrivKey,
|
||||||
);
|
);
|
||||||
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
|
|
||||||
ClientAuthentication.post,
|
|
||||||
);
|
|
||||||
//Client assertion signature algorithm
|
//Client assertion signature algorithm
|
||||||
Object.entries(ClientAssertionSigningAlg).forEach(([, value]) => {
|
Object.entries(ClientAssertionSigningAlg).forEach(([, value]) => {
|
||||||
providerBaseAdvancedSettingsPage.assertOIDCClientAuthSignAlg(value);
|
providerBaseAdvancedSettingsPage.assertOIDCClientAuthSignAlg(value);
|
||||||
});
|
});
|
||||||
|
//Client assertion audience
|
||||||
|
providerBaseAdvancedSettingsPage.typeClientAssertionAudience(
|
||||||
|
"http://localhost:8180",
|
||||||
|
);
|
||||||
|
providerBaseAdvancedSettingsPage.assertClientAssertionAudienceInputEqual(
|
||||||
|
"http://localhost:8180",
|
||||||
|
);
|
||||||
//OIDC Advanced Settings
|
//OIDC Advanced Settings
|
||||||
providerBaseAdvancedSettingsPage.assertOIDCSettingsAdvancedSwitches();
|
providerBaseAdvancedSettingsPage.assertOIDCSettingsAdvancedSwitches();
|
||||||
providerBaseAdvancedSettingsPage.selectPromptOption(PromptSelect.none);
|
providerBaseAdvancedSettingsPage.selectPromptOption(PromptSelect.none);
|
||||||
|
|
|
@ -29,10 +29,10 @@ export enum PromptSelect {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ClientAuthentication {
|
export enum ClientAuthentication {
|
||||||
post = "Client secret sent as basic auth",
|
post = "Client secret sent as post",
|
||||||
basicAuth = "Client secret as jwt",
|
basicAuth = "Client secret sent as basic auth",
|
||||||
jwt = "JWT signed with private key",
|
jwt = "JWT signed with client secret",
|
||||||
jwtPrivKey = "Client secret sent as post",
|
jwtPrivKey = "JWT signed with private key",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ClientAssertionSigningAlg {
|
export enum ClientAssertionSigningAlg {
|
||||||
|
@ -84,6 +84,7 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
||||||
#pkceMethod = "#pkceMethod";
|
#pkceMethod = "#pkceMethod";
|
||||||
#clientAuth = "#clientAuthentication";
|
#clientAuth = "#clientAuthentication";
|
||||||
#clientAssertionSigningAlg = "#clientAssertionSigningAlg";
|
#clientAssertionSigningAlg = "#clientAssertionSigningAlg";
|
||||||
|
#clientAssertionAudienceInput = "#clientAssertionAudience";
|
||||||
|
|
||||||
public clickSaveBtn() {
|
public clickSaveBtn() {
|
||||||
cy.findByTestId(this.#saveBtn).click();
|
cy.findByTestId(this.#saveBtn).click();
|
||||||
|
@ -187,6 +188,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public typeClientAssertionAudience(text: string) {
|
||||||
|
cy.get(this.#clientAssertionAudienceInput).type(text).blur();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public selectSyncModeOption(syncModeOption: SyncModeOption) {
|
public selectSyncModeOption(syncModeOption: SyncModeOption) {
|
||||||
cy.get(this.#syncModeSelect).click();
|
cy.get(this.#syncModeSelect).click();
|
||||||
super.clickSelectMenuItem(
|
super.clickSelectMenuItem(
|
||||||
|
@ -314,6 +320,13 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public assertClientAssertionAudienceInputEqual(text: string) {
|
||||||
|
cy.get(this.#clientAssertionAudienceInput)
|
||||||
|
.should("have.value", text)
|
||||||
|
.parent();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public assertOIDCUrl(url: string) {
|
public assertOIDCUrl(url: string) {
|
||||||
cy.findByTestId("jump-link-openid-connect-settings").click();
|
cy.findByTestId("jump-link-openid-connect-settings").click();
|
||||||
cy.findByTestId(url + "Url")
|
cy.findByTestId(url + "Url")
|
||||||
|
|
|
@ -2894,9 +2894,10 @@
|
||||||
"clientAuthentications": {
|
"clientAuthentications": {
|
||||||
"client_secret_post": "Client secret sent as post",
|
"client_secret_post": "Client secret sent as post",
|
||||||
"client_secret_basic": "Client secret sent as basic auth",
|
"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"
|
"private_key_jwt": "JWT signed with private key"
|
||||||
},
|
},
|
||||||
|
"clientAssertionAudience": "Client assertion audience",
|
||||||
"clientAssertionSigningAlg": "Client assertion signature algorithm",
|
"clientAssertionSigningAlg": "Client assertion signature algorithm",
|
||||||
"algorithmNotSpecified": "Algorithm not specified",
|
"algorithmNotSpecified": "Algorithm not specified",
|
||||||
"acceptsPromptNone": "Accepts prompt=none forward from client",
|
"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.",
|
"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 (,).",
|
"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.",
|
"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.",
|
"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.",
|
"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",
|
"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",
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { HelpItem } from "ui-shared";
|
||||||
import { ClientIdSecret } from "../component/ClientIdSecret";
|
import { ClientIdSecret } from "../component/ClientIdSecret";
|
||||||
import { sortProviders } from "../../util";
|
import { sortProviders } from "../../util";
|
||||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||||
|
import { TextField } from "../component/TextField";
|
||||||
|
|
||||||
const clientAuthentications = [
|
const clientAuthentications = [
|
||||||
"client_secret_post",
|
"client_secret_post",
|
||||||
|
@ -123,6 +124,13 @@ export const OIDCAuthentication = ({ create = true }: { create?: boolean }) => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
{(clientAuthMethod === "private_key_jwt" ||
|
||||||
|
clientAuthMethod === "client_secret_jwt") && (
|
||||||
|
<TextField
|
||||||
|
field="config.clientAssertionAudience"
|
||||||
|
label="clientAssertionAudience"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -62,6 +62,7 @@ import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.managers.ClientSessionCode;
|
import org.keycloak.services.managers.ClientSessionCode;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
import org.keycloak.vault.VaultStringSecret;
|
import org.keycloak.vault.VaultStringSecret;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
@ -427,7 +428,11 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
jwt.type(OAuth2Constants.JWT);
|
jwt.type(OAuth2Constants.JWT);
|
||||||
jwt.issuer(getConfig().getClientId());
|
jwt.issuer(getConfig().getClientId());
|
||||||
jwt.subject(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();
|
int expirationDelay = session.getContext().getRealm().getAccessCodeLifespan();
|
||||||
jwt.expiration(Time.currentTime() + expirationDelay);
|
jwt.expiration(Time.currentTime() + expirationDelay);
|
||||||
jwt.issuedNow();
|
jwt.issuedNow();
|
||||||
|
|
|
@ -154,7 +154,15 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel {
|
||||||
public void setClientAssertionSigningAlg(String signingAlg) {
|
public void setClientAssertionSigningAlg(String signingAlg) {
|
||||||
getConfig().put("clientAssertionSigningAlg", signingAlg);
|
getConfig().put("clientAssertionSigningAlg", signingAlg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getClientAssertionAudience() {
|
||||||
|
return getConfig().get("clientAssertionAudience");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientAssertionAudience(String audience) {
|
||||||
|
getConfig().put("clientAssertionAudience", audience);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validate(RealmModel realm) {
|
public void validate(RealmModel realm) {
|
||||||
SslRequired sslRequired = realm.getSslRequired();
|
SslRequired sslRequired = realm.getSslRequired();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue