Skip to content

Add option to configure authentication flows for new realm#126

Open
Prabhjot-Sethi wants to merge 2 commits into
mainfrom
auth-flows
Open

Add option to configure authentication flows for new realm#126
Prabhjot-Sethi wants to merge 2 commits into
mainfrom
auth-flows

Conversation

@Prabhjot-Sethi

@Prabhjot-Sethi Prabhjot-Sethi commented Sep 24, 2025

Copy link
Copy Markdown
Collaborator

This currently is required for only enabling user session limits configuration and in future can be extended for other configurations

Summary by CodeRabbit

  • New Features

    • Automatic creation and configuration of browser and IDP post-login authentication flows, including detection/use of forms subflows and ensuring session-limit steps are positioned last.
    • Enforced per-user session limits with configurable denial behavior and custom error messages.
    • Safer client provisioning that avoids duplicate protocol mappers and enforces sensible client defaults.
  • Reliability

    • Improved error handling, logging, and requeue behavior for more robust realm and client provisioning.

This currently is required for only enabling user session limits
configuration and in future can be extended for other configurations

Signed-off-by: Prabhjot Singh Sethi <prabhjot.sethi@gmail.com>
@coderabbitai

coderabbitai Bot commented Sep 24, 2025

Copy link
Copy Markdown

Walkthrough

Adds Keycloak authentication-flow management and client updates: new flow constants, SetupReconciler.updateRealmAuthFlows and detectFormsSubflow to ensure/copy/link flows, create/configure user-session-limits executions and reposition them, integrate these steps into Reconcile, and add multiple Keycloak client methods for flow manipulation.

Changes

Cohort / File(s) Summary
Controller: realm auth-flow & reconciler
pkg/controller/tenant/setup.go
Adds constants BrowserFlow, IDPPostLoginFlow; implements updateRealmAuthFlows(realm) and detectFormsSubflow; integrated into Reconcile to ensure/copy flows, detect forms subflow, locate/create/configure user-session-limits executions, set requirements and move executions to last position, and add requeue-on-error behavior and improved logging.
Controller: tenant client setup (enhanced)
pkg/controller/tenant/...setup.go
Adjusts tenant client creation/update: conditionally updates client fields (Enabled, PublicClient, StandardFlowEnabled), avoids duplicating protocol mappers by checking existing mappers before creating, and tightens error handling and requeue semantics.
Keycloak client: auth-flow utilities
pkg/keycloak/client.go
Adds public methods: CopyAuthenticationFlow, LocateAuthenticationExecution, ConfigureUserSessionLimit, LowerAuthenticationExecutionPriority, SetExecutionToPosition, GetMaxExecutionPriority. Implements flow copying, locating/creating executions by provider, numeric config handling, configuring session-limit executions, lowering/repositioning executions, and determining max execution priority; adds strconv and resty/v2 usage and wrapped admin-request error handling.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Reconciler as SetupReconciler
  participant Client as KeycloakClient
  participant API as Keycloak Admin API

  Reconciler->>Client: acquire token
  Client->>API: admin token exchange

  Reconciler->>Client: updateRealmAuthFlows(realm)
  Client->>API: GET /admin/realms/{realm}/authentication/flows
  API-->>Client: flows[]

  alt Ensure IDP post-login flow
    Reconciler->>Client: ensure or create "auth idp post login"
    Client->>API: POST/GET /authentication/flows
    API-->>Client: flow/execution
    Reconciler->>Client: ConfigureUserSessionLimit(...)
    Client->>API: PUT /authentication/executions/{id}/config
    API-->>Client: 200
  end

  alt Ensure custom browser flow
    Reconciler->>Client: ensure/copy "browser" -> "auth browser"
    Client->>API: POST /authentication/flows/{src}/copy
    API-->>Client: 201
    Reconciler->>Client: update realm to use custom browser flow
    Client->>API: PUT /realms/{realm}
    API-->>Client: 200
    Reconciler->>Client: detectFormsSubflow("auth browser")
    Client->>API: GET /authentication/flows/{flow}/executions
    API-->>Client: executions (including subflows)
    Reconciler->>Client: LocateAuthenticationExecution(formsSubflow, "user-session-limits")
    Client->>API: GET/POST /authentication/executions
    API-->>Client: execution
    Reconciler->>Client: ConfigureUserSessionLimit(...)
    loop Reposition until last
      Client->>API: POST /executions/{id}/lower-priority
      API-->>Client: 204
    end
  end

  Reconciler-->>Reconciler: success or requeue on error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I hop through realms with careful paws,
Copying browser flows and tuning laws,
I cap sessions, nudge steps to last,
Retry and requeue when things move fast.
Thump—setup done; the realms breathe easy. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly summarizes the primary change—adding support for configuring authentication flows on realm creation—using concise, specific language without extra noise. It directly reflects the main functionality introduced in the PR.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch auth-flows

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
pkg/controller/tenant/setup.go (2)

114-118: Avoid brittle “len(list) < 4” check; rely on locating the execution instead.
The magic number is environment‑dependent and may cause unnecessary requeues.

