[KEYCLOAK-8575] oidc idp basic auth (#6268)

* [KEYCLOAK-8575] Allow to choose between basic auth and form auth for oidc idp

* uncomment ui and add tests

* move basic auth to abstract identity provider (except for getting refresh tokens)

* removed duplications
This commit is contained in:
rradillen 2019-09-19 14:36:16 +02:00 committed by Marek Posolda
parent b833ce9dd3
commit b71198af9f
7 changed files with 212 additions and 126 deletions

View file

@ -27,6 +27,7 @@ import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient; import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
@ -35,6 +36,7 @@ import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder; import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity; import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.keycloak.common.util.Base64;
import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -46,15 +48,12 @@ import java.io.StringWriter;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
import org.apache.http.client.methods.HttpDelete;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -133,6 +132,12 @@ public class SimpleHttp {
return this; return this;
} }
public SimpleHttp authBasic(final String username, final String password) {
final String basicCredentials = String.format("%s:%s", username, password);
header("Authorization", "Basic " + Base64.encodeBytes(basicCredentials.getBytes()));
return this;
}
public SimpleHttp acceptJson() { public SimpleHttp acceptJson() {
if (headers == null || !headers.containsKey("Accept")) { if (headers == null || !headers.containsKey("Accept")) {
header("Accept", "application/json"); header("Accept", "application/json");

View file

@ -25,8 +25,8 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ExchangeExternalToken; import org.keycloak.broker.provider.ExchangeExternalToken;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken; import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
@ -47,10 +47,10 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint; import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
@ -465,6 +465,9 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
.param(OAuth2Constants.CLIENT_ASSERTION, jws); .param(OAuth2Constants.CLIENT_ASSERTION, jws);
} else { } else {
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) { try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
if (getConfig().isBasicAuthentication()) {
return tokenRequest.authBasic(getConfig().getClientId(), vaultStringSecret.get().orElse(getConfig().getClientSecret()));
}
return tokenRequest return tokenRequest
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId()) .param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, vaultStringSecret.get().orElse(getConfig().getClientSecret())); .param(OAUTH2_PARAMETER_CLIENT_SECRET, vaultStringSecret.get().orElse(getConfig().getClientSecret()));

View file

@ -100,6 +100,10 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel {
return false; return false;
} }
public boolean isBasicAuthentication(){
return getClientAuthMethod().equals(OIDCLoginProtocol.CLIENT_SECRET_BASIC);
}
public boolean isUiLocales() { public boolean isUiLocales() {
return Boolean.valueOf(getConfig().get("uiLocales")); return Boolean.valueOf(getConfig().get("uiLocales"));
} }

View file

@ -40,6 +40,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.JsonWebToken;
@ -94,41 +95,20 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return new OIDCEndpoint(callback, realm, event); return new OIDCEndpoint(callback, realm, event);
} }
protected class OIDCEndpoint extends Endpoint { /**
public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) { * Returns access token response as a string from a refresh token invocation on the remote OIDC broker
super(callback, realm, event); *
* @param session
* @param userSession
* @return
*/
public String refreshTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
String refreshToken = userSession.getNote(FEDERATED_REFRESH_TOKEN);
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
return getRefreshTokenRequest(session, refreshToken, getConfig().getClientId(), vaultStringSecret.get().orElse(getConfig().getClientSecret())).asString();
} catch (IOException e) {
throw new RuntimeException(e);
} }
@GET
@Path("logout_response")
public Response logoutResponse(@QueryParam("state") String state) {
if (state == null){
logger.error("no state parameter returned");
EventBuilder event = new EventBuilder(realm, session, clientConnection);
event.event(EventType.LOGOUT);
event.error(Errors.USER_SESSION_NOT_FOUND);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
UserSessionModel userSession = session.sessions().getUserSession(realm, state);
if (userSession == null) {
logger.error("no valid user session");
EventBuilder event = new EventBuilder(realm, session, clientConnection);
event.event(EventType.LOGOUT);
event.error(Errors.USER_SESSION_NOT_FOUND);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
logger.error("usersession in different state");
EventBuilder event = new EventBuilder(realm, session, clientConnection);
event.event(EventType.LOGOUT);
event.error(Errors.USER_SESSION_NOT_FOUND);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.SESSION_NOT_ACTIVE);
}
return AuthenticationManager.finishBrowserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers);
}
} }
@Override @Override
@ -180,50 +160,6 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
} }
} }
/**
* Returns access token response as a string from a refresh token invocation on the remote OIDC broker
*
* @param session
* @param userSession
* @return
*/
public String refreshTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
String refreshToken = userSession.getNote(FEDERATED_REFRESH_TOKEN);
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
return SimpleHttp.doPost(getConfig().getTokenUrl(), session)
.param("refresh_token", refreshToken)
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, vaultStringSecret.get().orElse(getConfig().getClientSecret())).asString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String getIDTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
String tokenExpirationString = userSession.getNote(FEDERATED_TOKEN_EXPIRATION);
long exp = tokenExpirationString == null ? 0 : Long.parseLong(tokenExpirationString);
int currentTime = Time.currentTime();
if (exp > 0 && currentTime > exp) {
String response = refreshTokenForLogout(session, userSession);
AccessTokenResponse tokenResponse = null;
try {
tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
return tokenResponse.getIdToken();
} else {
return userSession.getNote(FEDERATED_ID_TOKEN);
}
}
protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
}
@Override @Override
protected Response exchangeStoredToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) { protected Response exchangeStoredToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm()); FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
@ -240,11 +176,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
if (tokenResponse.getRefreshToken() == null) { if (tokenResponse.getRefreshToken() == null) {
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject); return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
} }
String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session) String response = getRefreshTokenRequest(session, tokenResponse.getRefreshToken(),
.param("refresh_token", tokenResponse.getRefreshToken()) getConfig().getClientId(), vaultStringSecret.get().orElse(getConfig().getClientSecret())).asString();
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, vaultStringSecret.get().orElse(getConfig().getClientSecret())).asString();
if (response.contains("error")) { if (response.contains("error")) {
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response); logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
model.setToken(null); model.setToken(null);
@ -292,6 +225,44 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
} }
} }
private String getIDTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
String tokenExpirationString = userSession.getNote(FEDERATED_TOKEN_EXPIRATION);
long exp = tokenExpirationString == null ? 0 : Long.parseLong(tokenExpirationString);
int currentTime = Time.currentTime();
if (exp > 0 && currentTime > exp) {
String response = refreshTokenForLogout(session, userSession);
AccessTokenResponse tokenResponse = null;
try {
tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
return tokenResponse.getIdToken();
} else {
return userSession.getNote(FEDERATED_ID_TOKEN);
}
}
protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
}
protected SimpleHttp getRefreshTokenRequest(KeycloakSession session, String refreshToken, String clientId, String clientSecret) {
if (OIDCLoginProtocol.CLIENT_SECRET_BASIC.equals(getConfig().getClientAuthMethod())) {
return SimpleHttp.doPost(getConfig().getTokenUrl(), session)
.param("refresh_token", refreshToken)
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
.authBasic(clientId, clientSecret);
}
return SimpleHttp.doPost(getConfig().getTokenUrl(), session)
.param("refresh_token", refreshToken)
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
.param(OAUTH2_PARAMETER_CLIENT_ID, clientId)
.param(OAUTH2_PARAMETER_CLIENT_SECRET, clientSecret);
}
@Override @Override
protected Response exchangeSessionToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) { protected Response exchangeSessionToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
String refreshToken = tokenUserSession.getNote(FEDERATED_REFRESH_TOKEN); String refreshToken = tokenUserSession.getNote(FEDERATED_REFRESH_TOKEN);
@ -317,11 +288,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
event.success(); event.success();
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build(); return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
} }
String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session) String response = getRefreshTokenRequest(session, refreshToken, getConfig().getClientId(), vaultStringSecret.get().orElse(getConfig().getClientSecret())).asString();
.param("refresh_token", refreshToken)
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, vaultStringSecret.get().orElse(getConfig().getClientSecret())).asString();
if (response.contains("error")) { if (response.contains("error")) {
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response); logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
event.detail(Details.REASON, "requested_issuer token expired"); event.detail(Details.REASON, "requested_issuer token expired");
@ -347,6 +314,42 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
} }
} }
protected class OIDCEndpoint extends Endpoint {
public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
super(callback, realm, event);
}
@GET
@Path("logout_response")
public Response logoutResponse(@QueryParam("state") String state) {
if (state == null){
logger.error("no state parameter returned");
EventBuilder event = new EventBuilder(realm, session, clientConnection);
event.event(EventType.LOGOUT);
event.error(Errors.USER_SESSION_NOT_FOUND);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
UserSessionModel userSession = session.sessions().getUserSession(realm, state);
if (userSession == null) {
logger.error("no valid user session");
EventBuilder event = new EventBuilder(realm, session, clientConnection);
event.event(EventType.LOGOUT);
event.error(Errors.USER_SESSION_NOT_FOUND);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
logger.error("usersession in different state");
EventBuilder event = new EventBuilder(realm, session, clientConnection);
event.event(EventType.LOGOUT);
event.error(Errors.USER_SESSION_NOT_FOUND);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.SESSION_NOT_ACTIVE);
}
return AuthenticationManager.finishBrowserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers);
}
}
@Override @Override
public BrokeredIdentityContext getFederatedIdentity(String response) { public BrokeredIdentityContext getFederatedIdentity(String response) {

View file

@ -141,6 +141,42 @@ public class IdentityProviderTest extends AbstractAdminTest {
assertEquals(ComponentRepresentation.SECRET_VALUE, rep.getConfig().get("clientSecret")); assertEquals(ComponentRepresentation.SECRET_VALUE, rep.getConfig().get("clientSecret"));
} }
@Test
public void testCreateWithBasicAuth() {
IdentityProviderRepresentation newIdentityProvider = createRep("new-identity-provider", "oidc");
newIdentityProvider.getConfig().put("clientId", "clientId");
newIdentityProvider.getConfig().put("clientSecret", "some secret value");
newIdentityProvider.getConfig().put("clientAuthMethod",OIDCLoginProtocol.CLIENT_SECRET_BASIC);
create(newIdentityProvider);
IdentityProviderResource identityProviderResource = realm.identityProviders().get("new-identity-provider");
assertNotNull(identityProviderResource);
IdentityProviderRepresentation representation = identityProviderResource.toRepresentation();
assertNotNull(representation);
assertNotNull(representation.getInternalId());
assertEquals("new-identity-provider", representation.getAlias());
assertEquals("oidc", representation.getProviderId());
assertEquals("clientId", representation.getConfig().get("clientId"));
assertEquals(ComponentRepresentation.SECRET_VALUE, representation.getConfig().get("clientSecret"));
assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, representation.getConfig().get("clientAuthMethod"));
assertTrue(representation.isEnabled());
assertFalse(representation.isStoreToken());
assertFalse(representation.isTrustEmail());
assertEquals("some secret value", testingClient.testing("admin-client-test").getIdentityProviderConfig("new-identity-provider").get("clientSecret"));
IdentityProviderRepresentation rep = realm.identityProviders().findAll().stream().filter(i -> i.getAlias().equals("new-identity-provider")).findFirst().get();
assertEquals(ComponentRepresentation.SECRET_VALUE, rep.getConfig().get("clientSecret"));
}
@Test @Test
public void testCreateWithJWT() { public void testCreateWithJWT() {
IdentityProviderRepresentation newIdentityProvider = createRep("new-identity-provider", "oidc"); IdentityProviderRepresentation newIdentityProvider = createRep("new-identity-provider", "oidc");

View file

@ -0,0 +1,35 @@
package org.keycloak.testsuite.broker;
import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.testsuite.arquillian.SuiteContext;
import java.util.List;
import java.util.Map;
import static org.keycloak.testsuite.broker.BrokerTestConstants.*;
import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider;
public class KcOidcBrokerClientSecretBasicAuthTest extends KcOidcBrokerTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerConfigurationWithBasicAuthAuthentication();
}
private class KcOidcBrokerConfigurationWithBasicAuthAuthentication extends KcOidcBrokerConfiguration {
@Override
public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) {
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
Map<String, String> config = idp.getConfig();
applyDefaultConfiguration(suiteContext, config);
config.put("clientAuthMethod", OIDCLoginProtocol.CLIENT_SECRET_BASIC);
return idp;
}
}
}

