pkg/client/keycloak/adapter/gocloak_adapter_auth_flow.go (507 lines of code) (raw):

package adapter import ( "context" "fmt" "math" "net/http" "path" "sort" "strings" "github.com/Nerzal/gocloak/v12" "github.com/pkg/errors" ) var errAuthFlowNotFound = NotFoundError("auth flow not found") type KeycloakAuthFlow struct { ID string `json:"id,omitempty"` Alias string `json:"alias"` Description string `json:"description"` ProviderID string `json:"providerId"` TopLevel bool `json:"topLevel"` BuiltIn bool `json:"builtIn"` ParentName string `json:"-"` ChildType string `json:"-"` ChildRequirement string `json:"-"` AuthenticationExecutions []AuthenticationExecution `json:"-"` } type KeycloakChildAuthFlow struct { Alias string `json:"alias"` Description string `json:"description"` Provider string `json:"provider"` Type string `json:"type"` } type AuthenticationExecution struct { Authenticator string `json:"authenticator"` Requirement string `json:"requirement"` Priority int `json:"priority"` ParentFlow string `json:"parentFlow"` AuthenticatorConfig *AuthenticatorConfig `json:"-"` AutheticatorFlow bool `json:"autheticatorFlow"` ID string `json:"-"` Alias string `json:"-"` } type FlowExecution struct { AuthenticationFlow bool `json:"authenticationFlow"` Configurable bool `json:"configurable"` Description string `json:"description"` DisplayName string `json:"displayName"` FlowID string `json:"flowId"` ID string `json:"id"` Index int `json:"index"` Level int `json:"level"` Requirement string `json:"requirement"` RequirementChoices []string `json:"requirementChoices"` AuthenticationConfig string `json:"authenticationConfig"` } type AuthenticatorConfig struct { Alias string `json:"alias"` Config map[string]string `json:"config"` } type orderByPriority []AuthenticationExecution func (a orderByPriority) Len() int { return len(a) } func (a orderByPriority) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a orderByPriority) Less(i, j int) bool { return a[i].Priority < a[j].Priority } func (a GoCloakAdapter) DeleteAuthFlow(realmName string, flow *KeycloakAuthFlow) error { if flow.ParentName != "" { execID, err := a.getFlowExecutionID(realmName, flow) if err != nil { return errors.Wrap(err, "unable to get flow exec id") } if err := a.deleteFlowExecution(realmName, execID); err != nil { return errors.Wrap(err, "unable to delete execution") } return nil } flowID, err := a.getAuthFlowID(realmName, flow) if err != nil { return errors.Wrap(err, "unable to get auth flow") } if _, _, err := a.unsetBrowserFlow(realmName, flow.Alias); err != nil { return errors.Wrapf(err, "unable to unset browser flow for realm: %s, alias: %s", realmName, flow.Alias) } if err := a.deleteAuthFlow(realmName, flowID); err != nil { return errors.Wrap(err, "unable to delete auth flow") } return nil } func (a GoCloakAdapter) SyncAuthFlow(realmName string, flow *KeycloakAuthFlow) error { id, err := a.syncBaseAuthFlow(realmName, flow) if err != nil { return errors.Wrap(err, "unable to sync base auth flow") } sort.Sort(orderByPriority(flow.AuthenticationExecutions)) for _, e := range flow.AuthenticationExecutions { if e.AutheticatorFlow { continue } e.ParentFlow = id if err := a.addAuthFlowExecution(realmName, &e); err != nil { return errors.Wrap(err, "unable to add auth execution") } } if err := a.adjustChildFlowsPriority(realmName, flow); err != nil { return errors.Wrap(err, "unable to adjust child flow priority") } return nil } func (a GoCloakAdapter) adjustChildFlowsPriority(realmName string, flow *KeycloakAuthFlow) error { childFlows := a.makeChildFlows(flow) if len(childFlows) == 0 { return nil } flowExecs, err := a.getFlowExecutions(realmName, flow.Alias) if err != nil { return errors.Wrap(err, "unable to get flow executions") } for i := range flowExecs { if err := a.adjustFlowExecutionPriority(realmName, flow.Alias, &flowExecs[i], len(flowExecs), childFlows); err != nil { return err } } return nil } func (a GoCloakAdapter) SetRealmBrowserFlow(ctx context.Context, realmName string, flowAlias string) error { realm, err := a.client.GetRealm(ctx, a.token.AccessToken, realmName) if err != nil { return errors.Wrap(err, "unable to get realm") } realm.BrowserFlow = &flowAlias if err := a.client.UpdateRealm(ctx, a.token.AccessToken, *realm); err != nil { return errors.Wrap(err, "unable to update realm") } return nil } func (a GoCloakAdapter) syncBaseAuthFlow(realmName string, flow *KeycloakAuthFlow) (string, error) { authFlowID, err := a.getAuthFlowID(realmName, flow) if err != nil { if !IsErrNotFound(err) { return "", errors.Wrap(err, "unable to get auth flow") } id, err := a.createAuthFlow(realmName, flow) if err != nil { return "", errors.Wrap(err, "unable to create auth flow") } authFlowID = id } else { if err := a.clearFlowExecutions(realmName, flow.Alias); err != nil { return "", errors.Wrap(err, "unable to clear flow executions") } } if flow.ParentName != "" && flow.ChildRequirement != "" { exec, err := a.getFlowExecution(realmName, flow) if err != nil { return "", err } // We cant set child flow requirement during creation, so we need to update it. exec.Requirement = flow.ChildRequirement if err := a.updateFlowExecution(realmName, flow.ParentName, exec); err != nil { return "", fmt.Errorf("unable to update flow execution requirement: %w", err) } } if err := a.validateChildFlowsCreated(realmName, flow); err != nil { return "", errors.Wrap(err, "child flows validation failed") } return authFlowID, nil } func (a GoCloakAdapter) validateChildFlowsCreated(realmName string, flow *KeycloakAuthFlow) error { childFlows := 0 for _, authExec := range flow.AuthenticationExecutions { if authExec.AutheticatorFlow { childFlows++ } } if childFlows == 0 { return nil } childExecs, err := a.getFlowExecutions(realmName, flow.Alias) if err != nil { return errors.Wrap(err, "unable to get flow executions") } for i := range childExecs { if childExecs[i].AuthenticationFlow && childExecs[i].Level == 0 { childFlows-- } } if childFlows == 0 { return nil } return errors.New("not all child flows created") } func (a GoCloakAdapter) clearFlowExecutions(realmName, flowAlias string) error { execs, err := a.getFlowExecutions(realmName, flowAlias) if err != nil { return errors.Wrap(err, "unable to get flow executions") } for i := range execs { if execs[i].AuthenticationFlow || execs[i].Level > 0 { continue } if err := a.deleteFlowExecution(realmName, execs[i].ID); err != nil { return errors.Wrap(err, "unable to delete flow execution") } // after deleting flow execution, we need to delete its config as well // as it is not deleted automatically if execs[i].AuthenticationConfig != "" { if err := a.deleteAuthFlowConfig(realmName, execs[i].AuthenticationConfig); err != nil { return fmt.Errorf("unable to delete flow execution config: %w", err) } } } return nil } func (a GoCloakAdapter) deleteFlowExecution(realmName, id string) error { rsp, err := a.startRestyRequest(). SetPathParams(map[string]string{ keycloakApiParamRealm: realmName, keycloakApiParamId: id, }). Delete(a.buildPath(authFlowExecutionDelete)) if err = a.checkError(err, rsp); err != nil { return errors.Wrap(err, "unable to delete flow execution") } return nil } func (a GoCloakAdapter) getFlowExecutionID(realmName string, flow *KeycloakAuthFlow) (string, error) { execs, err := a.getFlowExecutions(realmName, flow.ParentName) if err != nil { return "", errors.Wrap(err, "unable to get auth flow executions") } for i := range execs { if execs[i].DisplayName == flow.Alias { return execs[i].ID, nil } } return "", errAuthFlowNotFound } func (a GoCloakAdapter) getAuthFlowID(realmName string, flow *KeycloakAuthFlow) (string, error) { if flow.ParentName != "" { execs, err := a.getFlowExecutions(realmName, flow.ParentName) if err != nil { return "", errors.Wrap(err, "unable to get auth flow executions") } for i := range execs { if execs[i].DisplayName == flow.Alias { return execs[i].FlowID, nil } } return "", errAuthFlowNotFound } flows, err := a.GetRealmAuthFlows(realmName) if err != nil { return "", errors.Wrap(err, "unable to get realm auth flows") } for i := range flows { if flows[i].Alias == flow.Alias { return flows[i].ID, nil } } return "", errAuthFlowNotFound } func (a GoCloakAdapter) getFlowExecution(realmName string, flow *KeycloakAuthFlow) (*FlowExecution, error) { execs, err := a.getFlowExecutions(realmName, flow.ParentName) if err != nil { return nil, fmt.Errorf("unable to get auth flow executions: %w", err) } for i := range execs { if execs[i].DisplayName == flow.Alias { return &execs[i], nil } } return nil, errAuthFlowNotFound } func (a GoCloakAdapter) GetRealmAuthFlows(realmName string) ([]KeycloakAuthFlow, error) { var flows []KeycloakAuthFlow resp, err := a.startRestyRequest(). SetPathParams(map[string]string{ keycloakApiParamRealm: realmName, }). SetResult(&flows). Get(a.buildPath(authFlows)) if err = a.checkError(err, resp); err != nil { return nil, errors.Wrap(err, "unable to list auth flow by realm") } return flows, nil } func (a GoCloakAdapter) createAuthFlow(realmName string, flow *KeycloakAuthFlow) (id string, err error) { if flow.ParentName != "" { return a.createChildAuthFlow(realmName, flow) } resp, err := a.startRestyRequest(). SetPathParams(map[string]string{ keycloakApiParamRealm: realmName, }). SetBody(flow). Post(a.buildPath(authFlows)) if err = a.checkError(err, resp); err != nil { return "", errors.Wrap(err, "unable to create auth flow in realm") } id, err = getIDFromResponseLocation(resp.RawResponse) if err != nil { return "", errors.Wrap(err, "unable to get flow id") } return } func (a GoCloakAdapter) createChildAuthFlow(realmName string, flow *KeycloakAuthFlow) (string, error) { rsp, err := a.startRestyRequest(). SetPathParams(map[string]string{ keycloakApiParamRealm: realmName, }). SetBody(KeycloakChildAuthFlow{ Description: flow.Description, Alias: flow.Alias, Provider: flow.ProviderID, Type: flow.ChildType, }). Post(a.buildPath(path.Join(authFlows, flow.ParentName, "executions/flow"))) if err = a.checkError(err, rsp); err != nil { return "", errors.Wrap(err, "unable to create child auth flow in realm") } id, err := getIDFromResponseLocation(rsp.RawResponse) if err != nil { return "", errors.Wrap(err, "unable to get flow id") } return id, nil } func (a GoCloakAdapter) updateFlowExecution(realmName, parentFlowAlias string, flowExec *FlowExecution) error { rsp, err := a.startRestyRequest(). SetPathParams(map[string]string{ keycloakApiParamRealm: realmName, keycloakApiParamAlias: parentFlowAlias, }). SetBody(flowExec). Put(a.buildPath(authFlowExecutionGetUpdate)) if err = a.checkError(err, rsp); err != nil { return errors.Wrap(err, "unable to update flow execution") } return nil } func (a GoCloakAdapter) adjustExecutionPriority(realmName, executionID string, delta int) error { route := raiseExecutionPriority if delta < 0 { route = lowerExecutionPriority } for i := 0; i < int(math.Abs(float64(delta))); i++ { rsp, err := a.startRestyRequest(). SetPathParams(map[string]string{ keycloakApiParamRealm: realmName, keycloakApiParamId: executionID, }). SetBody(map[string]string{ keycloakApiParamRealm: realmName, "execution": executionID, }). Post(a.buildPath(route)) if err = a.checkError(err, rsp); err != nil { return errors.Wrap(err, "unable to adjust execution priority") } } return nil } func (a GoCloakAdapter) getFlowExecutions(realmName, flowAlias string) ([]FlowExecution, error) { var execs []FlowExecution rsp, err := a.startRestyRequest(). SetPathParams(map[string]string{ keycloakApiParamRealm: realmName, keycloakApiParamAlias: flowAlias, }). SetResult(&execs). Get(a.buildPath(authFlowExecutionGetUpdate)) if err = a.checkError(err, rsp); err != nil { return nil, errors.Wrap(err, "unable get flow executions") } return execs, nil } func (a GoCloakAdapter) deleteAuthFlow(realmName, id string) error { resp, err := a.startRestyRequest(). SetPathParams(map[string]string{ keycloakApiParamRealm: realmName, keycloakApiParamId: id, }). Delete(a.buildPath(authFlow)) if err = a.checkError(err, resp); err != nil { return errors.Wrap(err, "unable to delete auth flow") } return nil } func (a GoCloakAdapter) addAuthFlowExecution(realmName string, flowExec *AuthenticationExecution) error { resp, err := a.startRestyRequest(). SetPathParams(map[string]string{ keycloakApiParamRealm: realmName, }). SetBody(flowExec). Post(a.buildPath(authFlowExecutionCreate)) if err = a.checkError(err, resp); err != nil { return errors.Wrap(err, "unable to add auth flow execution") } flowExec.ID, err = getIDFromResponseLocation(resp.RawResponse) if err != nil { return errors.Wrap(err, "unable to get auth exec id") } if flowExec.AuthenticatorConfig != nil { if err := a.createAuthFlowExecutionConfig(realmName, flowExec); err != nil { return errors.Wrap(err, "unable to create auth flow execution config") } } return nil } func (a GoCloakAdapter) createAuthFlowExecutionConfig(realmName string, flowExec *AuthenticationExecution) error { resp, err := a.startRestyRequest(). SetPathParams(map[string]string{ keycloakApiParamRealm: realmName, keycloakApiParamId: flowExec.ID, }). SetBody(flowExec.AuthenticatorConfig). Post(a.buildPath(authFlowExecutionConfig)) if err = a.checkError(err, resp); err != nil { return errors.Wrap(err, "unable to add auth flow execution") } return nil } func getIDFromResponseLocation(response *http.Response) (string, error) { location := response.Header.Get("Location") if location == "" { return "", errors.New("location header is not set or empty") } locationParts := strings.Split(response.Header.Get("Location"), "/") if len(locationParts) == 0 { return "", errors.New("location header does not have ID") } return locationParts[len(locationParts)-1], nil } func (a GoCloakAdapter) unsetBrowserFlow(realmName, flowAlias string) (realm *gocloak.RealmRepresentation, isBrowserFlowUnset bool, err error) { realm, err = a.client.GetRealm(context.Background(), a.token.AccessToken, realmName) if err != nil { return nil, false, errors.Wrapf(err, "unable to get realm: %s", realmName) } if realm.BrowserFlow == nil || *realm.BrowserFlow != flowAlias { return realm, false, nil } authFlows, err := a.GetRealmAuthFlows(realmName) if err != nil { return nil, false, errors.Wrapf(err, "unable to get auth flows for realm: %s", realmName) } var replaceFlow *KeycloakAuthFlow for i := range authFlows { if authFlows[i].Alias != flowAlias { replaceFlow = &authFlows[i] break } } if replaceFlow == nil { return nil, false, errors.Errorf("unable to delete auth flow: %s, no replacement for browser flow found", flowAlias) } realm.BrowserFlow = &replaceFlow.Alias if err := a.client.UpdateRealm(context.Background(), a.token.AccessToken, *realm); err != nil { return nil, false, errors.Wrapf(err, "unable to update realm: %s", realmName) } return realm, true, nil } func (a GoCloakAdapter) makeChildFlows(flow *KeycloakAuthFlow) map[string]AuthenticationExecution { childFlows := make(map[string]AuthenticationExecution) for i, authExec := range flow.AuthenticationExecutions { if authExec.AutheticatorFlow { childFlows[authExec.Alias] = flow.AuthenticationExecutions[i] } } return childFlows } func (a GoCloakAdapter) adjustFlowExecutionPriority( realmName, flowAlias string, flowExec *FlowExecution, flowExecsCount int, childFlows map[string]AuthenticationExecution, ) error { if !flowExec.AuthenticationFlow || flowExec.Level != 0 { return nil } childFlow, ok := childFlows[flowExec.DisplayName] if !ok { return errors.Errorf("unable to find child flow with name: %s", flowExec.DisplayName) } if childFlow.Requirement != flowExec.Requirement { flowExec.Requirement = childFlow.Requirement if err := a.updateFlowExecution(realmName, flowAlias, flowExec); err != nil { return errors.Wrap(err, "unable to update flow execution") } } if childFlow.Priority == flowExec.Index { return nil } if childFlow.Priority < 0 || childFlow.Priority > flowExecsCount { return errors.Errorf("wrong flow priority, flow name: %s, priority: %d", childFlow.Alias, childFlow.Priority) } if err := a.adjustExecutionPriority(realmName, flowExec.ID, flowExec.Index-childFlow.Priority); err != nil { return errors.Wrap(err, "unable to adjust flow priority") } return nil } func (a GoCloakAdapter) deleteAuthFlowConfig(realmName, configID string) error { rsp, err := a.startRestyRequest(). SetPathParams(map[string]string{ keycloakApiParamRealm: realmName, keycloakApiParamId: configID, }). Delete(a.buildPath(authFlowConfig)) if err = a.checkError(err, rsp); err != nil { return fmt.Errorf("unable to delete auth flow config: %w", err) } return nil }