keycloak-uncached

binding custom flows

8/7/2015 8:00:07 PM

Changes

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