-	list, err := r.ctrl.client.GetAuthenticationExecutions(ctx, token, realm, BrowserFlow+" forms")
-	if err != nil || len(list) < 4 {
-		log.Printf("waiting for copy operation for auth browser flow to complete, error: %s", err)
-		return errors.Wrapf(errors.Unknown, "waiting for copy operation for auth browser flow to complete, error :%s", err)
-	}
+	list, err := r.ctrl.client.GetAuthenticationExecutions(ctx, token, realm, BrowserFlow+" forms")
+	if err != nil {
+		log.Printf("failed to fetch executions for %q, error: %s", BrowserFlow+" forms", err)
+		return err
+	}
+	_ = list // presence not enforced; locating execution below is authoritative

125-129: Config parameters should be configurable (not hard‑coded).
Session limit “5” and behavior could come from tenant config or defaults.

Would you like a follow‑up patch to read these from table.TenantEntry (with sane defaults)?

pkg/keycloak/client.go (1)

242-263: Improve error message; include src and dest, fix grammar.
Enhances observability when copy fails.

-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to create of copy authentication flow %s, status code: %d", src, resp.StatusCode())
-	}
+	if resp == nil || resp.IsError() {
+		return errors.Wrapf(
+			errors.Unknown,
+			"failed to copy authentication flow from %q to %q, status code: %d",
+			src, dest, resp.StatusCode(),
+		)
+	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 064ef50 and 8831016.

📒 Files selected for processing (2)
  • pkg/controller/tenant/setup.go (2 hunks)
  • pkg/keycloak/client.go (2 hunks)
🔇 Additional comments (7)
pkg/controller/tenant/setup.go (5)

31-35: Good constant naming and scoping.
Clear aliases for custom flows; suitable for reuse.


82-93: Solid flow: locate/create execution, configure, then set requirement.
This sequence is correct and idempotent for the IDP post-login flow.


94-100: Good: copy built-in browser flow to custom alias if missing.
Keeps built‑ins intact and enables customization.


102-112: Realm update is appropriate.
Setting BrowserFlow to the custom alias is the right hook.


177-182: Requeue-on-failure integration looks good.
Failure path requeues after 5s; consistent with realm creation path.

If we adopt the non‑panic change above, ensure reconcile propagates the error path properly.

pkg/keycloak/client.go (2)

265-308: Execution lookup/creation flow is sound.
Top‑level filtering and create‑if‑missing pattern is good.


350-363: Priority adjustment endpoint wrapper looks correct.
Endpoint path and error handling are appropriate.

Comment thread pkg/controller/tenant/setup.go
Comment on lines +53 to +60
for _, f := range flows {
if *f.Alias == IDPPostLoginFlow {
idpPostLoginFlow = true
}
if *f.Alias == BrowserFlow {
browserFlow = true
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Nil-pointer risk: f.Alias may be nil.
Guard before dereferencing to avoid panic.

-	for _, f := range flows {
-		if *f.Alias == IDPPostLoginFlow {
-			idpPostLoginFlow = true
-		}
-		if *f.Alias == BrowserFlow {
-			browserFlow = true
-		}
-	}
+	for _, f := range flows {
+		if f.Alias == nil {
+			continue
+		}
+		switch *f.Alias {
+		case IDPPostLoginFlow:
+			idpPostLoginFlow = true
+		case BrowserFlow:
+			browserFlow = true
+		}
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, f := range flows {
if *f.Alias == IDPPostLoginFlow {
idpPostLoginFlow = true
}
if *f.Alias == BrowserFlow {
browserFlow = true
}
}
for _, f := range flows {
if f.Alias == nil {
continue
}
switch *f.Alias {
case IDPPostLoginFlow:
idpPostLoginFlow = true
case BrowserFlow:
browserFlow = true
}
}
🤖 Prompt for AI Agents
In pkg/controller/tenant/setup.go around lines 53 to 60, the loop dereferences
f.Alias without checking for nil which can panic; update the loop to guard
against nil by checking if f.Alias != nil before comparing its value to
IDPPostLoginFlow or BrowserFlow, and only then set idpPostLoginFlow or
browserFlow accordingly (skip or safely handle entries with nil Alias).

Comment thread pkg/controller/tenant/setup.go Outdated
Comment thread pkg/keycloak/client.go
Comment on lines +310 to +348
func (c *Client) ConfigureUserSessionLimit(ctx context.Context, token, realm string, e *gocloak.ModifyAuthenticationExecutionRepresentation, sessions int, denyNewSession bool, alias, errorMsg string) error {
var err error
behavior := "Deny new session"
if !denyNewSession {
behavior = "Terminate oldest session"
}
config := struct {
//ID *string `json:"id,omitempty"`
Alias *string `json:"alias,omitempty"`
Config *map[string]string `json:"config,omitempty"`
}{
Alias: gocloak.StringP(alias),
Config: &map[string]string{
"userRealmLimit": strconv.Itoa(sessions),
"behavior": behavior,
"userClientLimit": "0",
"errorMessage": errorMsg,
},
}
var resp *resty.Response
if e.AuthenticationConfig == nil {
resp, err = c.GetRequestWithBearerAuth(ctx, token).
SetBody(config).
Post(c.getAdminRealmURL(c.url, realm, "authentication", "executions", *e.ID, "config"))
} else {
resp, err = c.GetRequestWithBearerAuth(ctx, token).
SetBody(config).
Put(c.getAdminRealmURL(c.url, realm, "authentication", "config", *e.AuthenticationConfig))
}

if err != nil {
return err
}

if resp == nil || resp.IsError() {
return errors.Wrapf(errors.Unknown, "failed to configure user session limit, status code: %d", resp.StatusCode())
}
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Defensive checks for execution IDs before POST/PUT.
Avoid nil deref if an unexpected payload is returned.

 func (c *Client) ConfigureUserSessionLimit(ctx context.Context, token, realm string, e *gocloak.ModifyAuthenticationExecutionRepresentation, sessions int, denyNewSession bool, alias, errorMsg string) error {
-	var err error
+	var err error
+	if e == nil || e.ID == nil {
+		return errors.Wrapf(errors.InvalidArgument, "execution is nil or missing ID")
+	}

Please confirm the expected strings for behavior in your Keycloak version are exactly:

  • "Deny new session"
  • "Terminate oldest session"

🌐 Web query:

Keycloak "Deny new session" "Terminate oldest session" "User session limit" "userRealmLimit"

💡 Result:

Summary — what these options do and how to configure them

  • Behavior options:

    • "Deny new session": when a user already has the maximum allowed sessions, a new login is rejected. [1]
    • "Terminate oldest session": when the limit is reached, Keycloak removes the oldest session and allows the new one. [1]
  • Config keys (used in the User Session Count Limiter authenticator):

    • userRealmLimit — max concurrent SSO sessions per user across the realm.
    • userClientLimit — max concurrent sessions per user per client.
    • behavior — either "Deny new session" or "Terminate oldest session".
    • Example JSON config: { "userClientLimit":"0", "behavior":"Deny new session", "userRealmLimit":"3" }. [2]
  • Where to enable:

    • Add the "User Session Count Limiter" authenticator to the relevant authentication flows (Browser, Direct Grant, Reset Credentials and Post-broker login flows), set it to REQUIRED, and configure the limits/behavior. [1][3]
  • Caveats/bugs:

    • There are reported issues where "Terminate oldest session" can remove the wrong session when both realm- and client-limits interact; test your flows and Keycloak version. [4]

Sources
[1] Keycloak Server Administration — User session limits. (keycloak.org)
[2] Example config and keys (userRealmLimit, userClientLimit, behavior). (doc.quobis.com)
[3] How to add/configure the authenticator in flows (documentation and examples). (manologg.github.io)
[4] GitHub issue reporting incorrect session removal with "Terminate oldest session". (github.com)

If you want, tell me your Keycloak version and I’ll give exact Admin Console steps or the JSON to apply via the Admin REST API.

Citations:


Fix nil derefs in ConfigureUserSessionLimit; confirm behavior strings

  • Add a defensive guard at the top of ConfigureUserSessionLimit: if e == nil || (e.AuthenticationConfig == nil && e.ID == nil) { return errors.Wrapf(errors.InvalidArgument, "execution is nil or missing ID") } — file: pkg/keycloak/client.go, function ConfigureUserSessionLimit.
  • Avoid dereferencing resp when nil: don’t call resp.StatusCode() if resp == nil; handle resp==nil and resp.IsError() separately (e.g., if resp == nil { return errors.Wrapf(errors.Unknown, "no response from admin API") } else if resp.IsError() { return errors.Wrapf(errors.Unknown, "failed to configure user session limit, status code: %d", resp.StatusCode()) }).
  • Behavior strings verified (Keycloak docs): "Deny new session", "Terminate oldest session".
🤖 Prompt for AI Agents
In pkg/keycloak/client.go around lines 310 to 348, add defensive checks and
safer resp handling: at the top of ConfigureUserSessionLimit return an
InvalidArgument error if e == nil or (e.AuthenticationConfig == nil && e.ID ==
nil); after making the HTTP call, handle resp == nil first (return an Unknown
error like "no response from admin API") and only then check resp.IsError() and
use resp.StatusCode() in that branch; keep the behavior strings as "Deny new
session" and "Terminate oldest session".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8831016 and e750aefb5067d1699aed44064c74ac1403f692fe.

📒 Files selected for processing (2)
  • pkg/controller/tenant/setup.go (5 hunks)
  • pkg/keycloak/client.go (2 hunks)
🔇 Additional comments (3)
pkg/controller/tenant/setup.go (2)

50-53: Return an error instead of panicking on token acquisition failure.
Line 50: panicking here will take down the controller; bubble the error up so the reconciler can requeue.

-	token, err := r.ctrl.client.GetAccessToken()
-	if err != nil {
-		log.Panicf("keycloak session not active: %s", err)
-	}
+	token, err := r.ctrl.client.GetAccessToken()
+	if err != nil {
+		return errors.Wrapf(errors.Unauthenticated, "keycloak session not active: %v", err)
+	}

63-71: Guard against nil aliases before dereferencing.
Line 65: f.Alias can be nil; dereferencing will panic when Keycloak returns sparse flow data.

-	for _, f := range flows {
-		if *f.Alias == IDPPostLoginFlow {
-			idpPostLoginFlow = true
-		}
-		if *f.Alias == BrowserFlow {
-			browserFlow = true
-		}
-	}
+	for _, f := range flows {
+		if f.Alias == nil {
+			continue
+		}
+		switch *f.Alias {
+		case IDPPostLoginFlow:
+			idpPostLoginFlow = true
+		case BrowserFlow:
+			browserFlow = true
+		}
+	}
pkg/keycloak/client.go (1)

310-345: Defend against nil execution data and responses in session-limit configuration.
Line 310: e (and e.ID) can be nil; line 344: calling StatusCode() when resp is nil will panic. Both conditions still need guarding.

-func (c *Client) ConfigureUserSessionLimit(ctx context.Context, token, realm string, e *gocloak.ModifyAuthenticationExecutionRepresentation, sessions int, denyNewSession bool, alias, errorMsg string) error {
-	var err error
+func (c *Client) ConfigureUserSessionLimit(ctx context.Context, token, realm string, e *gocloak.ModifyAuthenticationExecutionRepresentation, sessions int, denyNewSession bool, alias, errorMsg string) error {
+	if e == nil || e.ID == nil {
+		return errors.Wrapf(errors.InvalidArgument, "execution is nil or missing ID")
+	}
+	var err error
@@
-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to configure user session limit, status code: %d", resp.StatusCode())
+	if resp == nil {
+		return errors.Wrapf(errors.Unknown, "failed to configure user session limit: empty response from admin API")
+	}
+	if resp.IsError() {
+		return errors.Wrapf(errors.Unknown, "failed to configure user session limit, status code: %d", resp.StatusCode())

Comment thread pkg/controller/tenant/setup.go Outdated
Comment on lines +220 to +224
for _, flow := range flows {
if flow.Alias != nil && !*flow.BuiltIn {
flowName := *flow.Alias
if flowName != BrowserFlow && flowName != IDPPostLoginFlow {
executions, err := r.ctrl.client.GetAuthenticationExecutions(ctx, token, realm, flowName)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Check BuiltIn for nil before dereferencing.
Line 222: flow.BuiltIn is optional in the admin response; dereferencing without a nil guard will panic.

-	for _, flow := range flows {
-		if flow.Alias != nil && !*flow.BuiltIn {
+	for _, flow := range flows {
+		if flow.Alias != nil && (flow.BuiltIn == nil || !*flow.BuiltIn) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, flow := range flows {
if flow.Alias != nil && !*flow.BuiltIn {
flowName := *flow.Alias
if flowName != BrowserFlow && flowName != IDPPostLoginFlow {
executions, err := r.ctrl.client.GetAuthenticationExecutions(ctx, token, realm, flowName)
for _, flow := range flows {
if flow.Alias != nil && (flow.BuiltIn == nil || !*flow.BuiltIn) {
flowName := *flow.Alias
if flowName != BrowserFlow && flowName != IDPPostLoginFlow {
executions, err := r.ctrl.client.GetAuthenticationExecutions(ctx, token, realm, flowName)
🤖 Prompt for AI Agents
In pkg/controller/tenant/setup.go around lines 220 to 224, the code dereferences
flow.BuiltIn without checking for nil which can panic; update the conditional to
first ensure flow.BuiltIn is non-nil before dereferencing (e.g. require
flow.BuiltIn != nil && !*flow.BuiltIn alongside the existing flow.Alias != nil
check) so nil BuiltIn values are handled safely and the subsequent
GetAuthenticationExecutions call only runs for explicit non-built-in flows.

Comment thread pkg/keycloak/client.go
Comment on lines +258 to +260
if resp == nil || resp.IsError() {
return errors.Wrapf(errors.Unknown, "failed to create of copy authentication flow %s, status code: %d", src, resp.StatusCode())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle nil responses before calling StatusCode().
Line 259: when resp is nil the follow-up StatusCode() call panics; guard the nil case separately.

-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to create of copy authentication flow %s, status code: %d", src, resp.StatusCode())
+	if resp == nil {
+		return errors.Wrapf(errors.Unknown, "failed to copy authentication flow %s: empty response from admin API", src)
+	}
+	if resp.IsError() {
+		return errors.Wrapf(errors.Unknown, "failed to copy authentication flow %s, status code: %d", src, resp.StatusCode())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if resp == nil || resp.IsError() {
return errors.Wrapf(errors.Unknown, "failed to create of copy authentication flow %s, status code: %d", src, resp.StatusCode())
}
if resp == nil {
return errors.Wrapf(errors.Unknown, "failed to copy authentication flow %s: empty response from admin API", src)
}
if resp.IsError() {
return errors.Wrapf(errors.Unknown, "failed to copy authentication flow %s, status code: %d", src, resp.StatusCode())
}
🤖 Prompt for AI Agents
In pkg/keycloak/client.go around lines 258 to 260, the code may call
resp.StatusCode() when resp is nil causing a panic; first check if resp == nil
and return a wrapped error that notes there was no response (do not call
StatusCode), then separately check if resp.IsError() and return a wrapped error
that includes resp.StatusCode(); i.e. split the condition into two branches so
StatusCode() is only accessed when resp is non-nil.

Comment thread pkg/keycloak/client.go
Comment on lines +349 to +358
func (c *Client) LowerAuthenticationExecutionPriority(ctx context.Context, token, realm, executionId string) error {
resp, err := c.GetRequestWithBearerAuth(ctx, token).
Post(c.getAdminRealmURL(c.url, realm, "authentication", "executions", executionId, "lower-priority"))

if err != nil {
return err
}

if resp == nil || resp.IsError() {
return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority, status code: %d", resp.StatusCode())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Avoid dereferencing a nil response when lowering priority.
Line 357: guard the nil response before logging the status code.

-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority, status code: %d", resp.StatusCode())
+	if resp == nil {
+		return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority: empty response from admin API")
+	}
+	if resp.IsError() {
+		return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority, status code: %d", resp.StatusCode())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (c *Client) LowerAuthenticationExecutionPriority(ctx context.Context, token, realm, executionId string) error {
resp, err := c.GetRequestWithBearerAuth(ctx, token).
Post(c.getAdminRealmURL(c.url, realm, "authentication", "executions", executionId, "lower-priority"))
if err != nil {
return err
}
if resp == nil || resp.IsError() {
return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority, status code: %d", resp.StatusCode())
func (c *Client) LowerAuthenticationExecutionPriority(ctx context.Context, token, realm, executionId string) error {
resp, err := c.GetRequestWithBearerAuth(ctx, token).
Post(c.getAdminRealmURL(c.url, realm, "authentication", "executions", executionId, "lower-priority"))
if err != nil {
return err
}
if resp == nil {
return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority: empty response from admin API")
}
if resp.IsError() {
return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority, status code: %d", resp.StatusCode())
}
// …rest of implementation…
}
🤖 Prompt for AI Agents
In pkg/keycloak/client.go around lines 349 to 358, the current error branch
dereferences resp.StatusCode() before ensuring resp is non-nil; change the logic
to first check if resp == nil and return a wrapped error indicating no response,
and only if resp is non-nil and resp.IsError() return a wrapped error that
includes resp.StatusCode(); this ensures you never call StatusCode() on a nil
resp and preserves the original error paths.

@Suryanshu185 Suryanshu185 force-pushed the auth-flows branch 3 times, most recently from 0e6e9f4 to 100efac Compare October 6, 2025 10:24

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (5)
pkg/keycloak/client.go (3)

258-260: Handle nil responses before calling StatusCode().

Lines 258-260: when resp is nil, the follow-up StatusCode() call panics. Guard the nil case separately:

-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to create of copy authentication flow %s, status code: %d", src, resp.StatusCode())
+	if resp == nil {
+		return errors.Wrapf(errors.Unknown, "failed to copy authentication flow %s: empty response from admin API", src)
+	}
+	if resp.IsError() {
+		return errors.Wrapf(errors.Unknown, "failed to copy authentication flow %s, status code: %d", src, resp.StatusCode())

310-348: Add defensive checks for nil parameters and responses.

Two critical issues remain from past review:

  1. Missing nil guard for execution parameter: Lines 330-337 dereference e.ID and e.AuthenticationConfig without checking if e is nil.

  2. Nil response before StatusCode(): Lines 344-346 will panic if resp is nil.

Apply this diff:

 func (c *Client) ConfigureUserSessionLimit(ctx context.Context, token, realm string, e *gocloak.ModifyAuthenticationExecutionRepresentation, sessions int, denyNewSession bool, alias, errorMsg string) error {
+	if e == nil || e.ID == nil {
+		return errors.Wrapf(errors.InvalidArgument, "execution is nil or missing ID")
+	}
 	var err error
 	behavior := "Deny new session"
 	if !denyNewSession {
 		behavior = "Terminate oldest session"
 	}
 	config := struct {
 		//ID     *string            `json:"id,omitempty"`
 		Alias  *string            `json:"alias,omitempty"`
 		Config *map[string]string `json:"config,omitempty"`
 	}{
 		Alias: gocloak.StringP(alias),
 		Config: &map[string]string{
 			"userRealmLimit":  strconv.Itoa(sessions),
 			"behavior":        behavior,
 			"userClientLimit": "0",
 			"errorMessage":    errorMsg,
 		},
 	}
 	var resp *resty.Response
 	if e.AuthenticationConfig == nil {
 		resp, err = c.GetRequestWithBearerAuth(ctx, token).
 			SetBody(config).
 			Post(c.getAdminRealmURL(c.url, realm, "authentication", "executions", *e.ID, "config"))
 	} else {
 		resp, err = c.GetRequestWithBearerAuth(ctx, token).
 			SetBody(config).
 			Put(c.getAdminRealmURL(c.url, realm, "authentication", "config", *e.AuthenticationConfig))
 	}
 
 	if err != nil {
 		return err
 	}
 
-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to configure user session limit, status code: %d", resp.StatusCode())
+	if resp == nil {
+		return errors.Wrapf(errors.Unknown, "failed to configure user session limit: empty response from admin API")
+	}
+	if resp.IsError() {
+		return errors.Wrapf(errors.Unknown, "failed to configure user session limit, status code: %d", resp.StatusCode())
 	}
 	return nil
 }

358-360: Avoid dereferencing a nil response when lowering priority.

Lines 358-360: guard the nil response before logging the status code:

-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority, status code: %d", resp.StatusCode())
+	if resp == nil {
+		return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority: empty response from admin API")
+	}
+	if resp.IsError() {
+		return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority, status code: %d", resp.StatusCode())
pkg/controller/tenant/setup.go (2)

64-71: Guard against nil Alias before dereferencing.

Lines 64-71: the loop dereferences f.Alias without checking for nil, which can panic:

 	for _, f := range flows {
+		if f.Alias == nil {
+			continue
+		}
-		if *f.Alias == IDPPostLoginFlow {
+		switch *f.Alias {
+		case IDPPostLoginFlow:
 			idpPostLoginFlow = true
-		}
-		if *f.Alias == BrowserFlow {
+		case BrowserFlow:
 			browserFlow = true
 		}
 	}

220-224: Check BuiltIn for nil before dereferencing.

Line 221: flow.BuiltIn is optional in the admin response; dereferencing without a nil guard will panic:

 	for _, flow := range flows {
-		if flow.Alias != nil && !*flow.BuiltIn {
+		if flow.Alias != nil && (flow.BuiltIn == nil || !*flow.BuiltIn) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 0e6e9f48944ec98ac7baa3fe5c5bbb763123bd5b and 100efacd1e82c47570d7a8e3c6beb6f191797a8b.

📒 Files selected for processing (2)
  • pkg/controller/tenant/setup.go (4 hunks)
  • pkg/keycloak/client.go (2 hunks)
🔇 Additional comments (6)
pkg/keycloak/client.go (3)

265-308: LGTM!

The method correctly checks for nil pointers before dereferencing (lines 272, 277, 296, 301) and handles the create-if-not-found pattern appropriately.


365-413: LGTM!

The repositioning logic correctly guards against nil pointers (lines 380, 395) and includes a safety limit (maxAttempts = 20) to prevent infinite loops. The approach of repeatedly calling LowerAuthenticationExecutionPriority until the execution reaches the last position is appropriate.


415-433: LGTM with caveat on estimation.

The priority estimation (topLevelCount * 10) is a reasonable workaround given that the Priority field isn't exposed in the struct. The nil check on line 426 is properly implemented.

Note: This estimation assumes a spacing of 10 between priorities, which may not always hold if Keycloak's internal logic changes.

pkg/controller/tenant/setup.go (3)

31-44: LGTM!

The constants provide clear names for authentication flows, and the FlowExecutionReport type offers a structured representation for flow state (likely for reporting or external integration).


108-179: LGTM!

The flow orchestration correctly:

  • Detects the forms subflow dynamically (line 109)
  • Processes both target flows (IDPPostLoginFlow and forms subflow)
  • Locates/creates executions, configures session limits, positions them, and sets requirements
  • Includes verification and retry logic for positioning (lines 146-174)

The error handling and propagation are appropriate throughout.


335-363: LGTM!

The client configuration update logic correctly:

  • Guards against nil pointers before dereferencing (lines 343, 348, 353)
  • Only updates when necessary (needsUpdate flag)
  • Logs errors without failing the reconciliation

This ensures existing clients get the required defaults (Enabled, PublicClient, StandardFlowEnabled).

Comment thread pkg/controller/tenant/setup.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (6)
pkg/controller/tenant/setup.go (3)

64-71: Nil-pointer risk when dereferencing f.Alias.

Dereferencing f.Alias without a nil guard can panic. Add a nil check before comparison.

Apply this diff:

 	for _, f := range flows {
-		if *f.Alias == IDPPostLoginFlow {
+		if f.Alias != nil && *f.Alias == IDPPostLoginFlow {
 			idpPostLoginFlow = true
 		}
-		if *f.Alias == BrowserFlow {
+		if f.Alias != nil && *f.Alias == BrowserFlow {
 			browserFlow = true
 		}
 	}

363-363: Nil-pointer risk when comparing mapper names.

Dereferencing m.Name and mapper.Name without nil checks can panic. Verify both pointers are non-nil before comparison.

Apply this diff:

 					for _, m := range *found[0].ProtocolMappers {
-						if *m.Name == *mapper.Name {
+						if m.Name != nil && mapper.Name != nil && *m.Name == *mapper.Name {
 							mapperExists = true
 							break
 						}

50-53: Panic still present despite being marked as addressed.

The panic on token failure was flagged in a previous review and marked as "✅ Addressed in commit eb94349", but the code still contains log.Panicf on line 52. Return an error instead to allow the reconciler to requeue gracefully.

Apply this diff:

 	token, err := r.ctrl.client.GetAccessToken()
 	if err != nil {
-		log.Panicf("keycloak session not active: %s", err)
+		return errors.Wrapf(errors.Unauthenticated, "keycloak session not active: %s", err)
 	}
pkg/keycloak/client.go (3)

258-259: Nil response causes panic when calling StatusCode().

If resp is nil, calling resp.StatusCode() will panic. Handle the nil case separately before checking IsError().

Apply this diff:

-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to create of copy authentication flow %s, status code: %d", src, resp.StatusCode())
+	if resp == nil {
+		return errors.Wrapf(errors.Unknown, "failed to copy authentication flow %s: empty response from admin API", src)
+	}
+	if resp.IsError() {
+		return errors.Wrapf(errors.Unknown, "failed to copy authentication flow %s, status code: %d", src, resp.StatusCode())
 	}

310-348: Missing defensive checks and nil response handling.

  1. The function doesn't validate that e or e.ID is non-nil at the start, risking a panic when dereferencing on line 333.
  2. Line 344 calls resp.StatusCode() without first ensuring resp is non-nil.

Apply this diff:

 func (c *Client) ConfigureUserSessionLimit(ctx context.Context, token, realm string, e *gocloak.ModifyAuthenticationExecutionRepresentation, sessions int, denyNewSession bool, alias, errorMsg string) error {
+	if e == nil || e.ID == nil {
+		return errors.Wrapf(errors.InvalidArgument, "execution is nil or missing ID")
+	}
 	var err error
 	behavior := "Deny new session"

And:

-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to configure user session limit, status code: %d", resp.StatusCode())
+	if resp == nil {
+		return errors.Wrapf(errors.Unknown, "failed to configure user session limit: empty response from admin API")
+	}
+	if resp.IsError() {
+		return errors.Wrapf(errors.Unknown, "failed to configure user session limit, status code: %d", resp.StatusCode())
 	}

358-359: Nil response causes panic when calling StatusCode().

If resp is nil, calling resp.StatusCode() will panic. Handle the nil case separately before checking IsError().

Apply this diff:

-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority, status code: %d", resp.StatusCode())
+	if resp == nil {
+		return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority: empty response from admin API")
+	}
+	if resp.IsError() {
+		return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority, status code: %d", resp.StatusCode())
 	}
🧹 Nitpick comments (2)
pkg/controller/tenant/setup.go (2)

37-44: Unused type definition.

FlowExecutionReport is defined but never used within this file. Consider removing it if not needed, or add a comment explaining its intended future use.


168-174: Redundant repositioning call.

SetExecutionToPosition (line 132) already loops up to 20 times to position the execution last. Calling it again on line 170 is redundant, as the first call should have succeeded if positioning was possible.

Consider removing the redundant repositioning logic:

-		// If not last, attempt to reposition again
-		if !isLast {
-			err = r.ctrl.client.SetExecutionToPosition(ctx, token, realm, flowName, *userSessionExec.ID)
-			if err != nil {
-				return err
-			}
-		}
+		// If still not last after SetExecutionToPosition, log a warning
+		if !isLast {
+			log.Printf("Warning: execution %s in flow %s is not in last position after repositioning", *userSessionExec.ID, flowName)
+		}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 100efacd1e82c47570d7a8e3c6beb6f191797a8b and 68fd9ffef6d289895e0eb7d4e050c1fdb56a5cac.

📒 Files selected for processing (2)
  • pkg/controller/tenant/setup.go (4 hunks)
  • pkg/keycloak/client.go (2 hunks)
🔇 Additional comments (2)
pkg/keycloak/client.go (2)

365-413: LGTM!

The SetExecutionToPosition method correctly implements iterative priority lowering with proper safety limits and level-aware positioning logic. Good defensive checks for nil pointers (lines 380, 395) prevent panics.


415-433: LGTM!

The GetMaxExecutionPriority method correctly estimates maximum priority by counting top-level executions, with proper nil checking for the Level field.

- Add dynamic forms subflow detection with pattern matching and fallback
- Replace hardcoded flow names with robust flow discovery algorithm
- Implement SetExecutionToLastPosition for reliable execution positioning
- Configure user session limits to 5 sessions with deny new behavior (default)(TODO: make it configurable with a PUT api call)
- Fix execution positioning issues and infinite loop prevention

Signed-off-by: suryanshu185 <suryanshu.gupta@kluisz.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (6)
pkg/keycloak/client.go (3)

258-260: Handle nil responses before calling StatusCode().

This issue was previously flagged and remains unresolved. When resp is nil, calling resp.StatusCode() will panic. Additionally, there's a typo in the error message: "create of copy" should be "copy".

Apply this diff to fix both issues:

-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to create of copy authentication flow %s, status code: %d", src, resp.StatusCode())
+	if resp == nil {
+		return errors.Wrapf(errors.Unknown, "failed to copy authentication flow %s: empty response from admin API", src)
+	}
+	if resp.IsError() {
+		return errors.Wrapf(errors.Unknown, "failed to copy authentication flow %s, status code: %d", src, resp.StatusCode())

310-348: Add defensive nil checks and handle nil responses safely.

This issue was previously flagged and remains unresolved. The method needs:

  1. Defensive guard at the start to prevent nil dereference of e or e.ID
  2. Separate handling of resp == nil before calling resp.StatusCode()

Apply this diff:

 func (c *Client) ConfigureUserSessionLimit(ctx context.Context, token, realm string, e *gocloak.ModifyAuthenticationExecutionRepresentation, sessions int, denyNewSession bool, alias, errorMsg string) error {
+	if e == nil || e.ID == nil {
+		return errors.Wrapf(errors.InvalidArgument, "execution is nil or missing ID")
+	}
 	var err error
 	behavior := "Deny new session"
 	if !denyNewSession {
 		behavior = "Terminate oldest session"
 	}
 	config := struct {
 		//ID     *string            `json:"id,omitempty"`
 		Alias  *string            `json:"alias,omitempty"`
 		Config *map[string]string `json:"config,omitempty"`
 	}{
 		Alias: gocloak.StringP(alias),
 		Config: &map[string]string{
 			"userRealmLimit":  strconv.Itoa(sessions),
 			"behavior":        behavior,
 			"userClientLimit": "0",
 			"errorMessage":    errorMsg,
 		},
 	}
 	var resp *resty.Response
 	if e.AuthenticationConfig == nil {
 		resp, err = c.GetRequestWithBearerAuth(ctx, token).
 			SetBody(config).
 			Post(c.getAdminRealmURL(c.url, realm, "authentication", "executions", *e.ID, "config"))
 	} else {
 		resp, err = c.GetRequestWithBearerAuth(ctx, token).
 			SetBody(config).
 			Put(c.getAdminRealmURL(c.url, realm, "authentication", "config", *e.AuthenticationConfig))
 	}
 
 	if err != nil {
 		return err
 	}
 
-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to configure user session limit, status code: %d", resp.StatusCode())
+	if resp == nil {
+		return errors.Wrapf(errors.Unknown, "failed to configure user session limit: empty response from admin API")
+	}
+	if resp.IsError() {
+		return errors.Wrapf(errors.Unknown, "failed to configure user session limit, status code: %d", resp.StatusCode())
 	}
 	return nil
 }

350-363: Avoid dereferencing nil response when lowering priority.

This issue was previously flagged and remains unresolved. Guard the nil response before calling resp.StatusCode().

Apply this diff:

 func (c *Client) LowerAuthenticationExecutionPriority(ctx context.Context, token, realm, executionId string) error {
 	resp, err := c.GetRequestWithBearerAuth(ctx, token).
 		Post(c.getAdminRealmURL(c.url, realm, "authentication", "executions", executionId, "lower-priority"))
 
 	if err != nil {
 		return err
 	}
 
-	if resp == nil || resp.IsError() {
-		return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority, status code: %d", resp.StatusCode())
+	if resp == nil {
+		return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority: empty response from admin API")
+	}
+	if resp.IsError() {
+		return errors.Wrapf(errors.Unknown, "failed to lower authentication execution priority, status code: %d", resp.StatusCode())
 	}
 
 	return nil
 }
pkg/controller/tenant/setup.go (3)

41-44: Don't panic on transient token failures; return error to let reconciler requeue.

Panicking here crashes the controller process rather than allowing graceful retry. Return an error instead so the reconciler can requeue.

Apply this diff:

 	token, err := r.ctrl.client.GetAccessToken()
 	if err != nil {
-		log.Panicf("keycloak session not active: %s", err)
+		return fmt.Errorf("keycloak session not active: %w", err)
 	}

54-62: Nil-pointer risk: Guard f.Alias before dereferencing.

Both comparisons at lines 56 and 59 dereference f.Alias without checking for nil, which will panic if Keycloak returns a flow with a nil Alias.

Apply this diff:

 	var browserFlow, idpPostLoginFlow bool
 	for _, f := range flows {
-		if *f.Alias == IDPPostLoginFlow {
-			idpPostLoginFlow = true
-		}
-		if *f.Alias == BrowserFlow {
-			browserFlow = true
+		if f.Alias == nil {
+			continue
+		}
+		switch *f.Alias {
+		case IDPPostLoginFlow:
+			idpPostLoginFlow = true
+		case BrowserFlow:
+			browserFlow = true
 		}
 	}

348-368: Nil-pointer risk: Guard mapper names before dereferencing.

Line 354 dereferences both m.Name and mapper.Name without nil checks, which will panic if either is nil.

Apply this diff:

 		for _, mapper := range mList {
 			mapperExists := false
 			if len(found) != 0 {
 				// Check if mapper already exists
 				if found[0].ProtocolMappers != nil {
 					for _, m := range *found[0].ProtocolMappers {
-						if *m.Name == *mapper.Name {
+						if m.Name != nil && mapper.Name != nil && *m.Name == *mapper.Name {
 							mapperExists = true
 							break
 						}
 					}
 				}
 			}
🧹 Nitpick comments (1)
pkg/controller/tenant/setup.go (1)

108-167: Consider making the session limit configurable.

Line 117 hardcodes userRealmLimit=5. If different tenants require different limits, consider making this configurable via the tenant spec or a controller parameter.

The positioning verification logic (lines 146-165) is complex but handles nil checks properly. It correctly identifies when the user-session-limits execution is not last at its level and attempts repositioning.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 68fd9ffef6d289895e0eb7d4e050c1fdb56a5cac and 2208044.

📒 Files selected for processing (2)
  • pkg/controller/tenant/setup.go (4 hunks)
  • pkg/keycloak/client.go (2 hunks)
🔇 Additional comments (9)
pkg/keycloak/client.go (4)

10-10: LGTM!

The new imports are appropriately added to support numeric string conversion and HTTP response handling in the authentication flow management methods.

Also applies to: 16-16


265-308: LGTM!

The method correctly locates existing executions or creates new ones when needed. The logic properly filters for top-level executions and includes appropriate error handling for both the locate and create paths.


365-413: LGTM!

The method correctly repositions executions to the last position with proper safety mechanisms. The logic properly handles same-level comparisons and includes a reasonable attempt limit to prevent infinite loops.


415-433: Verify the priority estimation formula aligns with Keycloak's actual priority scheme.

The function estimates max priority as topLevelCount * 10, but this formula may not match Keycloak's actual priority assignment. The function name suggests it retrieves an actual maximum, but it computes an estimate instead.

Consider one of the following:

  1. Verify that Keycloak uses increments of 10 for execution priorities by checking the Admin REST API response for actual priority values
  2. Rename to EstimateMaxExecutionPriority to clarify it's an approximation
  3. If the estimation is sufficient for the use case, document why this formula is reliable
pkg/controller/tenant/setup.go (5)

31-35: LGTM: Clear flow constants.

The authentication flow constants are well-named and properly documented.


64-97: LGTM: Flow creation and realm update logic is sound.

The flow ensures both authentication flows exist (creating or copying as needed) and correctly updates the realm to use the custom browser flow.


172-199: LGTM: Convention-based subflow detection with validation.

The helper correctly constructs the expected forms subflow name by convention and validates its contents by checking for form authenticators. If Keycloak's naming convention changes in future versions, the validation check (lines 182-196) will catch it.


239-243: LGTM: Proper integration with reconciliation flow.

The auth flow configuration is invoked at the right point (after realm/client creation) with appropriate error handling and requeue logic.


291-320: LGTM: Client update logic with proper checks.

The code correctly verifies existing client configuration and only performs updates when necessary. The pattern of checking both nil and value is appropriate for optional fields.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants