Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughThis PR restructures SSO functionality by promoting OIDC to open-core availability and establishing a separate enterprise SAML module. It introduces core SSO services, routes, and models; adds organization role tracking to user responses; creates a new subscription endpoint; and updates documentation and dashboard UI to reflect licensing distinctions. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant SsoRoutes as SsoRoutes<br/>(Core)
participant SsoService
participant IdP as External IdP
participant DB as Database
participant AuthService
Client->>SsoRoutes: POST /auth/sso/init (email/orgSlug)
SsoRoutes->>SsoService: initSso(email, orgSlug)
SsoService->>DB: Fetch SSO config
SsoService->>SsoService: generateSecureState(orgId)
SsoService-->>SsoRoutes: SsoInitResponse (redirect URL, state)
SsoRoutes-->>Client: Redirect to IdP
Client->>IdP: Authenticate
IdP-->>Client: Redirect with code+state
Client->>SsoRoutes: GET /auth/sso/oidc/callback?code=X&state=Y
SsoRoutes->>SsoService: handleOidcCallback(code, state)
SsoService->>SsoService: decodeState(state)
SsoService->>SsoService: discoverOidcEndpoints(issuer)
SsoService->>IdP: Exchange code for token
IdP-->>SsoService: ID token
SsoService->>SsoService: Extract user info
SsoService->>DB: findOrCreateSsoUser()
SsoService->>DB: addToOrgIfNeeded()
SsoService->>AuthService: generateToken(userId, email)
AuthService-->>SsoService: JWT
SsoService-->>SsoRoutes: SsoCallbackData
SsoRoutes-->>Client: Set auth cookie, redirect to frontend
sequenceDiagram
participant Client
participant SamlRoutes as SamlRoutes<br/>(Enterprise)
participant SamlService
participant IdP as SAML IdP
participant DB as Database
Client->>SamlRoutes: POST /auth/sso/init/saml (email/orgSlug)
SamlRoutes->>SamlService: initSaml(email, orgSlug)
SamlService->>DB: Fetch SAML config by domain/org
SamlService->>SamlService: generateSamlRequest(config, state)
SamlService->>SamlService: generateSecureState(orgId)
SamlService-->>SamlRoutes: SsoInitResponse
SamlRoutes-->>Client: Redirect to IdP with SAML request
Client->>IdP: Authenticate
IdP-->>Client: SAML Response (POST to ACS)
Client->>SamlRoutes: POST /auth/sso/acs (SAMLResponse)
SamlRoutes->>SamlService: handleSamlResponse(response, relayState)
SamlService->>SamlService: decodeState(relayState)
SamlService->>DB: Fetch SAML config
SamlService->>SamlService: buildSamlSettings(config)
SamlService->>SamlService: Validate & extract assertion
SamlService->>DB: findOrCreateSsoUser()
SamlService-->>SamlRoutes: SsoCallbackData
SamlRoutes-->>Client: Set auth cookie, redirect
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment Tip You can disable sequence diagrams in the walkthrough.Disable the |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/src/main/kotlin/com/moneat/auth/services/AuthService.kt (1)
538-548:⚠️ Potential issue | 🟠 MajorPopulate org context in every
AuthResponse.user.This path now sets
organizationSlug/orgRole, butsignup(),login(), andrefreshToken()in this same file still buildUserResponsewithout them. The dashboard now gates Team/SSO offuser.orgRole, so the same user can get different UI capabilities depending on which auth response last hydrated state. Please return the same org context from allAuthServiceUserResponsebuilders.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/kotlin/com/moneat/auth/services/AuthService.kt` around lines 538 - 548, AuthService builds UserResponse without org context in signup(), login(), and refreshToken() causing inconsistent UI; update those UserResponse constructors to include the same organizationSlug and orgRole values used elsewhere (i.e. compute the final organization slug (finalSlug or equivalent from membership.organization?.slug) and use membership.role for orgRole) and pass them into the UserResponse instead of the current null, so every auth path returns a UserResponse populated with organizationSlug and orgRole.
🧹 Nitpick comments (7)
ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.kt (3)
157-204: SimplifygetSamlMetadatawith early return andrequireNotNull.The method structure can be simplified. Also, the null check at line 183 is redundant after the query - if
orgSlugis null, the exception is thrown at line 178-180, sossoConfigbeing null is the actual check needed.♻️ Proposed fix
fun getSamlMetadata(orgSlug: String?): String = transaction { - val ssoConfig = - if (orgSlug != null) { - val org = - Organizations - .selectAll() - .where { Organizations.slug eq orgSlug } - .firstOrNull() - ?: throw IllegalArgumentException( - "Organization not found" - ) - - SsoConfigurations - .selectAll() - .where { - (SsoConfigurations.organizationId eq org[Organizations.id]) and - (SsoConfigurations.providerType eq "saml") and - (SsoConfigurations.isEnabled eq true) - }.firstOrNull() - } else { - throw IllegalArgumentException( - "Organization slug is required" - ) - } - - if (ssoConfig == null) { - throw IllegalArgumentException( - "SAML SSO is not configured for this organization" - ) - } + requireNotNull(orgSlug) { "Organization slug is required" } + + val org = Organizations + .selectAll() + .where { Organizations.slug eq orgSlug } + .firstOrNull() + ?: throw IllegalArgumentException("Organization not found") + + val ssoConfig = requireNotNull( + SsoConfigurations + .selectAll() + .where { + (SsoConfigurations.organizationId eq org[Organizations.id]) and + (SsoConfigurations.providerType eq "saml") and + (SsoConfigurations.isEnabled eq true) + }.firstOrNull() + ) { "SAML SSO is not configured for this organization" } val settings = buildSamlSettings(ssoConfig)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.kt` around lines 157 - 204, Refactor getSamlMetadata to simplify control flow: validate orgSlug early with requireNotNull (or throw) and load the organization via Organizations.selectAll().where { Organizations.slug eq orgSlug }.firstOrNull(), then fetch the SSO row from SsoConfigurations (filtering organizationId, providerType "saml", isEnabled) and use requireNotNull on that result (instead of the later if (ssoConfig == null) block); keep calling buildSamlSettings(ssoConfig), validate metadata with Saml2Settings.validateMetadata, log errors with logger.error and throw if invalid, and return metadata—this removes nested if/else and the redundant null check while preserving existing validations.
74-78: UserequireNotNull()for null checks with exceptions.This pattern can be simplified using the idiomatic
requireNotNull().♻️ Proposed fix
- if (ssoConfig == null) { - throw IllegalArgumentException( - "SAML SSO is not configured for this organization" - ) - } + requireNotNull(ssoConfig) { + "SAML SSO is not configured for this organization" + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.kt` around lines 74 - 78, Replace the manual null check for ssoConfig in SamlService with the idiomatic Kotlin requireNotNull usage: use requireNotNull(ssoConfig) with the same message ("SAML SSO is not configured for this organization") and assign the result to a non-null val (or rebind) so subsequent code uses the non-null value; this keeps the IllegalArgumentException semantics but simplifies the check and makes the intent clearer.
38-42: Userequire()for argument preconditions.Per coding guidelines,
require()should be used for argument preconditions in helper/library code. This service method validates input arguments and should use the idiomatic Kotlin approach.♻️ Proposed fix
- if (email == null && orgSlug == null) { - throw IllegalArgumentException( - "Either email or orgSlug must be provided" - ) - } + require(!(email == null && orgSlug == null)) { + "Either email or orgSlug must be provided" + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.kt` around lines 38 - 42, Replace the manual null-check and throw with Kotlin's idiomatic require() precondition: in SamlService.kt where the code currently checks if (email == null && orgSlug == null) and throws IllegalArgumentException("Either email or orgSlug must be provided"), change it to use require(email != null || orgSlug != null) { "Either email or orgSlug must be provided" } so the argument validation uses the standard Kotlin helper instead of an explicit throw; update the surrounding method in SamlService accordingly.ee/backend/src/main/kotlin/com/moneat/enterprise/sso/routes/SamlRoutes.kt (1)
27-96: Consider extracting error handling to reduce cognitive complexity.Static analysis reports cognitive complexity of 18 (threshold: 15). The repetitive try-catch patterns across routes contribute to this. While the current code is readable and consistent, you could optionally extract a helper for error handling.
This is a minor concern for a route file with standard error handling patterns. The current implementation is clear and maintainable.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ee/backend/src/main/kotlin/com/moneat/enterprise/sso/routes/SamlRoutes.kt` around lines 27 - 96, The three route handlers in samlRoutes() (post("/init/saml"), get("/saml/metadata"), post("/saml/acs")) repeat identical try/catch logic causing high cognitive complexity; extract a reusable helper (e.g., withErrorHandling or handleSamlRoute) that accepts a suspend lambda and centralizes the IllegalArgumentException and generic Exception handling/logging/response behavior so you can replace the inline try/catch in each handler while still invoking SamlService.initSaml, SamlService.getSamlMetadata, and SamlService.handleSamlResponse and preserving AuthCookieUtils.setAuthCookie and the same redirects/responses.backend/src/main/kotlin/com/moneat/sso/services/SsoService.kt (2)
112-114: Consider usingrequire()for preconditions in service code.SonarCloud suggests replacing this
if-throwwithrequire(). Since this is service/library code (not a route handler), usingrequire()is idiomatic Kotlin for argument validation. As per coding guidelines,require()is appropriate "for internal argument preconditions in helper/library code."♻️ Proposed refactor
-if (email == null && orgSlug == null) { - throw IllegalArgumentException("Either email or orgSlug must be provided") -} +require(email != null || orgSlug != null) { + "Either email or orgSlug must be provided" +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/kotlin/com/moneat/sso/services/SsoService.kt` around lines 112 - 114, Replace the manual if-throw precondition in SsoService (where the method checks email and orgSlug) with Kotlin's idiomatic require(): instead of the current if (email == null && orgSlug == null) throw IllegalArgumentException(...), call require(email != null || orgSlug != null) { "Either email or orgSlug must be provided" } so the argument validation uses require() for this service/library-level precondition.
266-398: High cognitive complexity (24 vs. 15 allowed) — consider decomposition.SonarCloud flags this method for exceeding cognitive complexity limits. The function handles authorization, validation for multiple provider types, and persistence (both insert and update paths). Consider extracting:
validateSsoConfigRequest()for provider-specific validationpersistSsoConfig()for the insert/update logicThis would improve readability and testability.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/kotlin/com/moneat/sso/services/SsoService.kt` around lines 266 - 398, The configureSso method in SsoService is too complex; extract provider-specific validation and DB persistence into helper functions to reduce cognitive complexity: create validateSsoConfigRequest(providerType, request) (called from configureSso before transactions) that encapsulates SAML/OIDC field checks and UrlValidator usage, and create persistSsoConfig(organizationId, request, providerType, baseUrl) that performs the encryptSecret logic, computes effectiveRequireSso, and executes the insert-or-update against SsoConfigurations (preserving current field assignments and updatedAt behavior), then have configureSso call these helpers and return getSsoConfig(organizationId) as before. Ensure new helpers reference the same symbols used now (SsoProviderType, UrlValidator, encryptSecret, FeatureRegistry.hasModule("SAML"), SsoConfigurations, getSsoConfig) and keep all transactional DB access inside persistSsoConfig to maintain existing behavior.dashboard/src/hooks/useAuth.ts (1)
47-51: UseglobalThis.sessionStoragefor SSR compatibility.The hook accesses
sessionStoragedirectly. Per coding guidelines, preferglobalThis.sessionStorageover bare globals for SSR compatibility.♻️ Proposed fix
// Keep session flag in sync - sessionStorage.setItem('authenticated', 'true') + globalThis.sessionStorage.setItem('authenticated', 'true') }) .catch(() => { setUser(null) - sessionStorage.removeItem('authenticated') + globalThis.sessionStorage.removeItem('authenticated') })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@dashboard/src/hooks/useAuth.ts` around lines 47 - 51, The hook uses the global sessionStorage directly which breaks SSR; update references in useAuth (the promise handlers that call sessionStorage.setItem and sessionStorage.removeItem) to use globalThis.sessionStorage instead, i.e., replace direct sessionStorage access in the success and .catch blocks (where setUser is called) with globalThis.sessionStorage to ensure safe access during server-side rendering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/src/main/kotlin/com/moneat/sso/routes/SsoRoutes.kt`:
- Around line 161-207: The current catch-all for IllegalArgumentException in the
put("/config") handler misclassifies authorization/licensing failures as 400;
change the error handling so only validation errors are returned as 400 and let
domain/authorization/licensing errors propagate to the central StatusPages
mapper (or be thrown as domain-specific exceptions). Concretely: in SsoRoutes'
put("/config") block stop catching IllegalArgumentException broadly—catch only
BadRequestException (or equivalent validation errors) and respond
HttpStatusCode.BadRequest with ErrorResponse; remove the
IllegalArgumentException catch so configureSso(orgId, userId, request) can throw
domain-specific exceptions (e.g., an AuthorizationException or
LicensingException) or IllegalArgumentException/IllegalStateException to be
mapped by StatusPages to 403/appropriate codes. Ensure ssoService.configureSso
emits domain-specific exceptions for owner/licensing checks rather than
IllegalArgumentException if not already.
In `@backend/src/main/kotlin/com/moneat/sso/services/SsoService.kt`:
- Around line 789-806: The consumeNonce function currently swallows
non-IllegalArgumentExceptions when Redis is unavailable, allowing nonce replay;
change this to "fail closed" by replacing the generic exception handler so that
any Redis-access failure (exceptions from RedisConfig.sync().get/del) results in
throwing a clear runtime exception (e.g., IllegalStateException or a custom
SsoVerificationException) with context including the nonceB64 and
SSO_NONCE_PREFIX rather than only logger.warn; keep the existing behavior of
rethrowing IllegalArgumentException for explicit nonce reuse, but for all other
exceptions from RedisConfig ensure you propagate an error to abort the SSO flow
instead of silently continuing.
- Around line 620-622: The current use of kotlinx.coroutines.runBlocking in
discoverOidcEndpoints blocks the DB transaction thread (called from transaction
contexts like handleOidcCallback) and risks exhausting the connection pool;
change discoverOidcEndpoints to be a suspending function that performs the
httpClient.get call without runBlocking and either (a) invoke it before entering
the transaction in handleOidcCallback, or (b) if it must run inside
transactional logic, call it from within a coroutine-aware transaction using
newSuspendedTransaction so the HTTP call does not block the DB thread.
- Around line 642-649: The code force-unwraps issuerUrl when calling
discoverOidcEndpoints(issuerUrl!!) even though earlier validation only runs if
issuerUrl is non-blank; make issuerUrl non-null before use by either enforcing
an early check (e.g., requireNotNull or throw a descriptive exception) or by
moving UrlValidator.validateExternalUrl to run unconditionally after a non-null
assertion, and then call discoverOidcEndpoints with the guaranteed-non-null
issuerUrl; update references in this block: issuerUrl,
SsoConfigurations.oidcIssuerUrl, UrlValidator.validateExternalUrl, and
discoverOidcEndpoints to reflect the non-null contract.
- Around line 103-106: The current lazy property encryptionKey in SsoService
uses jwtSecret.toByteArray().copyOf(AES_KEY_LENGTH) which pads/truncates
insecurely; replace this with a proper key derivation step (e.g., HKDF or
PBKDF2) that deterministically derives a fixed-length AES key from jwtSecret and
any necessary salt/info, then return the derived key bytes (AES_KEY_LENGTH) as
the encryptionKey; locate the encryptionKey property and update it to call a KDF
helper (implement e.g., deriveAesKeyFromJwtSecret(jwtSecret): ByteArray using
javax.crypto.SecretKeyFactory/PBKDF2 or an HKDF implementation, and wrap result
in a SecretKeySpec or copyOfExact to AES_KEY_LENGTH), ensuring errors are
handled and no silent truncation/padding occurs.
In `@dashboard/src/components/SsoSettings.tsx`:
- Around line 308-325: The switch is disabled whenever hasSamlModule is false,
which prevents downgraded orgs from turning off an already-true
formData.requireSso; update the Switch so it's only disabled when the module is
missing AND the current value is false (e.g. disabled={!hasSamlModule &&
!formData.requireSso}), keeping the existing onCheckedChange (setFormData) logic
so users can flip requireSso off even after a downgrade; ensure you update the
Switch component usage in SsoSettings (checked={formData.requireSso},
onCheckedChange, disabled) accordingly.
In `@dashboard/src/routes/settings.tsx`:
- Around line 172-174: The SSO access logic is too permissive: derive a view
gate and an owner-only configure gate so the UI matches backend entitlements.
Replace the single canUseSso with two booleans, e.g. canViewSsoTab (true if
isSelfHosted && hasSamlModule OR (not isSelfHosted && (tier === 'TEAM' || tier
=== 'BUSINESS'))) and canConfigureSso (true only when user?.orgRole === 'owner'
and likewise scoped to self-hosted/module or SaaS tiers), and update usages of
configureSso() and the tab rendering to use canConfigureSso for editable actions
and canViewSsoTab for visibility; keep hasSamlModule considered only for
self-hosted paths and do not grant SaaS FREE-tier access based on the global
features list.
In `@ee/backend/src/main/kotlin/com/moneat/enterprise/sso/routes/SamlRoutes.kt`:
- Line 29: The code in SamlRoutes.kt uses EnvConfig.get("FRONTEND_URL")!! which
can NPE; replace this with a safe retrieval that either fails fast with a
descriptive exception or provides a sensible dev default. Locate the frontendUrl
assignment in SamlRoutes and change it to use a null-coalescing check on
EnvConfig.get("FRONTEND_URL") (e.g., EnvConfig.get(...) ?: throw
IllegalStateException("FRONTEND_URL must be set for SAML routes")) or supply a
clearly documented default (e.g., "http://localhost:3000") so the app won't
crash with a raw NPE. Ensure the error message references FRONTEND_URL and SAML
startup so logs are actionable.
In
`@ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.kt`:
- Around line 114-123: The SAML response validation is using relayState
incorrectly; change the flow so that when you create the AuthnRequest
(authnRequest.getId()) you capture and store that requestId alongside the relay
state, and then call samlResponseObj.isValid(requestId) instead of
samlResponseObj.isValid(relayState); update SamlService (the code around
SamlResponse, SamlResponse.loadXmlFromBase64 and the isValid call) to retrieve
the stored requestId and pass it into isValid(requestId), and remove relayState
from the isValid invocation so InResponseTo is validated correctly.
---
Outside diff comments:
In `@backend/src/main/kotlin/com/moneat/auth/services/AuthService.kt`:
- Around line 538-548: AuthService builds UserResponse without org context in
signup(), login(), and refreshToken() causing inconsistent UI; update those
UserResponse constructors to include the same organizationSlug and orgRole
values used elsewhere (i.e. compute the final organization slug (finalSlug or
equivalent from membership.organization?.slug) and use membership.role for
orgRole) and pass them into the UserResponse instead of the current null, so
every auth path returns a UserResponse populated with organizationSlug and
orgRole.
---
Nitpick comments:
In `@backend/src/main/kotlin/com/moneat/sso/services/SsoService.kt`:
- Around line 112-114: Replace the manual if-throw precondition in SsoService
(where the method checks email and orgSlug) with Kotlin's idiomatic require():
instead of the current if (email == null && orgSlug == null) throw
IllegalArgumentException(...), call require(email != null || orgSlug != null) {
"Either email or orgSlug must be provided" } so the argument validation uses
require() for this service/library-level precondition.
- Around line 266-398: The configureSso method in SsoService is too complex;
extract provider-specific validation and DB persistence into helper functions to
reduce cognitive complexity: create validateSsoConfigRequest(providerType,
request) (called from configureSso before transactions) that encapsulates
SAML/OIDC field checks and UrlValidator usage, and create
persistSsoConfig(organizationId, request, providerType, baseUrl) that performs
the encryptSecret logic, computes effectiveRequireSso, and executes the
insert-or-update against SsoConfigurations (preserving current field assignments
and updatedAt behavior), then have configureSso call these helpers and return
getSsoConfig(organizationId) as before. Ensure new helpers reference the same
symbols used now (SsoProviderType, UrlValidator, encryptSecret,
FeatureRegistry.hasModule("SAML"), SsoConfigurations, getSsoConfig) and keep all
transactional DB access inside persistSsoConfig to maintain existing behavior.
In `@dashboard/src/hooks/useAuth.ts`:
- Around line 47-51: The hook uses the global sessionStorage directly which
breaks SSR; update references in useAuth (the promise handlers that call
sessionStorage.setItem and sessionStorage.removeItem) to use
globalThis.sessionStorage instead, i.e., replace direct sessionStorage access in
the success and .catch blocks (where setUser is called) with
globalThis.sessionStorage to ensure safe access during server-side rendering.
In `@ee/backend/src/main/kotlin/com/moneat/enterprise/sso/routes/SamlRoutes.kt`:
- Around line 27-96: The three route handlers in samlRoutes()
(post("/init/saml"), get("/saml/metadata"), post("/saml/acs")) repeat identical
try/catch logic causing high cognitive complexity; extract a reusable helper
(e.g., withErrorHandling or handleSamlRoute) that accepts a suspend lambda and
centralizes the IllegalArgumentException and generic Exception
handling/logging/response behavior so you can replace the inline try/catch in
each handler while still invoking SamlService.initSaml,
SamlService.getSamlMetadata, and SamlService.handleSamlResponse and preserving
AuthCookieUtils.setAuthCookie and the same redirects/responses.
In
`@ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.kt`:
- Around line 157-204: Refactor getSamlMetadata to simplify control flow:
validate orgSlug early with requireNotNull (or throw) and load the organization
via Organizations.selectAll().where { Organizations.slug eq orgSlug
}.firstOrNull(), then fetch the SSO row from SsoConfigurations (filtering
organizationId, providerType "saml", isEnabled) and use requireNotNull on that
result (instead of the later if (ssoConfig == null) block); keep calling
buildSamlSettings(ssoConfig), validate metadata with
Saml2Settings.validateMetadata, log errors with logger.error and throw if
invalid, and return metadata—this removes nested if/else and the redundant null
check while preserving existing validations.
- Around line 74-78: Replace the manual null check for ssoConfig in SamlService
with the idiomatic Kotlin requireNotNull usage: use requireNotNull(ssoConfig)
with the same message ("SAML SSO is not configured for this organization") and
assign the result to a non-null val (or rebind) so subsequent code uses the
non-null value; this keeps the IllegalArgumentException semantics but simplifies
the check and makes the intent clearer.
- Around line 38-42: Replace the manual null-check and throw with Kotlin's
idiomatic require() precondition: in SamlService.kt where the code currently
checks if (email == null && orgSlug == null) and throws
IllegalArgumentException("Either email or orgSlug must be provided"), change it
to use require(email != null || orgSlug != null) { "Either email or orgSlug must
be provided" } so the argument validation uses the standard Kotlin helper
instead of an explicit throw; update the surrounding method in SamlService
accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 565bd550-8405-415c-8bda-50d57079dd73
📒 Files selected for processing (24)
.env.exampleCONTRIBUTING.mdREADME.mdbackend/src/main/kotlin/com/moneat/auth/services/AuthService.ktbackend/src/main/kotlin/com/moneat/events/models/ApiModels.ktbackend/src/main/kotlin/com/moneat/events/routes/ApiRoutes.ktbackend/src/main/kotlin/com/moneat/sso/SsoModule.ktbackend/src/main/kotlin/com/moneat/sso/models/SsoModels.ktbackend/src/main/kotlin/com/moneat/sso/routes/SsoRoutes.ktbackend/src/main/kotlin/com/moneat/sso/services/SsoService.ktbackend/src/main/resources/META-INF/services/com.moneat.enterprise.EnterpriseModuledashboard/src/components/SsoSettings.tsxdashboard/src/docs/pages/billing.mdxdashboard/src/docs/pages/sso-authentication.mdxdashboard/src/hooks/useAuth.tsdashboard/src/lib/api/modules/user.tsdashboard/src/routes/settings.tsxee/README.mdee/backend/src/main/kotlin/com/moneat/enterprise/sso/SamlModule.ktee/backend/src/main/kotlin/com/moneat/enterprise/sso/routes/SamlRoutes.ktee/backend/src/main/kotlin/com/moneat/enterprise/sso/routes/SsoRoutes.ktee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.ktee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SsoService.ktee/backend/src/main/resources/META-INF/services/com.moneat.enterprise.EnterpriseModule
💤 Files with no reviewable changes (2)
- ee/backend/src/main/kotlin/com/moneat/enterprise/sso/routes/SsoRoutes.kt
- ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SsoService.kt
| put("/config") { | ||
| try { | ||
| val principal = call.principal<JWTPrincipal>() | ||
| val userId = | ||
| principal?.payload?.getClaim("userId")?.asInt() | ||
| ?: return@put call.respond( | ||
| HttpStatusCode.Unauthorized, | ||
| ErrorResponse("Invalid token") | ||
| ) | ||
|
|
||
| val request = call.receive<SsoConfigRequest>() | ||
| val orgId = | ||
| call.parameters["organizationId"]?.toIntOrNull() | ||
| ?: return@put call.respond( | ||
| HttpStatusCode.BadRequest, | ||
| ErrorResponse("Missing organizationId") | ||
| ) | ||
|
|
||
| // Reject SAML config when SAML module is not loaded | ||
| val providerType = SsoProviderType.fromString( | ||
| request.providerType | ||
| ) | ||
| if (providerType == SsoProviderType.SAML && | ||
| !FeatureRegistry.hasModule("SAML") | ||
| ) { | ||
| return@put call.respond( | ||
| HttpStatusCode.Forbidden, | ||
| ErrorResponse( | ||
| "SAML SSO requires an enterprise license" | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| val config = ssoService.configureSso( | ||
| orgId, | ||
| userId, | ||
| request, | ||
| ) | ||
| call.respond(config) | ||
| } catch (e: IllegalArgumentException) { | ||
| logger.error(e) { | ||
| "Configure SSO failed: ${e.message}" | ||
| } | ||
| call.respond( | ||
| HttpStatusCode.BadRequest, | ||
| ErrorResponse(e.message) | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the SsoService file
fd -t f SsoService.ktRepository: moneat-io/moneat
Length of output: 121
🏁 Script executed:
# Look at the configureSso method to see what exceptions it throws
rg -A 50 "fun configureSso" --type ktRepository: moneat-io/moneat
Length of output: 86
🏁 Script executed:
# Check for StatusPages configuration in the application
rg -i "statuspage" --type kt -B 2 -A 5Repository: moneat-io/moneat
Length of output: 86
🏁 Script executed:
# Examine the full put("/config") handler to see the complete context
sed -n '145,220p' backend/src/main/kotlin/com/moneat/sso/routes/SsoRoutes.ktRepository: moneat-io/moneat
Length of output: 3145
🏁 Script executed:
# Read the SsoService.configureSso implementation
sed -n '1,50p' backend/src/main/kotlin/com/moneat/sso/services/SsoService.ktRepository: moneat-io/moneat
Length of output: 2230
🏁 Script executed:
# Search for the configureSso method definition with broader context
rg "fun configureSso" -A 100Repository: moneat-io/moneat
Length of output: 10480
🏁 Script executed:
# Look for exception throwing patterns in SsoService
rg "throw IllegalArgumentException" -B 5 -A 2Repository: moneat-io/moneat
Length of output: 50373
🏁 Script executed:
# Check for StatusPages module configuration
rg "install\(StatusPages" -A 20Repository: moneat-io/moneat
Length of output: 3634
🏁 Script executed:
# Get full StatusPages configuration from Monitoring.kt
cat backend/src/main/kotlin/com/moneat/plugins/Monitoring.ktRepository: moneat-io/moneat
Length of output: 7457
🏁 Script executed:
# Look for BadRequestException or domain exception definitions
rg "BadRequestException|ForbiddenException|UnauthorizedException" --type ktRepository: moneat-io/moneat
Length of output: 86
🏁 Script executed:
# Search for any exception handler configuration
rg "exception<.*>" --type kt -A 3Repository: moneat-io/moneat
Length of output: 86
🏁 Script executed:
# Search for exception class definitions
rg "class.*Exception|object.*Exception" | grep -v ".class" | head -20Repository: moneat-io/moneat
Length of output: 42
🏁 Script executed:
# Check imports in SsoRoutes to see what exception types are available
head -40 backend/src/main/kotlin/com/moneat/sso/routes/SsoRoutes.ktRepository: moneat-io/moneat
Length of output: 1699
🏁 Script executed:
# Look for any other route handlers that catch IllegalArgumentException to see pattern
rg "catch.*IllegalArgumentException" -B 5 -A 5 | head -80Repository: moneat-io/moneat
Length of output: 8050
🏁 Script executed:
# Check if BadRequestException is available from Ktor or if it's a custom class
rg "BadRequestException" --type kt | head -10Repository: moneat-io/moneat
Length of output: 86
🏁 Script executed:
# Look at the full context around the put("/config") catch block to understand all error types
sed -n '145,230p' backend/src/main/kotlin/com/moneat/sso/routes/SsoRoutes.ktRepository: moneat-io/moneat
Length of output: 3654
Don't report owner/licensing failures as HTTP 400.
configureSso() throws IllegalArgumentException for authorization failures ("Only organization owners can configure SSO") and entitlement failures ("SSO enforcement requires an enterprise license"). Catching all IllegalArgumentException here and returning 400 Bad Request misrepresents both as request validation errors. Per the coding guideline for **/routes/**/*.kt, throw domain-specific exceptions or use BadRequestException only for actual validation errors, and map IllegalArgumentException and IllegalStateException centrally via the StatusPages plugin. Authorization failures should return 403, not 400.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/main/kotlin/com/moneat/sso/routes/SsoRoutes.kt` around lines 161
- 207, The current catch-all for IllegalArgumentException in the put("/config")
handler misclassifies authorization/licensing failures as 400; change the error
handling so only validation errors are returned as 400 and let
domain/authorization/licensing errors propagate to the central StatusPages
mapper (or be thrown as domain-specific exceptions). Concretely: in SsoRoutes'
put("/config") block stop catching IllegalArgumentException broadly—catch only
BadRequestException (or equivalent validation errors) and respond
HttpStatusCode.BadRequest with ErrorResponse; remove the
IllegalArgumentException catch so configureSso(orgId, userId, request) can throw
domain-specific exceptions (e.g., an AuthorizationException or
LicensingException) or IllegalArgumentException/IllegalStateException to be
mapped by StatusPages to 403/appropriate codes. Ensure ssoService.configureSso
emits domain-specific exceptions for owner/licensing checks rather than
IllegalArgumentException if not already.
| private val encryptionKey: ByteArray by lazy { | ||
| val key = jwtSecret.toByteArray() | ||
| key.copyOf(AES_KEY_LENGTH) | ||
| } |
There was a problem hiding this comment.
Encryption key derivation truncates JWT secret unpredictably.
jwtSecret.toByteArray().copyOf(AES_KEY_LENGTH) pads with zeros if the secret is shorter than 32 bytes, which weakens the key. If the secret is longer, it silently truncates. Consider using a proper key derivation function (e.g., HKDF) to derive a fixed-length AES key from the JWT secret.
🔐 Proposed fix using HKDF-like derivation
private val encryptionKey: ByteArray by lazy {
- val key = jwtSecret.toByteArray()
- key.copyOf(AES_KEY_LENGTH)
+ val mac = Mac.getInstance("HmacSHA256")
+ mac.init(SecretKeySpec(jwtSecret.toByteArray(), "HmacSHA256"))
+ mac.doFinal("sso-encryption-key".toByteArray()).copyOf(AES_KEY_LENGTH)
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/main/kotlin/com/moneat/sso/services/SsoService.kt` around lines
103 - 106, The current lazy property encryptionKey in SsoService uses
jwtSecret.toByteArray().copyOf(AES_KEY_LENGTH) which pads/truncates insecurely;
replace this with a proper key derivation step (e.g., HKDF or PBKDF2) that
deterministically derives a fixed-length AES key from jwtSecret and any
necessary salt/info, then return the derived key bytes (AES_KEY_LENGTH) as the
encryptionKey; locate the encryptionKey property and update it to call a KDF
helper (implement e.g., deriveAesKeyFromJwtSecret(jwtSecret): ByteArray using
javax.crypto.SecretKeyFactory/PBKDF2 or an HKDF implementation, and wrap result
in a SecretKeySpec or copyOfExact to AES_KEY_LENGTH), ensuring errors are
handled and no silent truncation/padding occurs.
| val response = kotlinx.coroutines.runBlocking { | ||
| httpClient.get(discoveryUrl).bodyAsText() | ||
| } |
There was a problem hiding this comment.
runBlocking inside a transaction blocks thread pool — potential for thread starvation.
discoverOidcEndpoints is called from within transaction blocks (e.g., handleOidcCallback at line 209). Using runBlocking here blocks the database connection thread while waiting for the HTTP request, which can exhaust the connection pool under load. Consider:
- Making the HTTP call before entering the transaction, or
- Using a suspending function with
newSuspendedTransaction.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/main/kotlin/com/moneat/sso/services/SsoService.kt` around lines
620 - 622, The current use of kotlinx.coroutines.runBlocking in
discoverOidcEndpoints blocks the DB transaction thread (called from transaction
contexts like handleOidcCallback) and risks exhausting the connection pool;
change discoverOidcEndpoints to be a suspending function that performs the
httpClient.get call without runBlocking and either (a) invoke it before entering
the transaction in handleOidcCallback, or (b) if it must run inside
transactional logic, call it from within a coroutine-aware transaction using
newSuspendedTransaction so the HTTP call does not block the DB thread.
| val issuerUrl = ssoConfig[SsoConfigurations.oidcIssuerUrl] | ||
| if (!issuerUrl.isNullOrBlank()) { | ||
| UrlValidator.validateExternalUrl(issuerUrl) | ||
| } | ||
| val clientId = ssoConfig[SsoConfigurations.oidcClientId] | ||
| val redirectUri = "$baseUrl/auth/sso/oidc/callback" | ||
|
|
||
| val endpoints = discoverOidcEndpoints(issuerUrl!!) |
There was a problem hiding this comment.
Potential NPE: issuerUrl!! force-unwrap when issuerUrl could be null.
Line 643 validates only if !issuerUrl.isNullOrBlank(), so if issuerUrl is null, validation is skipped. However, line 649 then force-unwraps with issuerUrl!!, which would throw an NPE. The null check should throw early, or the validation should be unconditional.
🐛 Proposed fix
private fun generateOidcRequest(
ssoConfig: org.jetbrains.exposed.v1.core.ResultRow,
state: String,
): String {
val issuerUrl = ssoConfig[SsoConfigurations.oidcIssuerUrl]
- if (!issuerUrl.isNullOrBlank()) {
- UrlValidator.validateExternalUrl(issuerUrl)
- }
+ requireNotNull(issuerUrl) { "OIDC issuer URL not configured" }
+ UrlValidator.validateExternalUrl(issuerUrl)
val clientId = ssoConfig[SsoConfigurations.oidcClientId]
val redirectUri = "$baseUrl/auth/sso/oidc/callback"
- val endpoints = discoverOidcEndpoints(issuerUrl!!)
+ val endpoints = discoverOidcEndpoints(issuerUrl)📝 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.
| val issuerUrl = ssoConfig[SsoConfigurations.oidcIssuerUrl] | |
| if (!issuerUrl.isNullOrBlank()) { | |
| UrlValidator.validateExternalUrl(issuerUrl) | |
| } | |
| val clientId = ssoConfig[SsoConfigurations.oidcClientId] | |
| val redirectUri = "$baseUrl/auth/sso/oidc/callback" | |
| val endpoints = discoverOidcEndpoints(issuerUrl!!) | |
| val issuerUrl = ssoConfig[SsoConfigurations.oidcIssuerUrl] | |
| requireNotNull(issuerUrl) { "OIDC issuer URL not configured" } | |
| UrlValidator.validateExternalUrl(issuerUrl) | |
| val clientId = ssoConfig[SsoConfigurations.oidcClientId] | |
| val redirectUri = "$baseUrl/auth/sso/oidc/callback" | |
| val endpoints = discoverOidcEndpoints(issuerUrl) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/main/kotlin/com/moneat/sso/services/SsoService.kt` around lines
642 - 649, The code force-unwraps issuerUrl when calling
discoverOidcEndpoints(issuerUrl!!) even though earlier validation only runs if
issuerUrl is non-blank; make issuerUrl non-null before use by either enforcing
an early check (e.g., requireNotNull or throw a descriptive exception) or by
moving UrlValidator.validateExternalUrl to run unconditionally after a non-null
assertion, and then call discoverOidcEndpoints with the guaranteed-non-null
issuerUrl; update references in this block: issuerUrl,
SsoConfigurations.oidcIssuerUrl, UrlValidator.validateExternalUrl, and
discoverOidcEndpoints to reflect the non-null contract.
| <div className="flex items-center justify-between rounded-lg border p-3"> | ||
| <div className="space-y-0.5"> | ||
| <Label htmlFor="requireSso" className="text-base font-medium"> | ||
| <Label htmlFor="requireSso" className="text-sm font-medium"> | ||
| Require SSO | ||
| {!hasSamlModule && ( | ||
| <Badge variant="secondary" className="ml-2 text-[10px]">Enterprise</Badge> | ||
| )} | ||
| </Label> | ||
| <p className="text-sm text-muted-foreground"> | ||
| <p className="text-xs text-muted-foreground"> | ||
| Block password login for users in this organization | ||
| </p> | ||
| </div> | ||
| <Switch | ||
| id="requireSso" | ||
| checked={formData.requireSso} | ||
| onCheckedChange={(checked) => setFormData({ ...formData, requireSso: checked })} | ||
| disabled={!hasSamlModule} | ||
| /> |
There was a problem hiding this comment.
Let downgraded orgs turn Require SSO off.
If requireSso is already true and hasSamlModule flips to false, this switch renders checked and disabled, so every save still posts requireSso: true. The backend rejects that flag without the module, which leaves the user unable to clear enforcement from the UI or save unrelated SSO edits.
Suggested fix
- <Switch
- id="requireSso"
- checked={formData.requireSso}
- onCheckedChange={(checked) => setFormData({ ...formData, requireSso: checked })}
- disabled={!hasSamlModule}
- />
+ <Switch
+ id="requireSso"
+ checked={formData.requireSso}
+ onCheckedChange={(checked) => setFormData({ ...formData, requireSso: checked })}
+ disabled={!hasSamlModule && !formData.requireSso}
+ />📝 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.
| <div className="flex items-center justify-between rounded-lg border p-3"> | |
| <div className="space-y-0.5"> | |
| <Label htmlFor="requireSso" className="text-base font-medium"> | |
| <Label htmlFor="requireSso" className="text-sm font-medium"> | |
| Require SSO | |
| {!hasSamlModule && ( | |
| <Badge variant="secondary" className="ml-2 text-[10px]">Enterprise</Badge> | |
| )} | |
| </Label> | |
| <p className="text-sm text-muted-foreground"> | |
| <p className="text-xs text-muted-foreground"> | |
| Block password login for users in this organization | |
| </p> | |
| </div> | |
| <Switch | |
| id="requireSso" | |
| checked={formData.requireSso} | |
| onCheckedChange={(checked) => setFormData({ ...formData, requireSso: checked })} | |
| disabled={!hasSamlModule} | |
| /> | |
| <div className="flex items-center justify-between rounded-lg border p-3"> | |
| <div className="space-y.5"> | |
| <Label htmlFor="requireSso" className="text-sm font-medium"> | |
| Require SSO | |
| {!hasSamlModule && ( | |
| <Badge variant="secondary" className="ml-2 text-[10px]">Enterprise</Badge> | |
| )} | |
| </Label> | |
| <p className="text-xs text-muted-foreground"> | |
| Block password login for users in this organization | |
| </p> | |
| </div> | |
| <Switch | |
| id="requireSso" | |
| checked={formData.requireSso} | |
| onCheckedChange={(checked) => setFormData({ ...formData, requireSso: checked })} | |
| disabled={!hasSamlModule && !formData.requireSso} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@dashboard/src/components/SsoSettings.tsx` around lines 308 - 325, The switch
is disabled whenever hasSamlModule is false, which prevents downgraded orgs from
turning off an already-true formData.requireSso; update the Switch so it's only
disabled when the module is missing AND the current value is false (e.g.
disabled={!hasSamlModule && !formData.requireSso}), keeping the existing
onCheckedChange (setFormData) logic so users can flip requireSso off even after
a downgrade; ensure you update the Switch component usage in SsoSettings
(checked={formData.requireSso}, onCheckedChange, disabled) accordingly.
| const hasSamlModule = hasEnterpriseModule(features, 'saml') | ||
| const canManageTeam = user?.orgRole === 'admin' || user?.orgRole === 'owner' | ||
| const canUseSso = canManageTeam && (isSelfHosted || hasSamlModule || (!isSelfHosted && (tier === 'TEAM' || tier === 'BUSINESS'))) |
There was a problem hiding this comment.
Tighten the SSO tab gate to match backend entitlements and ownership.
hasEnterpriseModule(features, 'saml') is derived from the deployment-wide /v1/features modules list, so on SaaS it can make canUseSso true for FREE-tier orgs whenever the SAML module is loaded. canManageTeam also includes admins, but configureSso() only allows organization owners. As written, users can land on a fully editable SSO tab that the API will reject. Split this into a view gate and an owner-only configure gate, and keep SaaS access tied to TEAM/BUSINESS.
Also applies to: 325-327
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@dashboard/src/routes/settings.tsx` around lines 172 - 174, The SSO access
logic is too permissive: derive a view gate and an owner-only configure gate so
the UI matches backend entitlements. Replace the single canUseSso with two
booleans, e.g. canViewSsoTab (true if isSelfHosted && hasSamlModule OR (not
isSelfHosted && (tier === 'TEAM' || tier === 'BUSINESS'))) and canConfigureSso
(true only when user?.orgRole === 'owner' and likewise scoped to
self-hosted/module or SaaS tiers), and update usages of configureSso() and the
tab rendering to use canConfigureSso for editable actions and canViewSsoTab for
visibility; keep hasSamlModule considered only for self-hosted paths and do not
grant SaaS FREE-tier access based on the global features list.
|
|
||
| fun Route.samlRoutes() { | ||
| val samlService = SamlService() | ||
| val frontendUrl = EnvConfig.get("FRONTEND_URL")!! |
There was a problem hiding this comment.
Handle missing FRONTEND_URL gracefully.
Using !! on EnvConfig.get("FRONTEND_URL") will throw a NullPointerException at runtime if the environment variable is not set. Consider failing fast with a descriptive error or providing a sensible default for development.
🛡️ Proposed fix
- val frontendUrl = EnvConfig.get("FRONTEND_URL")!!
+ val frontendUrl = EnvConfig.get("FRONTEND_URL")
+ ?: error("FRONTEND_URL environment variable is required for SAML SSO")📝 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.
| val frontendUrl = EnvConfig.get("FRONTEND_URL")!! | |
| val frontendUrl = EnvConfig.get("FRONTEND_URL") | |
| ?: error("FRONTEND_URL environment variable is required for SAML SSO") |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ee/backend/src/main/kotlin/com/moneat/enterprise/sso/routes/SamlRoutes.kt` at
line 29, The code in SamlRoutes.kt uses EnvConfig.get("FRONTEND_URL")!! which
can NPE; replace this with a safe retrieval that either fails fast with a
descriptive exception or provides a sensible dev default. Locate the frontendUrl
assignment in SamlRoutes and change it to use a null-coalescing check on
EnvConfig.get("FRONTEND_URL") (e.g., EnvConfig.get(...) ?: throw
IllegalStateException("FRONTEND_URL must be set for SAML routes")) or supply a
clearly documented default (e.g., "http://localhost:3000") so the app won't
crash with a raw NPE. Ensure the error message references FRONTEND_URL and SAML
startup so logs are actionable.
| val samlResponseObj = SamlResponse(settings, null) | ||
| samlResponseObj.loadXmlFromBase64(samlResponse) | ||
|
|
||
| if (!samlResponseObj.isValid(relayState)) { | ||
| val error = samlResponseObj.error | ||
| logger.error { "SAML authentication failed: $error" } | ||
| throw IllegalArgumentException( | ||
| "SAML authentication failed: $error" | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
OneLogin java-saml SamlResponse isValid method signature parameters
💡 Result:
In OneLogin’s java-saml toolkit, com.onelogin.saml2.authn.SamlResponse exposes these isValid overloads:
public boolean isValid()[1]public boolean isValid(String requestId)— whererequestIdis the AuthNRequest ID your SP sent to the IdP [1]
Notes on the requestId parameter: passing null affects how InResponseTo is accepted/rejected depending on Saml2Settings.setRejectUnsolicitedResponsesWithInResponseTo(...) [2].
Sources: [1] [2]
🏁 Script executed:
# Read the SamlService.kt file around lines 114-123
sed -n '100,140p' ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.ktRepository: moneat-io/moneat
Length of output: 1585
🏁 Script executed:
# Find the method signature containing this code
sed -n '70,120p' ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.ktRepository: moneat-io/moneat
Length of output: 1866
🏁 Script executed:
# Search for generateSamlRequest method and AuthnRequest handling
sed -n '1,70p' ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.ktRepository: moneat-io/moneat
Length of output: 2783
🏁 Script executed:
# Search for generateSamlRequest and AuthnRequest usage to see if requestId is captured
rg -A 15 "fun generateSamlRequest" ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.ktRepository: moneat-io/moneat
Length of output: 579
🏁 Script executed:
# Check what's in the AuthnRequest to understand how to get its ID
rg -A 20 "val authnRequest = AuthnRequest" ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.ktRepository: moneat-io/moneat
Length of output: 902
🏁 Script executed:
# Search for how AuthnRequest ID is used elsewhere or stored
rg "AuthnRequest" ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.ktRepository: moneat-io/moneat
Length of output: 218
🏁 Script executed:
# Check what AuthnRequest provides - look for getId method
rg "getId\|getAuthnRequest" ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/Repository: moneat-io/moneat
Length of output: 42
🏁 Script executed:
# Check the method signature more broadly - what parameters does handleSamlResponse receive
sed -n '85,125p' ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.ktRepository: moneat-io/moneat
Length of output: 1425
Use isValid(requestId) instead of isValid(relayState) for proper SAML response validation.
The OneLogin java-saml library's SamlResponse.isValid() method accepts either no parameters or a String requestId parameter (the ID from the AuthnRequest) for InResponseTo validation. The code currently passes relayState, which is the CSRF state token and not a valid parameter.
To fix this, capture the AuthnRequest ID when creating the AuthnRequest (via authnRequest.getId()), store it alongside the relay state, and pass it to isValid(requestId) to properly validate the InResponseTo attribute.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ee/backend/src/main/kotlin/com/moneat/enterprise/sso/services/SamlService.kt`
around lines 114 - 123, The SAML response validation is using relayState
incorrectly; change the flow so that when you create the AuthnRequest
(authnRequest.getId()) you capture and store that requestId alongside the relay
state, and then call samlResponseObj.isValid(requestId) instead of
samlResponseObj.isValid(relayState); update SamlService (the code around
SamlResponse, SamlResponse.loadXmlFromBase64 and the isValid call) to retrieve
the stored requestId and pass it into isValid(requestId), and remove relayState
from the isValid invocation so InResponseTo is validated correctly.
|



Description
Per #186, I've decided to allow OIDC without requiring a license, and focus on allowing enterprise-grade features like enforce SSO for the enterprise license.
Summary by CodeRabbit
Release Notes
New Features
Documentation