View file

@ -174,7 +174,7 @@
ng-model="identityProvider.config.clientAuthMethod" ng-model="identityProvider.config.clientAuthMethod"
required> required>
<option id="clientAuth_post" name="clientAuth" value="client_secret_post" selected>{{:: 'client-auth.client_secret_post' | translate}}</option> <option id="clientAuth_post" name="clientAuth" value="client_secret_post" selected>{{:: 'client-auth.client_secret_post' | translate}}</option>
<!-- <option id="clientAuth_basic" name="clientAuth" value="client_secret_basic">{{:: 'client-auth.client_secret_basic' | translate}}</option> --> <option id="clientAuth_basic" name="clientAuth" value="client_secret_basic">{{:: 'client-auth.client_secret_basic' | translate}}</option>
<option id="clientAuth_secret_jwt" name="clientAuth" value="client_secret_jwt">{{:: 'client-auth.client_secret_jwt' | translate}}</option> <option id="clientAuth_secret_jwt" name="clientAuth" value="client_secret_jwt">{{:: 'client-auth.client_secret_jwt' | translate}}</option>
<option id="clientAuth_privatekey_jwt" name="clientAuth" value="private_key_jwt">{{:: 'client-auth.private_key_jwt' | translate}}</option> <option id="clientAuth_privatekey_jwt" name="clientAuth" value="private_key_jwt">{{:: 'client-auth.private_key_jwt' | translate}}</option>
</select> </select>