Merge remote-tracking branch 'upstream/master' into client-storage-spi
This commit is contained in:
commit
ddad1cb8af
35 changed files with 1217 additions and 21 deletions
|
@ -56,6 +56,7 @@ public class ClientRepresentation {
|
||||||
protected Boolean frontchannelLogout;
|
protected Boolean frontchannelLogout;
|
||||||
protected String protocol;
|
protected String protocol;
|
||||||
protected Map<String, String> attributes;
|
protected Map<String, String> attributes;
|
||||||
|
protected Map<String, String> authenticationFlowBindingOverrides;
|
||||||
protected Boolean fullScopeAllowed;
|
protected Boolean fullScopeAllowed;
|
||||||
protected Integer nodeReRegistrationTimeout;
|
protected Integer nodeReRegistrationTimeout;
|
||||||
protected Map<String, Integer> registeredNodes;
|
protected Map<String, Integer> registeredNodes;
|
||||||
|
@ -296,6 +297,14 @@ public class ClientRepresentation {
|
||||||
this.attributes = attributes;
|
this.attributes = attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getAuthenticationFlowBindingOverrides() {
|
||||||
|
return authenticationFlowBindingOverrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthenticationFlowBindingOverrides(Map<String, String> authenticationFlowBindingOverrides) {
|
||||||
|
this.authenticationFlowBindingOverrides = authenticationFlowBindingOverrides;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getNodeReRegistrationTimeout() {
|
public Integer getNodeReRegistrationTimeout() {
|
||||||
return nodeReRegistrationTimeout;
|
return nodeReRegistrationTimeout;
|
||||||
}
|
}
|
||||||
|
|
|
@ -345,6 +345,34 @@ public class ClientAdapter implements ClientModel {
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAuthenticationFlowBindingOverride(String name, String value) {
|
||||||
|
getDelegateForUpdate();
|
||||||
|
updated.setAuthenticationFlowBindingOverride(name, value);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAuthenticationFlowBindingOverride(String name) {
|
||||||
|
getDelegateForUpdate();
|
||||||
|
updated.removeAuthenticationFlowBindingOverride(name);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAuthenticationFlowBindingOverride(String name) {
|
||||||
|
if (isUpdated()) return updated.getAuthenticationFlowBindingOverride(name);
|
||||||
|
return cached.getAuthFlowBindings().get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getAuthenticationFlowBindingOverrides() {
|
||||||
|
if (isUpdated()) return updated.getAuthenticationFlowBindingOverrides();
|
||||||
|
Map<String, String> copy = new HashMap<String, String>();
|
||||||
|
copy.putAll(cached.getAuthFlowBindings());
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<ProtocolMapperModel> getProtocolMappers() {
|
public Set<ProtocolMapperModel> getProtocolMappers() {
|
||||||
if (isUpdated()) return updated.getProtocolMappers();
|
if (isUpdated()) return updated.getProtocolMappers();
|
||||||
|
|
|
@ -46,6 +46,7 @@ public class CachedClient extends AbstractRevisioned implements InRealm {
|
||||||
protected String registrationToken;
|
protected String registrationToken;
|
||||||
protected String protocol;
|
protected String protocol;
|
||||||
protected Map<String, String> attributes = new HashMap<String, String>();
|
protected Map<String, String> attributes = new HashMap<String, String>();
|
||||||
|
protected Map<String, String> authFlowBindings = new HashMap<String, String>();
|
||||||
protected boolean publicClient;
|
protected boolean publicClient;
|
||||||
protected boolean fullScopeAllowed;
|
protected boolean fullScopeAllowed;
|
||||||
protected boolean frontchannelLogout;
|
protected boolean frontchannelLogout;
|
||||||
|
@ -83,6 +84,7 @@ public class CachedClient extends AbstractRevisioned implements InRealm {
|
||||||
enabled = model.isEnabled();
|
enabled = model.isEnabled();
|
||||||
protocol = model.getProtocol();
|
protocol = model.getProtocol();
|
||||||
attributes.putAll(model.getAttributes());
|
attributes.putAll(model.getAttributes());
|
||||||
|
authFlowBindings.putAll(model.getAuthenticationFlowBindingOverrides());
|
||||||
notBefore = model.getNotBefore();
|
notBefore = model.getNotBefore();
|
||||||
frontchannelLogout = model.isFrontchannelLogout();
|
frontchannelLogout = model.isFrontchannelLogout();
|
||||||
publicClient = model.isPublicClient();
|
publicClient = model.isPublicClient();
|
||||||
|
@ -256,4 +258,8 @@ public class CachedClient extends AbstractRevisioned implements InRealm {
|
||||||
public boolean isUseTemplateMappers() {
|
public boolean isUseTemplateMappers() {
|
||||||
return useTemplateMappers;
|
return useTemplateMappers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getAuthFlowBindings() {
|
||||||
|
return authFlowBindings;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -270,6 +270,29 @@ public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAuthenticationFlowBindingOverride(String name, String value) {
|
||||||
|
entity.getAuthFlowBindings().put(name, value);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAuthenticationFlowBindingOverride(String name) {
|
||||||
|
entity.getAuthFlowBindings().remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAuthenticationFlowBindingOverride(String name) {
|
||||||
|
return entity.getAuthFlowBindings().get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getAuthenticationFlowBindingOverrides() {
|
||||||
|
Map<String, String> copy = new HashMap<>();
|
||||||
|
copy.putAll(entity.getAuthFlowBindings());
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAttribute(String name, String value) {
|
public void setAttribute(String name, String value) {
|
||||||
entity.getAttributes().put(name, value);
|
entity.getAttributes().put(name, value);
|
||||||
|
|
|
@ -119,6 +119,12 @@ public class ClientEntity {
|
||||||
@CollectionTable(name="CLIENT_ATTRIBUTES", joinColumns={ @JoinColumn(name="CLIENT_ID") })
|
@CollectionTable(name="CLIENT_ATTRIBUTES", joinColumns={ @JoinColumn(name="CLIENT_ID") })
|
||||||
protected Map<String, String> attributes = new HashMap<String, String>();
|
protected Map<String, String> attributes = new HashMap<String, String>();
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@MapKeyColumn(name="BINDING_NAME")
|
||||||
|
@Column(name="FLOW_ID", length = 4000)
|
||||||
|
@CollectionTable(name="CLIENT_AUTH_FLOW_BINDINGS", joinColumns={ @JoinColumn(name="CLIENT_ID") })
|
||||||
|
protected Map<String, String> authFlowBindings = new HashMap<String, String>();
|
||||||
|
|
||||||
@OneToMany(fetch = FetchType.LAZY, mappedBy = "client", cascade = CascadeType.REMOVE)
|
@OneToMany(fetch = FetchType.LAZY, mappedBy = "client", cascade = CascadeType.REMOVE)
|
||||||
Collection<ClientIdentityProviderMappingEntity> identityProviders = new ArrayList<ClientIdentityProviderMappingEntity>();
|
Collection<ClientIdentityProviderMappingEntity> identityProviders = new ArrayList<ClientIdentityProviderMappingEntity>();
|
||||||
|
|
||||||
|
@ -292,6 +298,14 @@ public class ClientEntity {
|
||||||
this.attributes = attributes;
|
this.attributes = attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getAuthFlowBindings() {
|
||||||
|
return authFlowBindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthFlowBindings(Map<String, String> authFlowBindings) {
|
||||||
|
this.authFlowBindings = authFlowBindings;
|
||||||
|
}
|
||||||
|
|
||||||
public String getProtocol() {
|
public String getProtocol() {
|
||||||
return protocol;
|
return protocol;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!--
|
||||||
|
~ * Copyright 2017 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.
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||||
|
|
||||||
|
<changeSet author="bburke@redhat.com" id="4.0.0-KEYCLOAK-6335">
|
||||||
|
<createTable tableName="CLIENT_AUTH_FLOW_BINDINGS">
|
||||||
|
<column name="CLIENT_ID" type="VARCHAR(36)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="FLOW_ID" type="VARCHAR(36)"/>
|
||||||
|
<column name="BINDING_NAME" type="VARCHAR(255)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
<addPrimaryKey columnNames="CLIENT_ID, BINDING_NAME" constraintName="C_CLI_FLOW_BIND" tableName="CLIENT_AUTH_FLOW_BINDINGS"/>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
|
@ -53,4 +53,5 @@
|
||||||
<include file="META-INF/jpa-changelog-3.4.0.xml"/>
|
<include file="META-INF/jpa-changelog-3.4.0.xml"/>
|
||||||
<include file="META-INF/jpa-changelog-3.4.1.xml"/>
|
<include file="META-INF/jpa-changelog-3.4.1.xml"/>
|
||||||
<include file="META-INF/jpa-changelog-3.4.2.xml"/>
|
<include file="META-INF/jpa-changelog-3.4.2.xml"/>
|
||||||
|
<include file="META-INF/jpa-changelog-4.0.0.xml"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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.models.utils;
|
||||||
|
|
||||||
|
import org.keycloak.models.AuthenticationFlowBindings;
|
||||||
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class AuthenticationFlowResolver {
|
||||||
|
|
||||||
|
public static AuthenticationFlowModel resolveBrowserFlow(AuthenticationSessionModel authSession) {
|
||||||
|
AuthenticationFlowModel flow = null;
|
||||||
|
ClientModel client = authSession.getClient();
|
||||||
|
String clientFlow = client.getAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING);
|
||||||
|
if (clientFlow != null) {
|
||||||
|
flow = authSession.getRealm().getAuthenticationFlowById(clientFlow);
|
||||||
|
if (flow == null) {
|
||||||
|
throw new ModelException("Client " + client.getClientId() + " has browser flow override, but this flow does not exist");
|
||||||
|
}
|
||||||
|
return flow;
|
||||||
|
}
|
||||||
|
return authSession.getRealm().getBrowserFlow();
|
||||||
|
}
|
||||||
|
public static AuthenticationFlowModel resolveDirectGrantFlow(AuthenticationSessionModel authSession) {
|
||||||
|
AuthenticationFlowModel flow = null;
|
||||||
|
ClientModel client = authSession.getClient();
|
||||||
|
String clientFlow = client.getAuthenticationFlowBindingOverride(AuthenticationFlowBindings.DIRECT_GRANT_BINDING);
|
||||||
|
if (clientFlow != null) {
|
||||||
|
flow = authSession.getRealm().getAuthenticationFlowById(clientFlow);
|
||||||
|
if (flow == null) {
|
||||||
|
throw new ModelException("Client " + client.getClientId() + " has direct grant flow override, but this flow does not exist");
|
||||||
|
}
|
||||||
|
return flow;
|
||||||
|
}
|
||||||
|
return authSession.getRealm().getDirectGrantFlow();
|
||||||
|
}
|
||||||
|
}
|
|
@ -494,6 +494,7 @@ public class ModelToRepresentation {
|
||||||
rep.setFrontchannelLogout(clientModel.isFrontchannelLogout());
|
rep.setFrontchannelLogout(clientModel.isFrontchannelLogout());
|
||||||
rep.setProtocol(clientModel.getProtocol());
|
rep.setProtocol(clientModel.getProtocol());
|
||||||
rep.setAttributes(clientModel.getAttributes());
|
rep.setAttributes(clientModel.getAttributes());
|
||||||
|
rep.setAuthenticationFlowBindingOverrides(clientModel.getAuthenticationFlowBindingOverrides());
|
||||||
rep.setFullScopeAllowed(clientModel.isFullScopeAllowed());
|
rep.setFullScopeAllowed(clientModel.isFullScopeAllowed());
|
||||||
rep.setBearerOnly(clientModel.isBearerOnly());
|
rep.setBearerOnly(clientModel.isBearerOnly());
|
||||||
rep.setConsentRequired(clientModel.isConsentRequired());
|
rep.setConsentRequired(clientModel.isConsentRequired());
|
||||||
|
|
|
@ -1084,6 +1084,17 @@ public class RepresentationToModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (resourceRep.getAuthenticationFlowBindingOverrides() != null) {
|
||||||
|
for (Map.Entry<String, String> entry : resourceRep.getAuthenticationFlowBindingOverrides().entrySet()) {
|
||||||
|
if (entry.getValue() == null || entry.getValue().trim().equals("")) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
client.setAuthenticationFlowBindingOverride(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (resourceRep.getRedirectUris() != null) {
|
if (resourceRep.getRedirectUris() != null) {
|
||||||
for (String redirectUri : resourceRep.getRedirectUris()) {
|
for (String redirectUri : resourceRep.getRedirectUris()) {
|
||||||
client.addRedirectUri(redirectUri);
|
client.addRedirectUri(redirectUri);
|
||||||
|
@ -1201,6 +1212,22 @@ public class RepresentationToModel {
|
||||||
resource.setAttribute(entry.getKey(), entry.getValue());
|
resource.setAttribute(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (rep.getAttributes() != null) {
|
||||||
|
for (Map.Entry<String, String> entry : rep.getAttributes().entrySet()) {
|
||||||
|
resource.setAttribute(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rep.getAuthenticationFlowBindingOverrides() != null) {
|
||||||
|
for (Map.Entry<String, String> entry : rep.getAuthenticationFlowBindingOverrides().entrySet()) {
|
||||||
|
if (entry.getValue() == null || entry.getValue().trim().equals("")) {
|
||||||
|
resource.removeAuthenticationFlowBindingOverride(entry.getKey());
|
||||||
|
} else {
|
||||||
|
resource.setAuthenticationFlowBindingOverride(entry.getKey(), entry.getValue());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (rep.getNotBefore() != null) {
|
if (rep.getNotBefore() != null) {
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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.models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines constants for authentication flow bindings. Strings used for lookup
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public interface AuthenticationFlowBindings {
|
||||||
|
String BROWSER_BINDING = "browser";
|
||||||
|
String DIRECT_GRANT_BINDING = "direct_grant";
|
||||||
|
}
|
|
@ -118,6 +118,18 @@ public interface ClientModel extends RoleContainerModel, ProtocolMapperContaine
|
||||||
String getAttribute(String name);
|
String getAttribute(String name);
|
||||||
Map<String, String> getAttributes();
|
Map<String, String> getAttributes();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get authentication flow binding override for this client. Allows client to override an authentication flow binding.
|
||||||
|
*
|
||||||
|
* @param binding examples are "browser", "direct_grant"
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getAuthenticationFlowBindingOverride(String binding);
|
||||||
|
public Map<String, String> getAuthenticationFlowBindingOverrides();
|
||||||
|
public void removeAuthenticationFlowBindingOverride(String binding);
|
||||||
|
public void setAuthenticationFlowBindingOverride(String binding, String flowId);
|
||||||
|
|
||||||
boolean isFrontchannelLogout();
|
boolean isFrontchannelLogout();
|
||||||
void setFrontchannelLogout(boolean flag);
|
void setFrontchannelLogout(boolean flag);
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,10 @@ public class OTPPolicy implements Serializable {
|
||||||
|
|
||||||
public static OTPPolicy DEFAULT_POLICY = new OTPPolicy(UserCredentialModel.TOTP, HmacOTP.HMAC_SHA1, 0, 6, 1, 30);
|
public static OTPPolicy DEFAULT_POLICY = new OTPPolicy(UserCredentialModel.TOTP, HmacOTP.HMAC_SHA1, 0, 6, 1, 30);
|
||||||
|
|
||||||
|
public String getAlgorithmKey() {
|
||||||
|
return algToKeyUriAlg.containsKey(algorithm) ? algToKeyUriAlg.get(algorithm) : algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,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.models.utils.AuthenticationFlowResolver;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
|
@ -646,7 +647,7 @@ public class AuthenticationProcessor {
|
||||||
AuthenticationProcessor processor = new AuthenticationProcessor();
|
AuthenticationProcessor processor = new AuthenticationProcessor();
|
||||||
processor.setAuthenticationSession(clone)
|
processor.setAuthenticationSession(clone)
|
||||||
.setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
|
.setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
|
||||||
.setFlowId(realm.getBrowserFlow().getId())
|
.setFlowId(AuthenticationFlowResolver.resolveBrowserFlow(clone).getId())
|
||||||
.setForwardedErrorMessage(reset.getErrorMessage())
|
.setForwardedErrorMessage(reset.getErrorMessage())
|
||||||
.setForwardedSuccessMessage(reset.getSuccessMessage())
|
.setForwardedSuccessMessage(reset.getSuccessMessage())
|
||||||
.setConnection(connection)
|
.setConnection(connection)
|
||||||
|
|
|
@ -39,6 +39,22 @@ import java.security.KeyStore;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* The default {@link HttpClientFactory} for {@link HttpClientProvider HttpClientProvider's} used by Keycloak for outbound HTTP calls.
|
||||||
|
* <p>
|
||||||
|
* The constructed clients can be configured via Keycloaks SPI configuration, e.g. {@code standalone.xml, standalone-ha.xml, domain.xml}.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* Examples for jboss-cli
|
||||||
|
* </p>
|
||||||
|
* <pre>
|
||||||
|
* {@code
|
||||||
|
*
|
||||||
|
* /subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:add(enabled=true)
|
||||||
|
* /subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.connection-pool-size,value=128)
|
||||||
|
* /subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.proxy-mappings,value=[".*\\.(google|googleapis)\\.com;http://www-proxy.acme.corp.com:8080",".*\\.acme\\.corp\\.com;NO_PROXY",".*;http://fallback:8080"])
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
* </p>
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class DefaultHttpClientFactory implements HttpClientFactory {
|
public class DefaultHttpClientFactory implements HttpClientFactory {
|
||||||
|
@ -127,6 +143,7 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
|
||||||
String clientKeystore = config.get("client-keystore");
|
String clientKeystore = config.get("client-keystore");
|
||||||
String clientKeystorePassword = config.get("client-keystore-password");
|
String clientKeystorePassword = config.get("client-keystore-password");
|
||||||
String clientPrivateKeyPassword = config.get("client-key-password");
|
String clientPrivateKeyPassword = config.get("client-key-password");
|
||||||
|
String[] proxyMappings = config.getArray("proxy-mappings");
|
||||||
|
|
||||||
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
|
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
|
||||||
boolean disableTrustManager = truststoreProvider == null || truststoreProvider.getTruststore() == null;
|
boolean disableTrustManager = truststoreProvider == null || truststoreProvider.getTruststore() == null;
|
||||||
|
@ -137,13 +154,15 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
|
||||||
: HttpClientBuilder.HostnameVerificationPolicy.valueOf(truststoreProvider.getPolicy().name());
|
: HttpClientBuilder.HostnameVerificationPolicy.valueOf(truststoreProvider.getPolicy().name());
|
||||||
|
|
||||||
HttpClientBuilder builder = new HttpClientBuilder();
|
HttpClientBuilder builder = new HttpClientBuilder();
|
||||||
|
|
||||||
builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
|
builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
|
||||||
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
|
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
|
||||||
.maxPooledPerRoute(maxPooledPerRoute)
|
.maxPooledPerRoute(maxPooledPerRoute)
|
||||||
.connectionPoolSize(connectionPoolSize)
|
.connectionPoolSize(connectionPoolSize)
|
||||||
.connectionTTL(connectionTTL, TimeUnit.MILLISECONDS)
|
.connectionTTL(connectionTTL, TimeUnit.MILLISECONDS)
|
||||||
.maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS)
|
.maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS)
|
||||||
.disableCookies(disableCookies);
|
.disableCookies(disableCookies)
|
||||||
|
.proxyMappings(ProxyMappings.valueOf(proxyMappings));
|
||||||
|
|
||||||
if (disableTrustManager) {
|
if (disableTrustManager) {
|
||||||
// TODO: is it ok to do away with disabling trust manager?
|
// TODO: is it ok to do away with disabling trust manager?
|
||||||
|
|
|
@ -52,7 +52,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class HttpClientBuilder {
|
public class HttpClientBuilder {
|
||||||
public static enum HostnameVerificationPolicy {
|
public enum HostnameVerificationPolicy {
|
||||||
/**
|
/**
|
||||||
* Hostname verification is not done on the server's certificate
|
* Hostname verification is not done on the server's certificate
|
||||||
*/
|
*/
|
||||||
|
@ -104,7 +104,7 @@ public class HttpClientBuilder {
|
||||||
protected long establishConnectionTimeout = -1;
|
protected long establishConnectionTimeout = -1;
|
||||||
protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS;
|
protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS;
|
||||||
protected boolean disableCookies = false;
|
protected boolean disableCookies = false;
|
||||||
|
protected ProxyMappings proxyMappings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Socket inactivity timeout
|
* Socket inactivity timeout
|
||||||
|
@ -208,6 +208,11 @@ public class HttpClientBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HttpClientBuilder proxyMappings(ProxyMappings proxyMappings) {
|
||||||
|
this.proxyMappings = proxyMappings;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static class VerifierWrapper implements X509HostnameVerifier {
|
static class VerifierWrapper implements X509HostnameVerifier {
|
||||||
protected HostnameVerifier verifier;
|
protected HostnameVerifier verifier;
|
||||||
|
@ -272,6 +277,7 @@ public class HttpClientBuilder {
|
||||||
tlsContext.init(null, null, null);
|
tlsContext.init(null, null, null);
|
||||||
sslsf = new SSLConnectionSocketFactory(tlsContext, verifier);
|
sslsf = new SSLConnectionSocketFactory(tlsContext, verifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestConfig requestConfig = RequestConfig.custom()
|
RequestConfig requestConfig = RequestConfig.custom()
|
||||||
.setConnectTimeout((int) establishConnectionTimeout)
|
.setConnectTimeout((int) establishConnectionTimeout)
|
||||||
.setSocketTimeout((int) socketTimeout).build();
|
.setSocketTimeout((int) socketTimeout).build();
|
||||||
|
@ -283,6 +289,11 @@ public class HttpClientBuilder {
|
||||||
.setMaxConnPerRoute(maxPooledPerRoute)
|
.setMaxConnPerRoute(maxPooledPerRoute)
|
||||||
.setConnectionTimeToLive(connectionTTL, connectionTTLUnit);
|
.setConnectionTimeToLive(connectionTTL, connectionTTLUnit);
|
||||||
|
|
||||||
|
|
||||||
|
if (proxyMappings != null && !proxyMappings.isEmpty()) {
|
||||||
|
builder.setRoutePlanner(new ProxyMappingsAwareRoutePlanner(proxyMappings));
|
||||||
|
}
|
||||||
|
|
||||||
if (maxConnectionIdleTime > 0) {
|
if (maxConnectionIdleTime > 0) {
|
||||||
// Will start background cleaner thread
|
// Will start background cleaner thread
|
||||||
builder.evictIdleConnections(maxConnectionIdleTime, maxConnectionIdleTimeUnit);
|
builder.evictIdleConnections(maxConnectionIdleTime, maxConnectionIdleTimeUnit);
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 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.connections.httpclient;
|
||||||
|
|
||||||
|
import org.apache.http.HttpHost;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ProxyMappings} describes an ordered mapping for hostname regex patterns to a {@link HttpHost} proxy.
|
||||||
|
* <p>
|
||||||
|
* Mappings can be created via {@link #valueOf(String...)} or {@link #valueOf(List)}.
|
||||||
|
* For a description of the mapping format see {@link ProxyMapping#valueOf(String)}
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||||
|
*/
|
||||||
|
public class ProxyMappings {
|
||||||
|
|
||||||
|
private static final ProxyMappings EMPTY_MAPPING = valueOf(Collections.emptyList());
|
||||||
|
|
||||||
|
private final List<ProxyMapping> entries;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link ProxyMappings} from the provided {@link ProxyMapping Entries}.
|
||||||
|
*
|
||||||
|
* @param entries
|
||||||
|
*/
|
||||||
|
public ProxyMappings(List<ProxyMapping> entries) {
|
||||||
|
this.entries = Collections.unmodifiableList(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link ProxyMappings} from the provided {@code List} of proxy mapping strings.
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* @param proxyMappings
|
||||||
|
*/
|
||||||
|
public static ProxyMappings valueOf(List<String> proxyMappings) {
|
||||||
|
|
||||||
|
if (proxyMappings == null || proxyMappings.isEmpty()) {
|
||||||
|
return EMPTY_MAPPING;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ProxyMapping> entries = proxyMappings.stream() //
|
||||||
|
.map(ProxyMapping::valueOf) //
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new ProxyMappings(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link ProxyMappings} from the provided {@code String[]} of proxy mapping strings.
|
||||||
|
*
|
||||||
|
* @param proxyMappings
|
||||||
|
* @return
|
||||||
|
* @see #valueOf(List)
|
||||||
|
* @see ProxyMapping#valueOf(String...)
|
||||||
|
*/
|
||||||
|
public static ProxyMappings valueOf(String... proxyMappings) {
|
||||||
|
|
||||||
|
if (proxyMappings == null || proxyMappings.length == 0) {
|
||||||
|
return EMPTY_MAPPING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valueOf(Arrays.asList(proxyMappings));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return this.entries.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param hostname
|
||||||
|
* @return the {@link HttpHost} proxy associated with the first matching hostname {@link Pattern}
|
||||||
|
* or {@literal null} if none matches.
|
||||||
|
*/
|
||||||
|
public HttpHost getProxyFor(String hostname) {
|
||||||
|
|
||||||
|
Objects.requireNonNull(hostname, "hostname");
|
||||||
|
|
||||||
|
return entries.stream() //
|
||||||
|
.filter(e -> e.matches(hostname)) //
|
||||||
|
.findFirst() //
|
||||||
|
.map(ProxyMapping::getProxy) //
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ProxyMapping} describes a Proxy Mapping with a Hostname {@link Pattern}
|
||||||
|
* that is mapped to a proxy {@link HttpHost}.
|
||||||
|
*/
|
||||||
|
public static class ProxyMapping {
|
||||||
|
|
||||||
|
public static final String NO_PROXY = "NO_PROXY";
|
||||||
|
private static final String DELIMITER = ";";
|
||||||
|
|
||||||
|
private final Pattern hostnamePattern;
|
||||||
|
|
||||||
|
private final HttpHost proxy;
|
||||||
|
|
||||||
|
public ProxyMapping(Pattern hostnamePattern, HttpHost proxy) {
|
||||||
|
this.hostnamePattern = hostnamePattern;
|
||||||
|
this.proxy = proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pattern getHostnamePattern() {
|
||||||
|
return hostnamePattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpHost getProxy() {
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matches(String hostname) {
|
||||||
|
return getHostnamePattern().matcher(hostname).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a mapping string into an {@link ProxyMapping}.
|
||||||
|
* <p>
|
||||||
|
* A proxy mapping string must have the following format: {@code hostnameRegex;www-proxy-uri}
|
||||||
|
* with semicolon as a delimiter.</p>
|
||||||
|
* <p>
|
||||||
|
* If no proxy should be used for a host pattern then use {@code NO_PROXY} as www-proxy-uri.
|
||||||
|
* </p>
|
||||||
|
* <p>Examples:
|
||||||
|
* <pre>
|
||||||
|
* {@code
|
||||||
|
*
|
||||||
|
* .*\.(google\.com|googleapis\.com);http://www-proxy.acme.corp.com:8080
|
||||||
|
* .*\.acme\.corp\.com;NO_PROXY
|
||||||
|
* .*;http://fallback:8080
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param mapping
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static ProxyMapping valueOf(String mapping) {
|
||||||
|
|
||||||
|
String[] mappingTokens = mapping.split(DELIMITER);
|
||||||
|
|
||||||
|
String hostPatternRegex = mappingTokens[0];
|
||||||
|
String proxyUriString = mappingTokens[1];
|
||||||
|
|
||||||
|
Pattern hostPattern = Pattern.compile(hostPatternRegex);
|
||||||
|
HttpHost proxyHost = toProxyHost(proxyUriString);
|
||||||
|
|
||||||
|
return new ProxyMapping(hostPattern, proxyHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpHost toProxyHost(String proxyUriString) {
|
||||||
|
|
||||||
|
if (NO_PROXY.equals(proxyUriString)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
URI uri = URI.create(proxyUriString);
|
||||||
|
return new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ProxyMapping{" +
|
||||||
|
"hostnamePattern=" + hostnamePattern +
|
||||||
|
", proxy=" + proxy +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 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.connections.httpclient;
|
||||||
|
|
||||||
|
import org.apache.http.HttpException;
|
||||||
|
import org.apache.http.HttpHost;
|
||||||
|
import org.apache.http.HttpRequest;
|
||||||
|
import org.apache.http.impl.conn.DefaultRoutePlanner;
|
||||||
|
import org.apache.http.impl.conn.DefaultSchemePortResolver;
|
||||||
|
import org.apache.http.protocol.HttpContext;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link DefaultRoutePlanner} that determines the proxy to use for a given target hostname by consulting
|
||||||
|
* the given {@link ProxyMappings}.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||||
|
* @see ProxyMappings
|
||||||
|
*/
|
||||||
|
public class ProxyMappingsAwareRoutePlanner extends DefaultRoutePlanner {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(ProxyMappingsAwareRoutePlanner.class);
|
||||||
|
|
||||||
|
private final ProxyMappings proxyMappings;
|
||||||
|
|
||||||
|
public ProxyMappingsAwareRoutePlanner(ProxyMappings proxyMappings) {
|
||||||
|
super(DefaultSchemePortResolver.INSTANCE);
|
||||||
|
this.proxyMappings = proxyMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HttpHost determineProxy(HttpHost target, HttpRequest request, HttpContext context) throws HttpException {
|
||||||
|
|
||||||
|
HttpHost proxy = proxyMappings.getProxyFor(target.getHostName());
|
||||||
|
LOG.debugf("Returning proxy=%s for targetHost=%s", proxy, target.getHostName());
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.models.utils.AuthenticationFlowResolver;
|
||||||
import org.keycloak.protocol.LoginProtocol.Error;
|
import org.keycloak.protocol.LoginProtocol.Error;
|
||||||
import org.keycloak.services.ErrorPageException;
|
import org.keycloak.services.ErrorPageException;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
@ -107,7 +108,7 @@ public abstract class AuthorizationEndpointBase {
|
||||||
* @return response to be returned to the browser
|
* @return response to be returned to the browser
|
||||||
*/
|
*/
|
||||||
protected Response handleBrowserAuthenticationRequest(AuthenticationSessionModel authSession, LoginProtocol protocol, boolean isPassive, boolean redirectToAuthentication) {
|
protected Response handleBrowserAuthenticationRequest(AuthenticationSessionModel authSession, LoginProtocol protocol, boolean isPassive, boolean redirectToAuthentication) {
|
||||||
AuthenticationFlowModel flow = getAuthenticationFlow();
|
AuthenticationFlowModel flow = getAuthenticationFlow(authSession);
|
||||||
String flowId = flow.getId();
|
String flowId = flow.getId();
|
||||||
AuthenticationProcessor processor = createProcessor(authSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
|
AuthenticationProcessor processor = createProcessor(authSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
|
||||||
event.detail(Details.CODE_ID, authSession.getParentSession().getId());
|
event.detail(Details.CODE_ID, authSession.getParentSession().getId());
|
||||||
|
@ -149,8 +150,8 @@ public abstract class AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AuthenticationFlowModel getAuthenticationFlow() {
|
protected AuthenticationFlowModel getAuthenticationFlow(AuthenticationSessionModel authSession) {
|
||||||
return realm.getBrowserFlow();
|
return AuthenticationFlowResolver.resolveBrowserFlow(authSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void checkSsl() {
|
protected void checkSsl() {
|
||||||
|
|
|
@ -88,7 +88,7 @@ public class DockerEndpoint extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected AuthenticationFlowModel getAuthenticationFlow() {
|
protected AuthenticationFlowModel getAuthenticationFlow(AuthenticationSessionModel authSession) {
|
||||||
return realm.getDockerAuthenticationFlow();
|
return realm.getDockerAuthenticationFlow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ import org.keycloak.models.KeycloakSessionFactory;
|
||||||
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.models.utils.AuthenticationFlowResolver;
|
||||||
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.TokenManager;
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
|
@ -491,7 +492,7 @@ public class TokenEndpoint {
|
||||||
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
||||||
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
|
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
|
||||||
|
|
||||||
AuthenticationFlowModel flow = realm.getDirectGrantFlow();
|
AuthenticationFlowModel flow = AuthenticationFlowResolver.resolveDirectGrantFlow(authSession);
|
||||||
String flowId = flow.getId();
|
String flowId = flow.getId();
|
||||||
AuthenticationProcessor processor = new AuthenticationProcessor();
|
AuthenticationProcessor processor = new AuthenticationProcessor();
|
||||||
processor.setAuthenticationSession(authSession)
|
processor.setAuthenticationSession(authSession)
|
||||||
|
|
|
@ -147,7 +147,7 @@ public class SamlEcpProfileService extends SamlService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected AuthenticationFlowModel getAuthenticationFlow() {
|
protected AuthenticationFlowModel getAuthenticationFlow(AuthenticationSessionModel authSession) {
|
||||||
for (AuthenticationFlowModel flowModel : realm.getAuthenticationFlows()) {
|
for (AuthenticationFlowModel flowModel : realm.getAuthenticationFlows()) {
|
||||||
if (flowModel.getAlias().equals(DefaultAuthenticationFlows.SAML_ECP_FLOW)) {
|
if (flowModel.getAlias().equals(DefaultAuthenticationFlows.SAML_ECP_FLOW)) {
|
||||||
return flowModel;
|
return flowModel;
|
||||||
|
|
|
@ -55,6 +55,7 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.models.utils.AuthenticationFlowResolver;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.LoginProtocolFactory;
|
import org.keycloak.protocol.LoginProtocolFactory;
|
||||||
|
@ -1121,7 +1122,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
|
|
||||||
protected Response browserAuthentication(AuthenticationSessionModel authSession, String errorMessage) {
|
protected Response browserAuthentication(AuthenticationSessionModel authSession, String errorMessage) {
|
||||||
this.event.event(EventType.LOGIN);
|
this.event.event(EventType.LOGIN);
|
||||||
AuthenticationFlowModel flow = realmModel.getBrowserFlow();
|
AuthenticationFlowModel flow = AuthenticationFlowResolver.resolveBrowserFlow(authSession);
|
||||||
String flowId = flow.getId();
|
String flowId = flow.getId();
|
||||||
AuthenticationProcessor processor = new AuthenticationProcessor();
|
AuthenticationProcessor processor = new AuthenticationProcessor();
|
||||||
processor.setAuthenticationSession(authSession)
|
processor.setAuthenticationSession(authSession)
|
||||||
|
|
|
@ -53,6 +53,7 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.models.UserConsentModel;
|
import org.keycloak.models.UserConsentModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.AuthenticationFlowResolver;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.models.utils.SystemClientUtil;
|
import org.keycloak.models.utils.SystemClientUtil;
|
||||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||||
|
@ -252,7 +253,7 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Response processAuthentication(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) {
|
protected Response processAuthentication(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) {
|
||||||
return processFlow(action, execution, authSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), errorMessage, new AuthenticationProcessor());
|
return processFlow(action, execution, authSession, AUTHENTICATE_PATH, AuthenticationFlowResolver.resolveBrowserFlow(authSession), errorMessage, new AuthenticationProcessor());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {
|
protected Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {
|
||||||
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 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.connections.httpclient;
|
||||||
|
|
||||||
|
import org.apache.http.HttpHost;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.ExpectedException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||||
|
import static org.hamcrest.CoreMatchers.nullValue;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ProxyMappings}.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||||
|
*/
|
||||||
|
public class ProxyMappingsTest {
|
||||||
|
|
||||||
|
private static final List<String> DEFAULT_MAPPINGS = Arrays.asList( //
|
||||||
|
".*\\.(google|googleapis)\\.com;http://proxy1:8080", //
|
||||||
|
".*\\.facebook\\.com;http://proxy2:8080" //
|
||||||
|
);
|
||||||
|
|
||||||
|
private static final List<String> MAPPINGS_WITH_FALLBACK = new ArrayList<>();
|
||||||
|
|
||||||
|
private static final List<String> MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION = new ArrayList<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
MAPPINGS_WITH_FALLBACK.addAll(DEFAULT_MAPPINGS);
|
||||||
|
MAPPINGS_WITH_FALLBACK.add(".*;http://fallback:8080");
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION.addAll(DEFAULT_MAPPINGS);
|
||||||
|
MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION.add(".*\\.acme\\.corp\\.com;NO_PROXY");
|
||||||
|
MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION.add(".*;http://fallback:8080");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExpectedException expectedException = ExpectedException.none();
|
||||||
|
|
||||||
|
ProxyMappings proxyMappings;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
proxyMappings = ProxyMappings.valueOf(DEFAULT_MAPPINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void proxyMappingFromEmptyListShouldBeEmpty() {
|
||||||
|
assertThat(new ProxyMappings(new ArrayList<>()).isEmpty(), is(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnProxy1ForConfiguredProxyMapping() {
|
||||||
|
|
||||||
|
HttpHost proxy = proxyMappings.getProxyFor("account.google.com");
|
||||||
|
assertThat(proxy, is(notNullValue()));
|
||||||
|
assertThat(proxy.getHostName(), is("proxy1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnProxy1ForConfiguredProxyMappingAlternative() {
|
||||||
|
|
||||||
|
HttpHost proxy = proxyMappings.getProxyFor("www.googleapis.com");
|
||||||
|
assertThat(proxy, is(notNullValue()));
|
||||||
|
assertThat(proxy.getHostName(), is("proxy1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnProxy1ForConfiguredProxyMappingWithSubDomain() {
|
||||||
|
|
||||||
|
HttpHost proxy = proxyMappings.getProxyFor("awesome.account.google.com");
|
||||||
|
assertThat(proxy, is(notNullValue()));
|
||||||
|
assertThat(proxy.getHostName(), is("proxy1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnProxy2ForConfiguredProxyMapping() {
|
||||||
|
|
||||||
|
HttpHost proxy = proxyMappings.getProxyFor("login.facebook.com");
|
||||||
|
assertThat(proxy, is(notNullValue()));
|
||||||
|
assertThat(proxy.getHostName(), is("proxy2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnNoProxyForUnknownHost() {
|
||||||
|
|
||||||
|
HttpHost proxy = proxyMappings.getProxyFor("login.microsoft.com");
|
||||||
|
assertThat(proxy, is(nullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRejectNull() {
|
||||||
|
|
||||||
|
expectedException.expect(NullPointerException.class);
|
||||||
|
expectedException.expectMessage("hostname");
|
||||||
|
|
||||||
|
proxyMappings.getProxyFor(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnFallbackForNotExplicitlyMappedHostname() {
|
||||||
|
|
||||||
|
ProxyMappings proxyMappingsWithFallback = ProxyMappings.valueOf(MAPPINGS_WITH_FALLBACK);
|
||||||
|
|
||||||
|
HttpHost proxy = proxyMappingsWithFallback.getProxyFor("login.salesforce.com");
|
||||||
|
assertThat(proxy.getHostName(), is("fallback"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnCorrectProxyOrFallback() {
|
||||||
|
|
||||||
|
ProxyMappings proxyMappingsWithFallback = ProxyMappings.valueOf(MAPPINGS_WITH_FALLBACK);
|
||||||
|
|
||||||
|
HttpHost forGoogle = proxyMappingsWithFallback.getProxyFor("login.google.com");
|
||||||
|
assertThat(forGoogle.getHostName(), is("proxy1"));
|
||||||
|
|
||||||
|
HttpHost forFacebook = proxyMappingsWithFallback.getProxyFor("login.facebook.com");
|
||||||
|
assertThat(forFacebook.getHostName(), is("proxy2"));
|
||||||
|
|
||||||
|
HttpHost forMicrosoft = proxyMappingsWithFallback.getProxyFor("login.microsoft.com");
|
||||||
|
assertThat(forMicrosoft.getHostName(), is("fallback"));
|
||||||
|
|
||||||
|
HttpHost forSalesForce = proxyMappingsWithFallback.getProxyFor("login.salesforce.com");
|
||||||
|
assertThat(forSalesForce.getHostName(), is("fallback"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnFallbackForNotExplicitlyMappedHostnameAndHonorProxyExceptions() {
|
||||||
|
|
||||||
|
ProxyMappings proxyMappingsWithFallbackAndProxyException = ProxyMappings.valueOf(MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION);
|
||||||
|
|
||||||
|
HttpHost forGoogle = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.google.com");
|
||||||
|
assertThat(forGoogle.getHostName(), is("proxy1"));
|
||||||
|
|
||||||
|
HttpHost forFacebook = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.facebook.com");
|
||||||
|
assertThat(forFacebook.getHostName(), is("proxy2"));
|
||||||
|
|
||||||
|
HttpHost forAcmeCorp = proxyMappingsWithFallbackAndProxyException.getProxyFor("myapp.acme.corp.com");
|
||||||
|
assertThat(forAcmeCorp, is(nullValue()));
|
||||||
|
|
||||||
|
HttpHost forMicrosoft = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.microsoft.com");
|
||||||
|
assertThat(forMicrosoft.getHostName(), is("fallback"));
|
||||||
|
|
||||||
|
HttpHost forSalesForce = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.salesforce.com");
|
||||||
|
assertThat(forSalesForce.getHostName(), is("fallback"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* 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.forms;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowError;
|
||||||
|
import org.keycloak.authentication.Authenticator;
|
||||||
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class UsernameOnlyAuthenticator implements Authenticator, AuthenticatorFactory {
|
||||||
|
public static final String PROVIDER_ID = "testsuite-username";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void authenticate(AuthenticationFlowContext context) {
|
||||||
|
String username = context.getHttpRequest().getDecodedFormParameters().getFirst("username");
|
||||||
|
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);
|
||||||
|
if (user == null) {
|
||||||
|
context.failure(AuthenticationFlowError.UNKNOWN_USER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.setUser(user);
|
||||||
|
context.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requiresUser() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void action(AuthenticationFlowContext context) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayType() {
|
||||||
|
return "Testsuite Username Only";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getReferenceCategory() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConfigurable() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||||
|
AuthenticationExecutionModel.Requirement.REQUIRED
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||||
|
return REQUIREMENT_CHOICES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUserSetupAllowed() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Testsuite Username authenticator. Username parameter sets username";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authenticator create(KeycloakSession session) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,4 +19,5 @@ org.keycloak.testsuite.forms.PassThroughAuthenticator
|
||||||
org.keycloak.testsuite.forms.PassThroughRegistration
|
org.keycloak.testsuite.forms.PassThroughRegistration
|
||||||
org.keycloak.testsuite.forms.ClickThroughAuthenticator
|
org.keycloak.testsuite.forms.ClickThroughAuthenticator
|
||||||
org.keycloak.testsuite.authentication.ExpectedParamAuthenticatorFactory
|
org.keycloak.testsuite.authentication.ExpectedParamAuthenticatorFactory
|
||||||
org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory
|
org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory
|
||||||
|
org.keycloak.testsuite.forms.UsernameOnlyAuthenticator
|
|
@ -837,7 +837,7 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertTrue(driver.findElement(By.id("kc-totp-secret-key")).getText().matches("[\\w]{4}( [\\w]{4}){7}"));
|
assertTrue(driver.findElement(By.id("kc-totp-secret-key")).getText().matches("[\\w]{4}( [\\w]{4}){7}"));
|
||||||
|
|
||||||
assertEquals("Type: Time-based", driver.findElement(By.id("kc-totp-type")).getText());
|
assertEquals("Type: Time-based", driver.findElement(By.id("kc-totp-type")).getText());
|
||||||
assertEquals("Algorithm: HmacSHA1", driver.findElement(By.id("kc-totp-algorithm")).getText());
|
assertEquals("Algorithm: SHA1", driver.findElement(By.id("kc-totp-algorithm")).getText());
|
||||||
assertEquals("Digits: 6", driver.findElement(By.id("kc-totp-digits")).getText());
|
assertEquals("Digits: 6", driver.findElement(By.id("kc-totp-digits")).getText());
|
||||||
assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());
|
assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());
|
||||||
|
|
||||||
|
|
|
@ -177,7 +177,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertTrue(driver.findElement(By.id("kc-totp-secret-key")).getText().matches("[\\w]{4}( [\\w]{4}){7}"));
|
assertTrue(driver.findElement(By.id("kc-totp-secret-key")).getText().matches("[\\w]{4}( [\\w]{4}){7}"));
|
||||||
|
|
||||||
assertEquals("Type: Time-based", driver.findElement(By.id("kc-totp-type")).getText());
|
assertEquals("Type: Time-based", driver.findElement(By.id("kc-totp-type")).getText());
|
||||||
assertEquals("Algorithm: HmacSHA1", driver.findElement(By.id("kc-totp-algorithm")).getText());
|
assertEquals("Algorithm: SHA1", driver.findElement(By.id("kc-totp-algorithm")).getText());
|
||||||
assertEquals("Digits: 6", driver.findElement(By.id("kc-totp-digits")).getText());
|
assertEquals("Digits: 6", driver.findElement(By.id("kc-totp-digits")).getText());
|
||||||
assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());
|
assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());
|
||||||
|
|
||||||
|
@ -217,9 +217,9 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
|
||||||
totpPage.clickManual();
|
totpPage.clickManual();
|
||||||
|
|
||||||
assertEquals("Type: Counter-based", driver.findElement(By.id("kc-totp-type")).getText());
|
assertEquals("Type: Counter-based", driver.findElement(By.id("kc-totp-type")).getText());
|
||||||
assertEquals("Algorithm: HmacSHA256", driver.findElement(By.id("kc-totp-algorithm")).getText());
|
assertEquals("Algorithm: SHA256", driver.findElement(By.id("kc-totp-algorithm")).getText());
|
||||||
assertEquals("Digits: 8", driver.findElement(By.id("kc-totp-digits")).getText());
|
assertEquals("Digits: 8", driver.findElement(By.id("kc-totp-digits")).getText());
|
||||||
assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());
|
assertEquals("Counter: 0", driver.findElement(By.id("kc-totp-counter")).getText());
|
||||||
} finally {
|
} finally {
|
||||||
rep.setOtpPolicyDigits(6);
|
rep.setOtpPolicyDigits(6);
|
||||||
rep.setOtpPolicyType("totp");
|
rep.setOtpPolicyType("totp");
|
||||||
|
|
|
@ -180,6 +180,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
||||||
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
|
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
|
||||||
addProviderInfo(result, "testsuite-dummy-registration", "Testsuite Dummy Pass Thru",
|
addProviderInfo(result, "testsuite-dummy-registration", "Testsuite Dummy Pass Thru",
|
||||||
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
|
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
|
||||||
|
addProviderInfo(result, "testsuite-username", "Testsuite Username Only",
|
||||||
|
"Testsuite Username authenticator. Username parameter sets username");
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,353 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 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.forms;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.container.test.api.Deployment;
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.admin.client.resource.ClientsResource;
|
||||||
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
|
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
|
import org.keycloak.models.AuthenticationFlowBindings;
|
||||||
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
|
import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
|
||||||
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
|
import org.keycloak.testsuite.pages.ErrorPage;
|
||||||
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
|
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
|
||||||
|
import org.keycloak.util.BasicAuthHelper;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
|
||||||
|
import javax.ws.rs.client.Client;
|
||||||
|
import javax.ws.rs.client.Entity;
|
||||||
|
import javax.ws.rs.client.WebTarget;
|
||||||
|
import javax.ws.rs.core.Form;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that clients can override auth flows
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
|
||||||
|
*/
|
||||||
|
public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
|
public static final String TEST_APP_DIRECT_OVERRIDE = "test-app-direct-override";
|
||||||
|
public static final String TEST_APP_FLOW = "test-app-flow";
|
||||||
|
@Rule
|
||||||
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected AppPage appPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected ErrorPage errorPage;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deployment
|
||||||
|
public static WebArchive deploy() {
|
||||||
|
return RunOnServerDeployment.create(UserResource.class)
|
||||||
|
.addPackages(true, "org.keycloak.testsuite");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setupFlows() {
|
||||||
|
testingClient.server().run(session -> {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
|
||||||
|
ClientModel client = session.realms().getClientByClientId("test-app-flow", realm);
|
||||||
|
if (client != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client = session.realms().getClientByClientId("test-app", realm);
|
||||||
|
client.setDirectAccessGrantsEnabled(true);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Parent flow
|
||||||
|
AuthenticationFlowModel browser = new AuthenticationFlowModel();
|
||||||
|
browser.setAlias("parent-flow");
|
||||||
|
browser.setDescription("browser based authentication");
|
||||||
|
browser.setProviderId("basic-flow");
|
||||||
|
browser.setTopLevel(true);
|
||||||
|
browser.setBuiltIn(true);
|
||||||
|
browser = realm.addAuthenticationFlow(browser);
|
||||||
|
|
||||||
|
// Subflow2
|
||||||
|
AuthenticationFlowModel subflow2 = new AuthenticationFlowModel();
|
||||||
|
subflow2.setTopLevel(false);
|
||||||
|
subflow2.setBuiltIn(true);
|
||||||
|
subflow2.setAlias("subflow-2");
|
||||||
|
subflow2.setDescription("username+password AND pushButton");
|
||||||
|
subflow2.setProviderId("basic-flow");
|
||||||
|
subflow2 = realm.addAuthenticationFlow(subflow2);
|
||||||
|
|
||||||
|
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
||||||
|
execution.setParentFlow(browser.getId());
|
||||||
|
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
||||||
|
execution.setFlowId(subflow2.getId());
|
||||||
|
execution.setPriority(20);
|
||||||
|
execution.setAuthenticatorFlow(true);
|
||||||
|
realm.addAuthenticatorExecution(execution);
|
||||||
|
|
||||||
|
// Subflow2 - push the button
|
||||||
|
execution = new AuthenticationExecutionModel();
|
||||||
|
execution.setParentFlow(subflow2.getId());
|
||||||
|
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||||
|
execution.setAuthenticator(PushButtonAuthenticatorFactory.PROVIDER_ID);
|
||||||
|
execution.setPriority(10);
|
||||||
|
execution.setAuthenticatorFlow(false);
|
||||||
|
|
||||||
|
realm.addAuthenticatorExecution(execution);
|
||||||
|
|
||||||
|
// Subflow2 - username-password
|
||||||
|
execution = new AuthenticationExecutionModel();
|
||||||
|
execution.setParentFlow(subflow2.getId());
|
||||||
|
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||||
|
execution.setAuthenticator(UsernamePasswordFormFactory.PROVIDER_ID);
|
||||||
|
execution.setPriority(20);
|
||||||
|
execution.setAuthenticatorFlow(false);
|
||||||
|
|
||||||
|
realm.addAuthenticatorExecution(execution);
|
||||||
|
|
||||||
|
client = realm.addClient(TEST_APP_FLOW);
|
||||||
|
client.setSecret("password");
|
||||||
|
client.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth");
|
||||||
|
client.setManagementUrl("http://localhost:8180/auth/realms/master/app/admin");
|
||||||
|
client.setEnabled(true);
|
||||||
|
client.addRedirectUri("http://localhost:8180/auth/realms/master/app/auth/*");
|
||||||
|
client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, browser.getId());
|
||||||
|
client.setPublicClient(false);
|
||||||
|
|
||||||
|
// Parent flow
|
||||||
|
AuthenticationFlowModel directGrant = new AuthenticationFlowModel();
|
||||||
|
directGrant.setAlias("direct-override-flow");
|
||||||
|
directGrant.setDescription("direct grant based authentication");
|
||||||
|
directGrant.setProviderId("basic-flow");
|
||||||
|
directGrant.setTopLevel(true);
|
||||||
|
directGrant.setBuiltIn(true);
|
||||||
|
directGrant = realm.addAuthenticationFlow(directGrant);
|
||||||
|
|
||||||
|
execution = new AuthenticationExecutionModel();
|
||||||
|
execution.setParentFlow(directGrant.getId());
|
||||||
|
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||||
|
execution.setAuthenticator(UsernameOnlyAuthenticator.PROVIDER_ID);
|
||||||
|
execution.setPriority(10);
|
||||||
|
execution.setAuthenticatorFlow(false);
|
||||||
|
|
||||||
|
realm.addAuthenticatorExecution(execution);
|
||||||
|
|
||||||
|
client = realm.addClient(TEST_APP_DIRECT_OVERRIDE);
|
||||||
|
client.setSecret("password");
|
||||||
|
client.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth");
|
||||||
|
client.setManagementUrl("http://localhost:8180/auth/realms/master/app/admin");
|
||||||
|
client.setEnabled(true);
|
||||||
|
client.addRedirectUri("http://localhost:8180/auth/realms/master/app/auth/*");
|
||||||
|
client.setPublicClient(false);
|
||||||
|
client.setDirectAccessGrantsEnabled(true);
|
||||||
|
client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, browser.getId());
|
||||||
|
client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.DIRECT_GRANT_BINDING, directGrant.getId());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//@Test
|
||||||
|
public void testRunConsole() throws Exception {
|
||||||
|
Thread.sleep(10000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWithClientBrowserOverride() throws Exception {
|
||||||
|
oauth.clientId(TEST_APP_FLOW);
|
||||||
|
String loginFormUrl = oauth.getLoginFormUrl();
|
||||||
|
log.info("loginFormUrl: " + loginFormUrl);
|
||||||
|
|
||||||
|
//Thread.sleep(10000000);
|
||||||
|
|
||||||
|
driver.navigate().to(loginFormUrl);
|
||||||
|
|
||||||
|
Assert.assertEquals("PushTheButton", driver.getTitle());
|
||||||
|
|
||||||
|
// Push the button. I am redirected to username+password form
|
||||||
|
driver.findElement(By.name("submit1")).click();
|
||||||
|
|
||||||
|
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
|
||||||
|
// Fill username+password. I am successfully authenticated
|
||||||
|
oauth.fillLoginForm("test-user@localhost", "password");
|
||||||
|
appPage.assertCurrent();
|
||||||
|
|
||||||
|
events.expectLogin().client("test-app-flow").detail(Details.USERNAME, "test-user@localhost").assertEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoOverrideBrowser() throws Exception {
|
||||||
|
String clientId = "test-app";
|
||||||
|
testNoOverrideBrowser(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testNoOverrideBrowser(String clientId) {
|
||||||
|
oauth.clientId(clientId);
|
||||||
|
String loginFormUrl = oauth.getLoginFormUrl();
|
||||||
|
log.info("loginFormUrl: " + loginFormUrl);
|
||||||
|
|
||||||
|
//Thread.sleep(10000000);
|
||||||
|
|
||||||
|
driver.navigate().to(loginFormUrl);
|
||||||
|
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
|
||||||
|
// Fill username+password. I am successfully authenticated
|
||||||
|
oauth.fillLoginForm("test-user@localhost", "password");
|
||||||
|
appPage.assertCurrent();
|
||||||
|
|
||||||
|
events.expectLogin().client(clientId).detail(Details.USERNAME, "test-user@localhost").assertEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGrantAccessTokenNoOverride() throws Exception {
|
||||||
|
testDirectGrantNoOverride("test-app");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testDirectGrantNoOverride(String clientId) {
|
||||||
|
Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
|
||||||
|
String grantUri = oauth.getResourceOwnerPasswordCredentialGrantUrl();
|
||||||
|
WebTarget grantTarget = httpClient.target(grantUri);
|
||||||
|
|
||||||
|
{ // test no password
|
||||||
|
String header = BasicAuthHelper.createHeader(clientId, "password");
|
||||||
|
Form form = new Form();
|
||||||
|
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
|
||||||
|
form.param("username", "test-user@localhost");
|
||||||
|
Response response = grantTarget.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, header)
|
||||||
|
.post(Entity.form(form));
|
||||||
|
assertEquals(401, response.getStatus());
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // test invalid password
|
||||||
|
String header = BasicAuthHelper.createHeader(clientId, "password");
|
||||||
|
Form form = new Form();
|
||||||
|
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
|
||||||
|
form.param("username", "test-user@localhost");
|
||||||
|
form.param("password", "invalid");
|
||||||
|
Response response = grantTarget.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, header)
|
||||||
|
.post(Entity.form(form));
|
||||||
|
assertEquals(401, response.getStatus());
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // test valid password
|
||||||
|
String header = BasicAuthHelper.createHeader(clientId, "password");
|
||||||
|
Form form = new Form();
|
||||||
|
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
|
||||||
|
form.param("username", "test-user@localhost");
|
||||||
|
form.param("password", "password");
|
||||||
|
Response response = grantTarget.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, header)
|
||||||
|
.post(Entity.form(form));
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient.close();
|
||||||
|
events.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGrantAccessTokenWithClientOverride() throws Exception {
|
||||||
|
String clientId = TEST_APP_DIRECT_OVERRIDE;
|
||||||
|
Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
|
||||||
|
String grantUri = oauth.getResourceOwnerPasswordCredentialGrantUrl();
|
||||||
|
WebTarget grantTarget = httpClient.target(grantUri);
|
||||||
|
|
||||||
|
{ // test no password
|
||||||
|
String header = BasicAuthHelper.createHeader(clientId, "password");
|
||||||
|
Form form = new Form();
|
||||||
|
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
|
||||||
|
form.param("username", "test-user@localhost");
|
||||||
|
Response response = grantTarget.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, header)
|
||||||
|
.post(Entity.form(form));
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient.close();
|
||||||
|
events.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestInterface() throws Exception {
|
||||||
|
ClientsResource clients = adminClient.realm("test").clients();
|
||||||
|
List<ClientRepresentation> query = clients.findByClientId(TEST_APP_DIRECT_OVERRIDE);
|
||||||
|
ClientRepresentation clientRep = query.get(0);
|
||||||
|
String directGrantFlowId = clientRep.getAuthenticationFlowBindingOverrides().get(AuthenticationFlowBindings.DIRECT_GRANT_BINDING);
|
||||||
|
Assert.assertNotNull(directGrantFlowId);
|
||||||
|
clientRep.getAuthenticationFlowBindingOverrides().put(AuthenticationFlowBindings.DIRECT_GRANT_BINDING, "");
|
||||||
|
clients.get(clientRep.getId()).update(clientRep);
|
||||||
|
testDirectGrantNoOverride(TEST_APP_DIRECT_OVERRIDE);
|
||||||
|
clientRep.getAuthenticationFlowBindingOverrides().put(AuthenticationFlowBindings.DIRECT_GRANT_BINDING, directGrantFlowId);
|
||||||
|
clients.get(clientRep.getId()).update(clientRep);
|
||||||
|
testGrantAccessTokenWithClientOverride();
|
||||||
|
|
||||||
|
query = clients.findByClientId(TEST_APP_FLOW);
|
||||||
|
clientRep = query.get(0);
|
||||||
|
String browserFlowId = clientRep.getAuthenticationFlowBindingOverrides().get(AuthenticationFlowBindings.BROWSER_BINDING);
|
||||||
|
Assert.assertNotNull(browserFlowId);
|
||||||
|
clientRep.getAuthenticationFlowBindingOverrides().put(AuthenticationFlowBindings.BROWSER_BINDING, "");
|
||||||
|
clients.get(clientRep.getId()).update(clientRep);
|
||||||
|
testNoOverrideBrowser(TEST_APP_FLOW);
|
||||||
|
clientRep.getAuthenticationFlowBindingOverrides().put(AuthenticationFlowBindings.BROWSER_BINDING, browserFlowId);
|
||||||
|
clients.get(clientRep.getId()).update(clientRep);
|
||||||
|
testWithClientBrowserOverride();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -114,6 +114,7 @@ totpType=Type
|
||||||
totpAlgorithm=Algorithm
|
totpAlgorithm=Algorithm
|
||||||
totpDigits=Digits
|
totpDigits=Digits
|
||||||
totpInterval=Interval
|
totpInterval=Interval
|
||||||
|
totpCounter=Counter
|
||||||
|
|
||||||
missingUsernameMessage=Please specify username.
|
missingUsernameMessage=Please specify username.
|
||||||
missingFirstNameMessage=Please specify first name.
|
missingFirstNameMessage=Please specify first name.
|
||||||
|
|
|
@ -49,9 +49,13 @@
|
||||||
<p>${msg("totpManualStep3")}</p>
|
<p>${msg("totpManualStep3")}</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li id="kc-totp-type">${msg("totpType")}: ${msg("totp." + totp.policy.type)}</li>
|
<li id="kc-totp-type">${msg("totpType")}: ${msg("totp." + totp.policy.type)}</li>
|
||||||
<li id="kc-totp-algorithm">${msg("totpAlgorithm")}: ${totp.policy.algorithm}</li>
|
<li id="kc-totp-algorithm">${msg("totpAlgorithm")}: ${totp.policy.getAlgorithmKey()}</li>
|
||||||
<li id="kc-totp-digits">${msg("totpDigits")}: ${totp.policy.digits}</li>
|
<li id="kc-totp-digits">${msg("totpDigits")}: ${totp.policy.digits}</li>
|
||||||
<li id="kc-totp-period">${msg("totpInterval")}: ${totp.policy.period}</li>
|
<#if totp.policy.type = "totp">
|
||||||
|
<li id="kc-totp-period">${msg("totpInterval")}: ${totp.policy.period}</li>
|
||||||
|
<#elseif totp.policy.type = "hotp">
|
||||||
|
<li id="kc-totp-counter">${msg("totpCounter")}: ${totp.policy.initialCounter}</li>
|
||||||
|
</#if>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<#else>
|
<#else>
|
||||||
|
|
|
@ -28,9 +28,13 @@
|
||||||
<p>${msg("loginTotpManualStep3")}</p>
|
<p>${msg("loginTotpManualStep3")}</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li id="kc-totp-type">${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}</li>
|
<li id="kc-totp-type">${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}</li>
|
||||||
<li id="kc-totp-algorithm">${msg("loginTotpAlgorithm")}: ${totp.policy.algorithm}</li>
|
<li id="kc-totp-algorithm">${msg("loginTotpAlgorithm")}: ${totp.policy.getAlgorithmKey()}</li>
|
||||||
<li id="kc-totp-digits">${msg("loginTotpDigits")}: ${totp.policy.digits}</li>
|
<li id="kc-totp-digits">${msg("loginTotpDigits")}: ${totp.policy.digits}</li>
|
||||||
<li id="kc-totp-period">${msg("loginTotpInterval")}: ${totp.policy.period}</li>
|
<#if totp.policy.type = "totp">
|
||||||
|
<li id="kc-totp-period">${msg("loginTotpInterval")}: ${totp.policy.period}</li>
|
||||||
|
<#elseif totp.policy.type = "hotp">
|
||||||
|
<li id="kc-totp-counter">${msg("loginTotpCounter")}: ${totp.policy.initialCounter}</li>
|
||||||
|
</#if>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<#else>
|
<#else>
|
||||||
|
|
|
@ -80,6 +80,7 @@ loginTotpType=Type
|
||||||
loginTotpAlgorithm=Algorithm
|
loginTotpAlgorithm=Algorithm
|
||||||
loginTotpDigits=Digits
|
loginTotpDigits=Digits
|
||||||
loginTotpInterval=Interval
|
loginTotpInterval=Interval
|
||||||
|
loginTotpCounter=Counter
|
||||||
|
|
||||||
loginTotp.totp=Time-based
|
loginTotp.totp=Time-based
|
||||||
loginTotp.hotp=Counter-based
|
loginTotp.hotp=Counter-based
|
||||||
|
|
Loading…
Reference in a new issue