keycloak-uncached
Changes
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js 13(+11 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html 49(+49 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html 1(+1 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java 20(+20 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java 39(+39 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomRegistrationFlowTest.java 125(+125 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughAuthenticator.java 125(+125 -0)
Details
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml
index 4866834..c80f25c 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml
@@ -43,6 +43,15 @@
<column name="OTP_POLICY_TYPE" type="VARCHAR(36)" defaultValue="totp">
<constraints nullable="true"/>
</column>
+ <column name="BROWSER_FLOW" type="VARCHAR(36)">
+ <constraints nullable="true"/>
+ </column>
+ <column name="REGISTRATION_FLOW" type="VARCHAR(36)">
+ <constraints nullable="true"/>
+ </column>
+ <column name="DIRECT_GRANT_FLOW" type="VARCHAR(36)">
+ <constraints nullable="true"/>
+ </column>
</addColumn>
</changeSet>
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index d93a0d6..43aeb61 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -87,6 +87,9 @@ public class RealmRepresentation {
protected List<AuthenticationFlowRepresentation> authenticationFlows;
protected List<AuthenticatorConfigRepresentation> authenticatorConfig;
protected List<RequiredActionProviderRepresentation> requiredActions;
+ protected String browserFlow;
+ protected String registrationFlow;
+ protected String directGrantFlow;
@Deprecated
protected Boolean social;
@@ -708,4 +711,28 @@ public class RealmRepresentation {
public void setOtpPolicyPeriod(Integer otpPolicyPeriod) {
this.otpPolicyPeriod = otpPolicyPeriod;
}
+
+ public String getBrowserFlow() {
+ return browserFlow;
+ }
+
+ public void setBrowserFlow(String browserFlow) {
+ this.browserFlow = browserFlow;
+ }
+
+ public String getRegistrationFlow() {
+ return registrationFlow;
+ }
+
+ public void setRegistrationFlow(String registrationFlow) {
+ this.registrationFlow = registrationFlow;
+ }
+
+ public String getDirectGrantFlow() {
+ return directGrantFlow;
+ }
+
+ public void setDirectGrantFlow(String directGrantFlow) {
+ this.directGrantFlow = directGrantFlow;
+ }
}
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index 878f13d..19a3562 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -1087,6 +1087,21 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'AuthenticationFlowsCtrl'
})
+ .when('/realms/:realm/authentication/flow-bindings', {
+ templateUrl : resourceUrl + '/partials/authentication-flow-bindings.html',
+ resolve : {
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ },
+ flows : function(AuthenticationFlowsLoader) {
+ return AuthenticationFlowsLoader();
+ },
+ serverInfo : function(ServerInfo) {
+ return ServerInfo.delay;
+ }
+ },
+ controller : 'RealmFlowBindingCtrl'
+ })
.when('/realms/:realm/authentication/flows/:flow', {
templateUrl : resourceUrl + '/partials/authentication-flows.html',
resolve : {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 0ed6fa4..9aeb0e8 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -376,6 +376,7 @@ module.controller('RealmOtpPolicyCtrl', function($scope, Current, Realm, realm,
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/otp-policy");
});
+
module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");
@@ -1620,6 +1621,13 @@ module.controller('IdentityProviderMapperCreateCtrl', function($scope, realm, id
});
+module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
+ $scope.flows = flows;
+
+ genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/flow-bindings");
+});
+
+
module.controller('CreateFlowCtrl', function($scope, realm,
AuthenticationFlows,
Notifications, $location) {
@@ -1770,8 +1778,9 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo
};
$scope.removeFlow = function() {
- AuthenticationFlows.remove({realm: realm, flow: flow.id}, function() {
- $route.reload();
+ console.log('Remove flow:' + $scope.flow.alias);
+ AuthenticationFlows.remove({realm: realm.realm, flow: $scope.flow.id}, function() {
+ $location.url("/realms/" + realm.realm + '/authentication/flows');
Notifications.success("Flow removed");
})
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html
new file mode 100755
index 0000000..050c5f1
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html
@@ -0,0 +1,49 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <h1>Authentication</h1>
+
+ <kc-tabs-authentication></kc-tabs-authentication>
+
+ <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
+ <div class="form-group">
+ <label for="browser" class="col-md-2 control-label">Browser Flow</label>
+ <div class="col-md-2">
+ <div>
+ <select id="browser" ng-model="realm.browserFlow" class="form-control" ng-options="flow.alias as flow.alias for flow in flows">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>Select the flow you want to use for browser authentication.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label for="registration" class="col-md-2 control-label">Registration Flow</label>
+ <div class="col-md-2">
+ <div>
+ <select id="registration" ng-model="realm.registrationFlow" class="form-control" ng-options="flow.alias as flow.alias for flow in flows">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>Select the flow you want to use for registration.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label for="grant" class="col-md-2 control-label">Direct Grant Flow</label>
+ <div class="col-md-2">
+ <div>
+ <select id="grant" ng-model="realm.directGrantFlow" class="form-control" ng-options="flow.alias as flow.alias for flow in flows">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>Select the flow you want to use for direct grant authentication.</kc-tooltip>
+ </div>
+
+ <div class="form-group" data-ng-show="access.manageRealm">
+ <div class="col-md-12">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ </div>
+ </div>
+ </form>
+
+</div>
+
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html
index 214a9ad..0fcd65a 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html
@@ -1,5 +1,6 @@
<ul class="nav nav-tabs">
<li ng-class="{active: path[3] == 'flows'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/flows">Flows</a></li>
+ <li ng-class="{active: path[3] == 'flow-bindings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/flow-bindings">Bindings</a></li>
<li ng-class="{active: path[3] == 'required-actions'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/required-actions">Required Actions</a></li>
<li ng-class="{active: path[3] == 'password-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/password-policy">Password Policy</a></li>
<li ng-class="{active: path[3] == 'otp-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/otp-policy">OTP Policy</a></li>
diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java
index 2b35150..acec3b0 100755
--- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java
+++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java
@@ -24,7 +24,10 @@ public class MigrateTo1_5_0 {
public void migrate(KeycloakSession session) {
List<RealmModel> realms = session.realms().getRealms();
for (RealmModel realm : realms) {
- realm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
+ realm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
+ realm.setBrowserFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW));
+ realm.setRegistrationFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.REGISTRATION_FLOW));
+ realm.setDirectGrantFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW));
}
}
diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
index 64228e4..1d4859d 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
@@ -86,6 +86,9 @@ public class RealmEntity extends AbstractIdentifiableEntity {
private List<AuthenticationFlowEntity> authenticationFlows = new ArrayList<>();
private List<AuthenticatorConfigEntity> authenticatorConfigs = new ArrayList<>();
private List<RequiredActionProviderEntity> requiredActionProviders = new ArrayList<>();
+ private String browserFlow;
+ private String registrationFlow;
+ private String directGrantFlow;
public String getName() {
@@ -566,6 +569,30 @@ public class RealmEntity extends AbstractIdentifiableEntity {
public void setOtpPolicyPeriod(int otpPolicyPeriod) {
this.otpPolicyPeriod = otpPolicyPeriod;
}
+
+ public String getBrowserFlow() {
+ return browserFlow;
+ }
+
+ public void setBrowserFlow(String browserFlow) {
+ this.browserFlow = browserFlow;
+ }
+
+ public String getRegistrationFlow() {
+ return registrationFlow;
+ }
+
+ public void setRegistrationFlow(String registrationFlow) {
+ this.registrationFlow = registrationFlow;
+ }
+
+ public String getDirectGrantFlow() {
+ return directGrantFlow;
+ }
+
+ public void setDirectGrantFlow(String directGrantFlow) {
+ this.directGrantFlow = directGrantFlow;
+ }
}
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index a9b81a6..9f39377 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -182,6 +182,15 @@ public interface RealmModel extends RoleContainerModel {
void setSmtpConfig(Map<String, String> smtpConfig);
+ AuthenticationFlowModel getBrowserFlow();
+ void setBrowserFlow(AuthenticationFlowModel flow);
+
+ AuthenticationFlowModel getRegistrationFlow();
+ void setRegistrationFlow(AuthenticationFlowModel flow);
+
+ AuthenticationFlowModel getDirectGrantFlow();
+ void setDirectGrantFlow(AuthenticationFlowModel flow);
+
List<AuthenticationFlowModel> getAuthenticationFlows();
AuthenticationFlowModel getFlowByAlias(String alias);
AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model);
diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
index 7995887..2c0b8e6 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
@@ -39,6 +39,7 @@ public class DefaultAuthenticationFlows {
registrationFlow.setTopLevel(true);
registrationFlow.setBuiltIn(true);
registrationFlow = realm.addAuthenticationFlow(registrationFlow);
+ realm.setRegistrationFlow(registrationFlow);
AuthenticationFlowModel registrationFormFlow = new AuthenticationFlowModel();
registrationFormFlow.setAlias(REGISTRATION_FORM_FLOW);
@@ -125,6 +126,7 @@ public class DefaultAuthenticationFlows {
grant.setTopLevel(true);
grant.setBuiltIn(true);
grant = realm.addAuthenticationFlow(grant);
+ realm.setDirectGrantFlow(grant);
// username
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
@@ -171,6 +173,8 @@ public class DefaultAuthenticationFlows {
browser.setTopLevel(true);
browser.setBuiltIn(true);
browser = realm.addAuthenticationFlow(browser);
+ realm.setBrowserFlow(browser);
+
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setParentFlow(browser.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 39a936d..f0583ba 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -160,6 +160,9 @@ public class ModelToRepresentation {
rep.setOtpPolicyInitialCounter(otpPolicy.getInitialCounter());
rep.setOtpPolicyType(otpPolicy.getType());
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
+ if (realm.getBrowserFlow() != null) rep.setBrowserFlow(realm.getBrowserFlow().getAlias());
+ if (realm.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias());
+ if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());
List<String> defaultRoles = realm.getDefaultRoles();
if (!defaultRoles.isEmpty()) {
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index f90e7b9..940307c 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -343,7 +343,22 @@ public class RepresentationToModel {
newRealm.addAuthenticatorExecution(execution);
}
}
- }
+ }
+ if (rep.getBrowserFlow() == null) {
+ newRealm.setBrowserFlow(newRealm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW));
+ } else {
+ newRealm.setBrowserFlow(newRealm.getFlowByAlias(rep.getBrowserFlow()));
+ }
+ if (rep.getRegistrationFlow() == null) {
+ newRealm.setRegistrationFlow(newRealm.getFlowByAlias(DefaultAuthenticationFlows.REGISTRATION_FLOW));
+ } else {
+ newRealm.setRegistrationFlow(newRealm.getFlowByAlias(rep.getRegistrationFlow()));
+ }
+ if (rep.getDirectGrantFlow() == null) {
+ newRealm.setDirectGrantFlow(newRealm.getFlowByAlias(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW));
+ } else {
+ newRealm.setDirectGrantFlow(newRealm.getFlowByAlias(rep.getDirectGrantFlow()));
+ }
}
@@ -542,6 +557,15 @@ public class RepresentationToModel {
if(rep.getDefaultLocale() != null){
realm.setDefaultLocale(rep.getDefaultLocale());
}
+ if (rep.getBrowserFlow() != null) {
+ realm.setBrowserFlow(realm.getFlowByAlias(rep.getBrowserFlow()));
+ }
+ if (rep.getRegistrationFlow() != null) {
+ realm.setRegistrationFlow(realm.getFlowByAlias(rep.getRegistrationFlow()));
+ }
+ if (rep.getDirectGrantFlow() != null) {
+ realm.setDirectGrantFlow(realm.getFlowByAlias(rep.getDirectGrantFlow()));
+ }
}
// Basic realm stuff
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
index d4b3916..535328f 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
@@ -1231,6 +1231,46 @@ public class RealmAdapter implements RealmModel {
}
@Override
+ public AuthenticationFlowModel getBrowserFlow() {
+ String flowId = realm.getBrowserFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setBrowserFlow(AuthenticationFlowModel flow) {
+ realm.setBrowserFlow(flow.getId());
+
+ }
+
+ @Override
+ public AuthenticationFlowModel getRegistrationFlow() {
+ String flowId = realm.getRegistrationFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setRegistrationFlow(AuthenticationFlowModel flow) {
+ realm.setRegistrationFlow(flow.getId());
+
+ }
+
+ @Override
+ public AuthenticationFlowModel getDirectGrantFlow() {
+ String flowId = realm.getDirectGrantFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setDirectGrantFlow(AuthenticationFlowModel flow) {
+ realm.setDirectGrantFlow(flow.getId());
+
+ }
+
+
+ @Override
public List<AuthenticationFlowModel> getAuthenticationFlows() {
List<AuthenticationFlowEntity> flows = realm.getAuthenticationFlows();
if (flows.size() == 0) return Collections.EMPTY_LIST;
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
index e93d415..a406d9d 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
@@ -91,6 +91,10 @@ public class CachedRealm implements Serializable {
private MultivaluedHashMap<String, AuthenticationExecutionModel> authenticationExecutions = new MultivaluedHashMap<>();
private Map<String, AuthenticationExecutionModel> executionsById = new HashMap<>();
+ private AuthenticationFlowModel browserFlow;
+ private AuthenticationFlowModel registrationFlow;
+ private AuthenticationFlowModel directGrantFlow;
+
private boolean eventsEnabled;
private long eventsExpiration;
private Set<String> eventsListeners = new HashSet<String>();
@@ -214,6 +218,10 @@ public class CachedRealm implements Serializable {
requiredActionProvidersByAlias.put(action.getAlias(), action);
}
+ browserFlow = model.getBrowserFlow();
+ registrationFlow = model.getRegistrationFlow();
+ directGrantFlow = model.getDirectGrantFlow();
+
}
@@ -463,4 +471,16 @@ public class CachedRealm implements Serializable {
public OTPPolicy getOtpPolicy() {
return otpPolicy;
}
+
+ public AuthenticationFlowModel getBrowserFlow() {
+ return browserFlow;
+ }
+
+ public AuthenticationFlowModel getRegistrationFlow() {
+ return registrationFlow;
+ }
+
+ public AuthenticationFlowModel getDirectGrantFlow() {
+ return directGrantFlow;
+ }
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
index cb38a53..7526f50 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
@@ -1032,6 +1032,45 @@ public class RealmAdapter implements RealmModel {
}
@Override
+ public AuthenticationFlowModel getBrowserFlow() {
+ if (updated != null) return updated.getBrowserFlow();
+ return cached.getBrowserFlow();
+ }
+
+ @Override
+ public void setBrowserFlow(AuthenticationFlowModel flow) {
+ getDelegateForUpdate();
+ updated.setBrowserFlow(flow);
+
+ }
+
+ @Override
+ public AuthenticationFlowModel getRegistrationFlow() {
+ if (updated != null) return updated.getRegistrationFlow();
+ return cached.getRegistrationFlow();
+ }
+
+ @Override
+ public void setRegistrationFlow(AuthenticationFlowModel flow) {
+ getDelegateForUpdate();
+ updated.setRegistrationFlow(flow);
+
+ }
+
+ @Override
+ public AuthenticationFlowModel getDirectGrantFlow() {
+ if (updated != null) return updated.getDirectGrantFlow();
+ return cached.getDirectGrantFlow();
+ }
+
+ @Override
+ public void setDirectGrantFlow(AuthenticationFlowModel flow) {
+ getDelegateForUpdate();
+ updated.setDirectGrantFlow(flow);
+
+ }
+
+ @Override
public List<AuthenticationFlowModel> getAuthenticationFlows() {
if (updated != null) return updated.getAuthenticationFlows();
List<AuthenticationFlowModel> models = new ArrayList<>();
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index 67ba5a6..ddb6e34 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -179,6 +179,17 @@ public class RealmEntity {
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
Collection<AuthenticationFlowEntity> authenticationFlows = new ArrayList<>();
+ @Column(name="BROWSER_FLOW")
+ protected String browserFlow;
+
+ @Column(name="REGISTRATION_FLOW")
+ protected String registrationFlow;
+
+
+ @Column(name="DIRECT_GRANT_FLOW")
+ protected String directGrantFlow;
+
+
@Column(name="INTERNATIONALIZATION_ENABLED")
@@ -643,5 +654,29 @@ public class RealmEntity {
public void setOtpPolicyPeriod(int otpPolicyPeriod) {
this.otpPolicyPeriod = otpPolicyPeriod;
}
+
+ public String getBrowserFlow() {
+ return browserFlow;
+ }
+
+ public void setBrowserFlow(String browserFlow) {
+ this.browserFlow = browserFlow;
+ }
+
+ public String getRegistrationFlow() {
+ return registrationFlow;
+ }
+
+ public void setRegistrationFlow(String registrationFlow) {
+ this.registrationFlow = registrationFlow;
+ }
+
+ public String getDirectGrantFlow() {
+ return directGrantFlow;
+ }
+
+ public void setDirectGrantFlow(String directGrantFlow) {
+ this.directGrantFlow = directGrantFlow;
+ }
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 9706393..15ca36f 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1542,6 +1542,45 @@ public class RealmAdapter implements RealmModel {
}
@Override
+ public AuthenticationFlowModel getBrowserFlow() {
+ String flowId = realm.getBrowserFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setBrowserFlow(AuthenticationFlowModel flow) {
+ realm.setBrowserFlow(flow.getId());
+
+ }
+
+ @Override
+ public AuthenticationFlowModel getRegistrationFlow() {
+ String flowId = realm.getRegistrationFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setRegistrationFlow(AuthenticationFlowModel flow) {
+ realm.setRegistrationFlow(flow.getId());
+
+ }
+
+ @Override
+ public AuthenticationFlowModel getDirectGrantFlow() {
+ String flowId = realm.getDirectGrantFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setDirectGrantFlow(AuthenticationFlowModel flow) {
+ realm.setDirectGrantFlow(flow.getId());
+
+ }
+
+ @Override
public List<AuthenticationFlowModel> getAuthenticationFlows() {
TypedQuery<AuthenticationFlowEntity> query = em.createNamedQuery("getAuthenticationFlowsByRealm", AuthenticationFlowEntity.class);
query.setParameter("realm", realm);
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index d5d31eb..295ba6d 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -1310,6 +1310,49 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
}
@Override
+ public AuthenticationFlowModel getBrowserFlow() {
+ String flowId = realm.getBrowserFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setBrowserFlow(AuthenticationFlowModel flow) {
+ realm.setBrowserFlow(flow.getId());
+ updateRealm();
+
+ }
+
+ @Override
+ public AuthenticationFlowModel getRegistrationFlow() {
+ String flowId = realm.getRegistrationFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setRegistrationFlow(AuthenticationFlowModel flow) {
+ realm.setRegistrationFlow(flow.getId());
+ updateRealm();
+
+ }
+
+ @Override
+ public AuthenticationFlowModel getDirectGrantFlow() {
+ String flowId = realm.getDirectGrantFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setDirectGrantFlow(AuthenticationFlowModel flow) {
+ realm.setDirectGrantFlow(flow.getId());
+ updateRealm();
+
+ }
+
+
+ @Override
public List<AuthenticationFlowModel> getAuthenticationFlows() {
List<AuthenticationFlowEntity> flows = getMongoEntity().getAuthenticationFlows();
List<AuthenticationFlowModel> models = new LinkedList<>();
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
index 9b93942..8c0481f 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -501,7 +501,7 @@ public class SamlService {
return buildRedirectToIdentityProvider(identityProvider.getAlias(), new ClientSessionCode(realm, clientSession).getCode() );
}
}
- AuthenticationFlowModel flow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW);
+ AuthenticationFlowModel flow = realm.getBrowserFlow();
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
diff --git a/services/src/main/java/org/keycloak/authentication/Authenticator.java b/services/src/main/java/org/keycloak/authentication/Authenticator.java
index 674f892..932e4ad 100755
--- a/services/src/main/java/org/keycloak/authentication/Authenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/Authenticator.java
@@ -6,12 +6,36 @@ import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
/**
-* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
-* @version $Revision: 1 $
-*/
+ * This interface is for users that want to add custom authenticators to an authentication flow.
+ * You must implement this interface as well as an AuthenticatorFactory.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
public interface Authenticator extends Provider {
+
+ /**
+ * Initial call for the authenticator. If this is a form, a challenge with a Response rendering the form is usually sent
+ *
+ * @param context
+ */
void authenticate(AuthenticatorContext context);
+
+ /**
+ * Does this authenticator require that the user has already been identified? That AuthenticatorContext.getUser() is not null?
+ *
+ * @return
+ */
boolean requiresUser();
+
+ /**
+ * Is this authenticator configured for this user.
+ *
+ * @param session
+ * @param realm
+ * @param user
+ * @return
+ */
boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user);
/**
@@ -20,6 +44,11 @@ public interface Authenticator extends Provider {
*/
void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user);
+ /**
+ * Usually implements a form action.
+ *
+ * @param context
+ */
void action(AuthenticatorContext context);
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java
index 9ed9371..7e5e691 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java
@@ -6,6 +6,12 @@ import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.ProviderFactory;
/**
+ * Factory for creating Authenticator instances
+ *
+ * You must specify a file
+ * META-INF/services/org.keycloak.authentication.AuthenticatorFactory in the jar that this class is contained in
+ * This file must have the fully qualified class name of all your AuthentitoryFactory classes
+ *
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
diff --git a/services/src/main/java/org/keycloak/authentication/ConfigurableAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/ConfigurableAuthenticatorFactory.java
index ab807ca..48dbcf8 100755
--- a/services/src/main/java/org/keycloak/authentication/ConfigurableAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/ConfigurableAuthenticatorFactory.java
@@ -8,6 +8,11 @@ import org.keycloak.provider.ConfiguredProvider;
* @version $Revision: 1 $
*/
public interface ConfigurableAuthenticatorFactory extends ConfiguredProvider {
+ /**
+ * Friendly name for the authenticator
+ *
+ * @return
+ */
String getDisplayType();
/**
@@ -17,6 +22,11 @@ public interface ConfigurableAuthenticatorFactory extends ConfiguredProvider {
*/
String getReferenceCategory();
+ /**
+ * Is this authenticator configurable?
+ *
+ * @return
+ */
boolean isConfigurable();
/**
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 64a26d4..e368029 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -263,7 +263,7 @@ public class AuthorizationEndpoint {
clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE);
- AuthenticationFlowModel flow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW);
+ AuthenticationFlowModel flow = realm.getBrowserFlow();
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index b6dfcd7..9fb6137 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -331,7 +331,7 @@ public class TokenEndpoint {
clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
- AuthenticationFlowModel flow = realm.getFlowByAlias(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW);
+ AuthenticationFlowModel flow = realm.getDirectGrantFlow();
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index 4476557..3ac8af5 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -469,7 +469,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
protected Response browserAuthentication(ClientSessionModel clientSession, String errorMessage) {
this.event.event(EventType.LOGIN);
- AuthenticationFlowModel flow = realmModel.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW);
+ AuthenticationFlowModel flow = realmModel.getBrowserFlow();
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index c6b77b5..dc83039 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -152,7 +152,7 @@ public class LoginActionsService {
ClientSessionCode clientCode;
Response response;
- boolean verifyCode(String flow, String code, String requiredAction) {
+ boolean verifyCode(AuthenticationFlowModel flow, String code, String requiredAction) {
if (!verifyCode(flow, code)) {
return false;
} else if (!clientCode.isValidAction(requiredAction)) {
@@ -175,7 +175,7 @@ public class LoginActionsService {
}
}
- boolean verifyCode(String flow, String code, String requiredAction, String alternativeRequiredAction) {
+ boolean verifyCode(AuthenticationFlowModel flow, String code, String requiredAction, String alternativeRequiredAction) {
if (!verifyCode(flow, code)) {
return false;
} else if (!(clientCode.isValidAction(requiredAction) || clientCode.isValidAction(alternativeRequiredAction))) {
@@ -201,7 +201,7 @@ public class LoginActionsService {
}
}
- public boolean verifyCode(String flow, String code) {
+ public boolean verifyCode(AuthenticationFlowModel flow, String code) {
if (!checkSsl()) {
event.error(Errors.SSL_REQUIRED);
response = ErrorPage.error(session, Messages.HTTPS_REQUIRED);
@@ -268,7 +268,7 @@ public class LoginActionsService {
@QueryParam("execution") String execution) {
event.event(EventType.LOGIN);
Checks checks = new Checks();
- if (!checks.verifyCode(DefaultAuthenticationFlows.BROWSER_FLOW, code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionModel.Action.RECOVER_PASSWORD.name())) {
+ if (!checks.verifyCode(realm.getBrowserFlow(), code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionModel.Action.RECOVER_PASSWORD.name())) {
return checks.response;
}
event.detail(Details.CODE_ID, code);
@@ -284,12 +284,10 @@ public class LoginActionsService {
}
protected Response processAuthentication(String execution, ClientSessionModel clientSession, String errorMessage) {
- String flowAlias = DefaultAuthenticationFlows.BROWSER_FLOW;
- return processFlow(execution, clientSession, flowAlias, errorMessage);
+ return processFlow(execution, clientSession, realm.getBrowserFlow(), errorMessage);
}
- protected Response processFlow(String execution, ClientSessionModel clientSession, String flowAlias, String errorMessage) {
- AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias);
+ protected Response processFlow(String execution, ClientSessionModel clientSession, AuthenticationFlowModel flow, String errorMessage) {
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
.setFlowId(flow.getId())
@@ -325,7 +323,7 @@ public class LoginActionsService {
@QueryParam("execution") String execution) {
event.event(EventType.LOGIN);
Checks checks = new Checks();
- if (!checks.verifyCode(DefaultAuthenticationFlows.BROWSER_FLOW, code, ClientSessionModel.Action.AUTHENTICATE.name())) {
+ if (!checks.verifyCode(realm.getBrowserFlow(), code, ClientSessionModel.Action.AUTHENTICATE.name())) {
return checks.response;
}
final ClientSessionCode clientCode = checks.clientCode;
@@ -335,8 +333,7 @@ public class LoginActionsService {
}
protected Response processRegistration(String execution, ClientSessionModel clientSession, String errorMessage) {
- String flowAlias = DefaultAuthenticationFlows.REGISTRATION_FLOW;
- return processFlow(execution, clientSession, flowAlias, errorMessage);
+ return processFlow(execution, clientSession, realm.getRegistrationFlow(), errorMessage);
}
@@ -357,7 +354,7 @@ public class LoginActionsService {
}
Checks checks = new Checks();
- if (!checks.verifyCode(DefaultAuthenticationFlows.REGISTRATION_FLOW, code, ClientSessionModel.Action.AUTHENTICATE.name())) {
+ if (!checks.verifyCode(realm.getRegistrationFlow(), code, ClientSessionModel.Action.AUTHENTICATE.name())) {
return checks.response;
}
event.detail(Details.CODE_ID, code);
@@ -383,7 +380,7 @@ public class LoginActionsService {
@QueryParam("execution") String execution) {
event.event(EventType.REGISTER);
Checks checks = new Checks();
- if (!checks.verifyCode(DefaultAuthenticationFlows.REGISTRATION_FLOW, code, ClientSessionModel.Action.AUTHENTICATE.name())) {
+ if (!checks.verifyCode(realm.getRegistrationFlow(), code, ClientSessionModel.Action.AUTHENTICATE.name())) {
return checks.response;
}
if (!realm.isRegistrationAllowed()) {
@@ -484,7 +481,7 @@ public class LoginActionsService {
final MultivaluedMap<String, String> formData) {
event.event(EventType.UPDATE_PROFILE);
Checks checks = new Checks();
- if (!checks.verifyCode(DefaultAuthenticationFlows.BROWSER_FLOW, code, ClientSessionModel.Action.UPDATE_PROFILE.name())) {
+ if (!checks.verifyCode(realm.getBrowserFlow(), code, ClientSessionModel.Action.UPDATE_PROFILE.name())) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
@@ -546,7 +543,7 @@ public class LoginActionsService {
final MultivaluedMap<String, String> formData) {
event.event(EventType.UPDATE_TOTP);
Checks checks = new Checks();
- if (!checks.verifyCode(DefaultAuthenticationFlows.BROWSER_FLOW, code, ClientSessionModel.Action.CONFIGURE_TOTP.name())) {
+ if (!checks.verifyCode(realm.getBrowserFlow(), code, ClientSessionModel.Action.CONFIGURE_TOTP.name())) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
@@ -598,7 +595,7 @@ public class LoginActionsService {
final MultivaluedMap<String, String> formData) {
event.event(EventType.UPDATE_PASSWORD);
Checks checks = new Checks();
- if (!checks.verifyCode(DefaultAuthenticationFlows.BROWSER_FLOW, code, ClientSessionModel.Action.UPDATE_PASSWORD.name(), ClientSessionModel.Action.RECOVER_PASSWORD.name())) {
+ if (!checks.verifyCode(realm.getBrowserFlow(), code, ClientSessionModel.Action.UPDATE_PASSWORD.name(), ClientSessionModel.Action.RECOVER_PASSWORD.name())) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
@@ -661,7 +658,7 @@ public class LoginActionsService {
event.event(EventType.VERIFY_EMAIL);
if (key != null) {
Checks checks = new Checks();
- if (!checks.verifyCode(DefaultAuthenticationFlows.BROWSER_FLOW, key, ClientSessionModel.Action.VERIFY_EMAIL.name())) {
+ if (!checks.verifyCode(realm.getBrowserFlow(), key, ClientSessionModel.Action.VERIFY_EMAIL.name())) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
@@ -688,7 +685,7 @@ public class LoginActionsService {
return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
} else {
Checks checks = new Checks();
- if (!checks.verifyCode(DefaultAuthenticationFlows.BROWSER_FLOW, code, ClientSessionModel.Action.VERIFY_EMAIL.name())) {
+ if (!checks.verifyCode(realm.getBrowserFlow(), code, ClientSessionModel.Action.VERIFY_EMAIL.name())) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
@@ -711,7 +708,7 @@ public class LoginActionsService {
event.event(EventType.RESET_PASSWORD);
if (key != null) {
Checks checks = new Checks();
- if (!checks.verifyCode(DefaultAuthenticationFlows.BROWSER_FLOW, key, ClientSessionModel.Action.RECOVER_PASSWORD.name())) {
+ if (!checks.verifyCode(realm.getBrowserFlow(), key, ClientSessionModel.Action.RECOVER_PASSWORD.name())) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
@@ -732,7 +729,7 @@ public class LoginActionsService {
final MultivaluedMap<String, String> formData) {
event.event(EventType.SEND_RESET_PASSWORD);
Checks checks = new Checks();
- if (!checks.verifyCode(DefaultAuthenticationFlows.BROWSER_FLOW, code)) {
+ if (!checks.verifyCode(realm.getBrowserFlow(), code)) {
return checks.response;
}
final ClientSessionCode accessCode = checks.clientCode;
@@ -850,7 +847,7 @@ public class LoginActionsService {
throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE));
}
Checks checks = new Checks();
- if (!checks.verifyCode(DefaultAuthenticationFlows.BROWSER_FLOW, code, action)) {
+ if (!checks.verifyCode(realm.getBrowserFlow(), code, action)) {
return checks.response;
}
final ClientSessionCode clientCode = checks.clientCode;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
new file mode 100755
index 0000000..731a3f1
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
@@ -0,0 +1,208 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.forms;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.events.Details;
+import org.keycloak.events.Event;
+import org.keycloak.events.EventType;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.BrowserSecurityHeaders;
+import org.keycloak.models.PasswordPolicy;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.AppPage.RequestType;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.pages.RegisterPage;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.keycloak.util.Time;
+import org.openqa.selenium.WebDriver;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.core.Response;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class CustomFlowTest {
+
+ @ClassRule
+ public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ UserModel user = manager.getSession().users().addUser(appRealm, "login-test");
+ user.setEmail("login@test.com");
+ user.setEnabled(true);
+
+ userId = user.getId();
+
+ AuthenticationFlowModel flow = new AuthenticationFlowModel();
+ flow.setAlias("dummy");
+ flow.setDescription("dummy pass through flow");
+ flow.setProviderId("basic-flow");
+ flow.setTopLevel(true);
+ flow.setBuiltIn(false);
+ flow = appRealm.addAuthenticationFlow(flow);
+ appRealm.setBrowserFlow(flow);
+ appRealm.setDirectGrantFlow(flow);
+
+ AuthenticationExecutionModel execution;
+
+ execution = new AuthenticationExecutionModel();
+ execution.setParentFlow(flow.getId());
+ execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+ execution.setAuthenticator(PassThroughAuthenticator.PROVIDER_ID);
+ execution.setPriority(10);
+ execution.setAuthenticatorFlow(false);
+ appRealm.addAuthenticatorExecution(execution);
+
+
+
+ flow = new AuthenticationFlowModel();
+ flow.setAlias("dummy registration");
+ flow.setDescription("dummy pass through registration");
+ flow.setProviderId("basic-flow");
+ flow.setTopLevel(true);
+ flow.setBuiltIn(false);
+ flow = appRealm.addAuthenticationFlow(flow);
+ appRealm.setRegistrationFlow(flow);
+
+ execution = new AuthenticationExecutionModel();
+ execution.setParentFlow(flow.getId());
+ execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+ execution.setAuthenticator(PassThroughRegistration.PROVIDER_ID);
+ execution.setPriority(10);
+ execution.setAuthenticatorFlow(false);
+ appRealm.addAuthenticatorExecution(execution);
+
+
+ }
+ });
+
+ @Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+ @Rule
+ public WebRule webRule = new WebRule(this);
+
+ @WebResource
+ protected OAuthClient oauth;
+
+ @WebResource
+ protected WebDriver driver;
+
+ @WebResource
+ protected AppPage appPage;
+
+ @WebResource
+ protected LoginPage loginPage;
+
+ @WebResource
+ protected ErrorPage errorPage;
+
+ @WebResource
+ protected LoginPasswordUpdatePage updatePasswordPage;
+
+ @WebResource
+ protected RegisterPage registerPage;
+
+ private static String userId;
+
+ @Test
+ public void loginSuccess() {
+
+ PassThroughAuthenticator.username = "login-test";
+
+ oauth.openLoginForm();
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+
+ events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+ }
+
+ @Test
+ public void grantTest() throws Exception {
+ PassThroughAuthenticator.username = "login-test";
+ grantAccessToken("login-test");
+ }
+
+
+ private void grantAccessToken(String login) throws Exception {
+
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", login, "password");
+
+ assertEquals(200, response.getStatusCode());
+
+ AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+ RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+
+ events.expectLogin()
+ .client("test-app")
+ .user(userId)
+ .session(accessToken.getSessionState())
+ .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.TOKEN_ID, accessToken.getId())
+ .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+ .detail(Details.USERNAME, login)
+ .removeDetail(Details.CODE_ID)
+ .removeDetail(Details.REDIRECT_URI)
+ .removeDetail(Details.CONSENT)
+ .assertEvent();
+
+ assertEquals(accessToken.getSessionState(), refreshToken.getSessionState());
+
+ OAuthClient.AccessTokenResponse refreshedResponse = oauth.doRefreshTokenRequest(response.getRefreshToken(), "password");
+
+ AccessToken refreshedAccessToken = oauth.verifyToken(refreshedResponse.getAccessToken());
+ RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshedResponse.getRefreshToken());
+
+ assertEquals(accessToken.getSessionState(), refreshedAccessToken.getSessionState());
+ assertEquals(accessToken.getSessionState(), refreshedRefreshToken.getSessionState());
+
+ events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).user(userId).client("test-app").assertEvent();
+ }
+
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomRegistrationFlowTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomRegistrationFlowTest.java
new file mode 100755
index 0000000..8ab67d9
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomRegistrationFlowTest.java
@@ -0,0 +1,125 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.forms;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.events.Details;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.AppPage.RequestType;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.pages.RegisterPage;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.WebDriver;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class CustomRegistrationFlowTest {
+
+ @ClassRule
+ public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ AuthenticationFlowModel flow = new AuthenticationFlowModel();
+ flow.setAlias("dummy registration");
+ flow.setDescription("dummy pass through registration");
+ flow.setProviderId("basic-flow");
+ flow.setTopLevel(true);
+ flow.setBuiltIn(false);
+ flow = appRealm.addAuthenticationFlow(flow);
+ appRealm.setRegistrationFlow(flow);
+
+ AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
+ execution.setParentFlow(flow.getId());
+ execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+ execution.setAuthenticator(PassThroughRegistration.PROVIDER_ID);
+ execution.setPriority(10);
+ execution.setAuthenticatorFlow(false);
+ appRealm.addAuthenticatorExecution(execution);
+
+
+ }
+ });
+
+ @Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+ @Rule
+ public WebRule webRule = new WebRule(this);
+
+ @WebResource
+ protected OAuthClient oauth;
+
+ @WebResource
+ protected WebDriver driver;
+
+ @WebResource
+ protected AppPage appPage;
+
+ @WebResource
+ protected LoginPage loginPage;
+
+ @WebResource
+ protected ErrorPage errorPage;
+
+ @WebResource
+ protected LoginPasswordUpdatePage updatePasswordPage;
+
+ @WebResource
+ protected RegisterPage registerPage;
+
+ private static String userId;
+
+ @Test
+ public void registerUserSuccess() {
+ loginPage.open();
+ loginPage.clickRegister();
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ String userId = events.expectRegister(PassThroughRegistration.username, PassThroughRegistration.email).assertEvent().getUserId();
+ events.expectLogin().detail("username", PassThroughRegistration.username).user(userId).assertEvent();
+ }
+
+
+
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughAuthenticator.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughAuthenticator.java
new file mode 100755
index 0000000..264eaef
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughAuthenticator.java
@@ -0,0 +1,125 @@
+package org.keycloak.testsuite.forms;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorContext;
+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 PassThroughAuthenticator implements Authenticator, AuthenticatorFactory {
+ public static final String PROVIDER_ID = "dummy-passthrough";
+ public static String username = "test-user@localhost";
+
+ @Override
+ public void authenticate(AuthenticatorContext context) {
+ UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);
+ if (user == null) {
+ context.failure(AuthenticationProcessor.Error.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(AuthenticatorContext context) {
+
+ }
+
+ @Override
+ public Authenticator create() {
+ return this;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Dummy Pass Thru";
+ }
+
+ @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 "Dummy authenticator. Just passes through and is hardcoded to a specific user";
+ }
+
+ @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;
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughRegistration.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughRegistration.java
new file mode 100755
index 0000000..6f8498f
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughRegistration.java
@@ -0,0 +1,144 @@
+package org.keycloak.testsuite.forms;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorContext;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
+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.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.services.resources.AttributeFormDataProcessor;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class PassThroughRegistration implements Authenticator, AuthenticatorFactory {
+ public static final String PROVIDER_ID = "dummy-registration";
+ public static String username = "new-user@localhost";
+ public static String email = "new-user@localhost";
+
+ @Override
+ public void authenticate(AuthenticatorContext context) {
+ context.getEvent().detail(Details.USERNAME, username)
+ .detail(Details.REGISTER_METHOD, "form")
+ .detail(Details.EMAIL, email)
+ ;
+ UserModel user = context.getSession().users().addUser(context.getRealm(), username);
+ user.setEnabled(true);
+
+ user.setEmail(email);
+ context.getClientSession().setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
+ context.setUser(user);
+ context.getEvent().user(user);
+ context.getEvent().success();
+ context.newEvent().event(EventType.LOGIN);
+ context.getEvent().client(context.getClientSession().getClient().getClientId())
+ .detail(Details.REDIRECT_URI, context.getClientSession().getRedirectUri())
+ .detail(Details.AUTH_METHOD, context.getClientSession().getAuthMethod());
+ String authType = context.getClientSession().getNote(Details.AUTH_TYPE);
+ if (authType != null) {
+ context.getEvent().detail(Details.AUTH_TYPE, authType);
+ }
+ 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(AuthenticatorContext context) {
+
+ }
+
+ @Override
+ public Authenticator create() {
+ return this;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Dummy Pass Thru";
+ }
+
+ @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 "Dummy authenticator. Just passes through and is hardcoded to a specific user";
+ }
+
+ @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;
+ }
+}
diff --git a/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
new file mode 100755
index 0000000..61c9297
--- /dev/null
+++ b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -0,0 +1,2 @@
+org.keycloak.testsuite.forms.PassThroughAuthenticator
+org.keycloak.testsuite.forms.PassThroughRegistration
\ No newline at end of file