Skip to content

[WIP] Building Lexbox login#4139

Draft
imnasnainaec wants to merge 28 commits intomasterfrom
lexbox-login
Draft

[WIP] Building Lexbox login#4139
imnasnainaec wants to merge 28 commits intomasterfrom
lexbox-login

Conversation

@imnasnainaec
Copy link
Copy Markdown
Collaborator

@imnasnainaec imnasnainaec commented Feb 5, 2026

Working toward some form of #3719

Vibe coding locally with GitHub Copilot.

https://app.devin.ai/review/sillsdev/TheCombine/pull/4139


This change is Reviewable

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Lexbox authentication with login and logout functionality
    • New authentication status endpoint to query current login state
    • Added authentication UI component for user login/logout interactions
  • Tests

    • Added comprehensive test coverage for authentication flows and controller endpoints

@imnasnainaec imnasnainaec self-assigned this Feb 5, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 5, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57b7e052-fdaa-4ac3-91d0-29014389fae8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lexbox-login

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.

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 5, 2026

Codecov Report

❌ Patch coverage is 87.50000% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.03%. Comparing base (93fb4a9) to head (d491162).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
Backend/Controllers/AuthController.cs 84.00% 4 Missing and 4 partials ⚠️
src/components/Lexbox/LexboxLogin.tsx 87.09% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4139      +/-   ##
==========================================
+ Coverage   75.94%   76.03%   +0.09%     
==========================================
  Files         303      307       +4     
  Lines       11352    11448      +96     
  Branches     1407     1418      +11     
==========================================
+ Hits         8621     8705      +84     
- Misses       2330     2337       +7     
- Partials      401      406       +5     
Flag Coverage Δ
backend 87.24% <87.69%> (+<0.01%) ⬆️
frontend 66.89% <87.09%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@imnasnainaec

This comment was marked as outdated.

@imnasnainaec imnasnainaec changed the title [WIP] Start building Lexbox login [WIP] Building Lexbox login Feb 5, 2026
@imnasnainaec imnasnainaec force-pushed the lexbox-login branch 3 times, most recently from d49af8a to d54a65d Compare February 10, 2026 21:50
@github-actions github-actions bot added the dependencies Pull requests that update a dependency file label Feb 23, 2026
Copy link
Copy Markdown

@myieye myieye left a comment

Choose a reason for hiding this comment

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

@myieye reviewed 17 files and all commit messages, and made 6 comments.
Reviewable status: 17 of 24 files reviewed, all discussions resolved (waiting on imnasnainaec).


Backend/Startup.cs line 137 at r3 (raw file):

            var key = ASCII.GetBytes(secretKey);

            var lexboxAuthority = Configuration["LexboxAuth:Authority"]?.Trim().TrimEnd('/');

It's largely a syntax thing, but you could get rid of almost all of this by putting the values (fallback as well as any overrides you're wanting) in appsettings.json files and doing this:
configuration.Bind("LexboxAuth:Oidc", options)
See: AuthKernel.cs


Backend/Startup.cs line 149 at r3 (raw file):

                ?? "https://lexbox.org/.well-known/openid-configuration";
            var lexboxPrompt = Configuration["LexboxAuth:Prompt"] ?? "select_account";
            var lexboxScope = Configuration["LexboxAuth:Scope"] ?? "profile openid offline_access sendandreceive";

offline_access is primarily for refresh tokens. So, I'm pretty sure you can leave that one out.


Backend/Controllers/AuthController.cs line 34 at r3 (raw file):

            using var activity = OtelService.StartActivityWithTag(otelTagName, "getting auth status");

            if (!_permissionService.IsCurrentUserAuthenticated(HttpContext))

I expect there's a class attribute available to handle this check.


Backend/Controllers/AuthController.cs line 45 at r3 (raw file):

            }

            var user = GetUserFromClaims(result.Principal);

Since GetUserFromClaims can return null, you could consider letting it also take null and handling the if case above.


Backend/Controllers/AuthController.cs line 105 at r3 (raw file):

        private static LexboxAuthUser? GetUserFromClaims(System.Security.Claims.ClaimsPrincipal principal)
        {
            var userId = principal.FindFirst("sub")?.Value?.Trim();

This method would probably be more readable if you try to get an ID first. If that doesn't work, return null.
Then try to get a displayName and return the object.

There's also only a single use of System.Security.Claims.ClaimTypes. I don't know what's in there, but presumably more constants that would make other FindFirsts more consistent and readable.

It's also maybe more confusing than helpful to have so many display name fallbacks. I'm not entirely sure which claims we should expect here, but it might be worth checking and simplifying.

I'd also be tempted to throw exceptions for things we don't expect.
E.g. Is it possible for the sub claim to be missing? Should NameIdentifier really be a fallback for userId? I'd expect the ID claim to be vey stable whichever it is. Presumably sub. That's what we use.


Backend.Tests/Controllers/AuthControllerTests.cs line 89 at r3 (raw file):

            var result = await _controller.GetLexboxLogin();

            Assert.That(result, Is.InstanceOf<EmptyResult>());

EmptyResult doesn't sound like the same thing as "ReturnsExpectedLoginPath"

Copy link
Copy Markdown
Collaborator Author

@imnasnainaec imnasnainaec left a comment

Choose a reason for hiding this comment

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

@imnasnainaec reviewed 5 files and all commit messages, and made 2 comments.
Reviewable status: 16 of 24 files reviewed, all discussions resolved (waiting on myieye).


Backend/Controllers/AuthController.cs line 34 at r3 (raw file):

Previously, myieye (Tim Haasdyk) wrote…

I expect there's a class attribute available to handle this check.

There is, but this is the pattern we have in all our controllers. It should be updated everywhere... some other time.


Backend/Controllers/AuthController.cs line 105 at r3 (raw file):

Previously, myieye (Tim Haasdyk) wrote…

This method would probably be more readable if you try to get an ID first. If that doesn't work, return null.
Then try to get a displayName and return the object.

There's also only a single use of System.Security.Claims.ClaimTypes. I don't know what's in there, but presumably more constants that would make other FindFirsts more consistent and readable.

It's also maybe more confusing than helpful to have so many display name fallbacks. I'm not entirely sure which claims we should expect here, but it might be worth checking and simplifying.

I'd also be tempted to throw exceptions for things we don't expect.
E.g. Is it possible for the sub claim to be missing? Should NameIdentifier really be a fallback for userId? I'd expect the ID claim to be vey stable whichever it is. Presumably sub. That's what we use.

Updated.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (7)
Backend/Models/LexboxAuthUser.cs (1)

5-6: Tighten auth-user invariants with non-null required properties.

Line 5-Line 6 are nullable, but this model represents authenticated identity data. Making these required non-null avoids accidental invalid instances.

Proposed refactor
-        public string? UserId { get; init; }
-        public string? DisplayName { get; init; }
+        public required string UserId { get; init; }
+        public required string DisplayName { get; init; }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Backend/Models/LexboxAuthUser.cs` around lines 5 - 6, The LexboxAuthUser
model currently allows nullable UserId and DisplayName which undermines
authenticated identity invariants; update the LexboxAuthUser type so UserId and
DisplayName are non-nullable and required (e.g., change from string? to string
and mark as required or add a constructor that enforces them) so instances
cannot be created without valid values; target the UserId and DisplayName
properties on the LexboxAuthUser class when making this change.
src/components/Lexbox/LexboxLogin.tsx (2)

53-63: Consider calling onStatusChange("logged-in") when status changes to logged-in.

The onStatusChange callback is only invoked for logout. For consistency and to allow parent components to react to login events, consider also calling it when the user becomes logged in (after loadStatus detects a logged-in state).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Lexbox/LexboxLogin.tsx` around lines 53 - 63, The component
currently only calls props.onStatusChange("logged-out") inside handleLogout; add
a matching call for login events by invoking props.onStatusChange("logged-in")
when loadStatus detects the user is logged in (e.g., after successful login flow
or inside the code path that processes loadStatus results). Locate the
login/status-refresh logic (functions loadStatus and any login handler) and call
props.onStatusChange("logged-in") when the status returned by loadStatus
indicates a logged-in state so parent components are notified consistently; keep
existing handleLogout behavior intact.

49-51: Remove unnecessary async or add loading state for login.

The handleLogin function is marked async but doesn't await anything. Either remove async for clarity, or consider adding loading state feedback if the login flow should show a spinner.

Additionally, consider handling the case where window.open() returns null (popup blocked).

♻️ Suggested fix
-  const handleLogin = async (): Promise<void> => {
-    window.open(getLexboxLoginUrl());
+  const handleLogin = (): void => {
+    const popup = window.open(getLexboxLoginUrl());
+    if (!popup) {
+      console.warn("Popup was blocked");
+    }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Lexbox/LexboxLogin.tsx` around lines 49 - 51, The handleLogin
function is marked async but doesn't await anything and doesn't handle popup
blocking; either remove the async keyword from handleLogin (in LexboxLogin
component) to reflect it’s synchronous, or implement a simple loading state
(e.g., setLoading true/false) around the login flow and await any async
operations, and after calling window.open(getLexboxLoginUrl()) check the
returned window handle for null and handle that case (e.g., show an error/toast
informing the user their popup was blocked).
Backend/appsettings.json (1)

2-16: Consider making ClientId configurable via environment variable.

The ClientId is hardcoded to "the-combine". For flexibility across different deployment environments (development, staging, production), consider allowing this to be overridden via environment variable, similar to how other sensitive configuration is handled in Startup.cs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Backend/appsettings.json` around lines 2 - 16, The LexboxAuth.ClientId is
hardcoded; update the auth configuration to accept an environment override: read
an env var (e.g. LEXBOX_CLIENT_ID) and use it if present, otherwise fall back to
the existing "LexboxAuth:ClientId" value from appsettings.json. Implement this
change where the LexboxAuth settings are bound/used (e.g., in
Startup.ConfigureServices or the code that constructs the authentication
options), referencing the "LexboxAuth" section and "ClientId" key so deployments
can override client ID without changing appsettings.json.
src/components/Lexbox/tests/LexboxLogin.test.tsx (1)

26-41: Consider adding a test for error handling.

The test suite covers the happy paths well. However, the component has error handling in loadStatus() (lines 37-39 in LexboxLogin.tsx) that sets status to undefined on failure. Consider adding a test case to verify the component behavior when getLexboxAuthStatus rejects.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Lexbox/tests/LexboxLogin.test.tsx` around lines 26 - 41, Add a
new test on LexboxLogin that simulates getLexboxAuthStatus rejecting to exercise
the loadStatus error path: have
mockGetLexboxAuthStatus.mockRejectedValueOnce(new Error("fail")) (or similar),
render <LexboxLogin /> inside act, wait for loadStatus to run, then assert the
component set status to undefined by checking the UI state (e.g., login button
is disabled or an error message/placeholder is shown) and ensure no unexpected
calls to mockGetLexboxLoginUrl or window.open occur; reference the loadStatus
function, mockGetLexboxAuthStatus, LexboxLogin component, and
mockGetLexboxLoginUrl when locating where to add the test.
Backend/Controllers/AuthController.cs (2)

22-22: Consider using PascalCase for the constant name.

C# naming conventions recommend PascalCase for const fields. Consider renaming to OtelTagName.

-        private const string otelTagName = "otel.AuthController";
+        private const string OtelTagName = "otel.AuthController";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Backend/Controllers/AuthController.cs` at line 22, Rename the const field
otelTagName to PascalCase OtelTagName and update all references to it (e.g., in
AuthController methods and any callers) to use the new identifier; ensure the
const's value remains unchanged and run/adjust any unit tests or usages that
reference the old otelTagName symbol so the code compiles.

65-76: Consider using POST instead of GET for the logout endpoint.

Using GET for logout creates a minor CSRF risk — an attacker could embed the logout URL in an <img> tag to force users to log out. While the impact is low (user inconvenience only), using POST with anti-forgery protection is the safer pattern for state-changing operations.

-        [HttpGet("lexbox-logout", Name = "LogOutLexbox")]
+        [HttpPost("lexbox-logout", Name = "LogOutLexbox")]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Backend/Controllers/AuthController.cs` around lines 65 - 76, Change the
logout endpoint from GET to a POST to avoid CSRF risk: update the attribute on
the LogOutLexbox action from [HttpGet("lexbox-logout", Name = "LogOutLexbox")]
to an HTTP POST attribute (e.g., [HttpPost("lexbox-logout", Name =
"LogOutLexbox")]) and add anti-forgery validation (e.g.,
[ValidateAntiForgeryToken]) to the method; keep the existing method body that
calls OtelService.StartActivityWithTag and SignOutAsync with LexboxCookieScheme
and LexboxOidcScheme, and update any clients or tests that call LogOutLexbox to
send a POST with the antiforgery token.
🤖 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/BackendFramework.csproj`:
- Around line 21-24: The project currently mixes
Microsoft.AspNetCore.Authentication.JwtBearer (8.0.24) with
Microsoft.IdentityModel.Tokens and System.IdentityModel.Tokens.Jwt (8.16.0),
causing a runtime API mismatch; fix by aligning versions: either downgrade
Microsoft.IdentityModel.Tokens and System.IdentityModel.Tokens.Jwt to the 7.x
line (e.g., 7.1.2) to match JwtBearer 8.0.24, or upgrade
Microsoft.AspNetCore.Authentication.JwtBearer to a release that officially
supports IdentityModel 8.x; update the PackageReference entries for
Microsoft.AspNetCore.Authentication.JwtBearer, Microsoft.IdentityModel.Tokens,
and System.IdentityModel.Tokens.Jwt accordingly so all three packages are on a
mutually compatible major version.

In `@src/components/Lexbox/LexboxLogin.tsx`:
- Around line 45-47: The component LexboxLogin currently calls loadStatus() only
on mount inside the useEffect, so add a mechanism to refresh status after OAuth
completes: attach a window "focus" event listener in the same useEffect (or a
new one) that calls loadStatus when the window regains focus, and remove the
listener on cleanup; alternatively (optional) implement a BroadcastChannel or
localStorage postMessage in your OAuth popup flow to notify the main window to
call loadStatus, or start a short polling interval while login is in
progress—update the useEffect around loadStatus (and any openOAuthWindow
handler) to include the focus listener/cleanup or the messaging/polling
subscribed/unsubscribed logic.

In `@src/components/ProjectScreen/CreateProject.tsx`:
- Around line 276-279: The LexboxLogin component is currently rendered inside
the project form in CreateProject, which can cause its internal buttons to
trigger the form's createProject submit; move the <LexboxLogin /> JSX out of the
form element (place it before or after the <form> that wraps the project inputs)
so its buttons are not nested inside the form, and verify createProject remains
attached only to the form's onSubmit handler.

---

Nitpick comments:
In `@Backend/appsettings.json`:
- Around line 2-16: The LexboxAuth.ClientId is hardcoded; update the auth
configuration to accept an environment override: read an env var (e.g.
LEXBOX_CLIENT_ID) and use it if present, otherwise fall back to the existing
"LexboxAuth:ClientId" value from appsettings.json. Implement this change where
the LexboxAuth settings are bound/used (e.g., in Startup.ConfigureServices or
the code that constructs the authentication options), referencing the
"LexboxAuth" section and "ClientId" key so deployments can override client ID
without changing appsettings.json.

In `@Backend/Controllers/AuthController.cs`:
- Line 22: Rename the const field otelTagName to PascalCase OtelTagName and
update all references to it (e.g., in AuthController methods and any callers) to
use the new identifier; ensure the const's value remains unchanged and
run/adjust any unit tests or usages that reference the old otelTagName symbol so
the code compiles.
- Around line 65-76: Change the logout endpoint from GET to a POST to avoid CSRF
risk: update the attribute on the LogOutLexbox action from
[HttpGet("lexbox-logout", Name = "LogOutLexbox")] to an HTTP POST attribute
(e.g., [HttpPost("lexbox-logout", Name = "LogOutLexbox")]) and add anti-forgery
validation (e.g., [ValidateAntiForgeryToken]) to the method; keep the existing
method body that calls OtelService.StartActivityWithTag and SignOutAsync with
LexboxCookieScheme and LexboxOidcScheme, and update any clients or tests that
call LogOutLexbox to send a POST with the antiforgery token.

In `@Backend/Models/LexboxAuthUser.cs`:
- Around line 5-6: The LexboxAuthUser model currently allows nullable UserId and
DisplayName which undermines authenticated identity invariants; update the
LexboxAuthUser type so UserId and DisplayName are non-nullable and required
(e.g., change from string? to string and mark as required or add a constructor
that enforces them) so instances cannot be created without valid values; target
the UserId and DisplayName properties on the LexboxAuthUser class when making
this change.

In `@src/components/Lexbox/LexboxLogin.tsx`:
- Around line 53-63: The component currently only calls
props.onStatusChange("logged-out") inside handleLogout; add a matching call for
login events by invoking props.onStatusChange("logged-in") when loadStatus
detects the user is logged in (e.g., after successful login flow or inside the
code path that processes loadStatus results). Locate the login/status-refresh
logic (functions loadStatus and any login handler) and call
props.onStatusChange("logged-in") when the status returned by loadStatus
indicates a logged-in state so parent components are notified consistently; keep
existing handleLogout behavior intact.
- Around line 49-51: The handleLogin function is marked async but doesn't await
anything and doesn't handle popup blocking; either remove the async keyword from
handleLogin (in LexboxLogin component) to reflect it’s synchronous, or implement
a simple loading state (e.g., setLoading true/false) around the login flow and
await any async operations, and after calling window.open(getLexboxLoginUrl())
check the returned window handle for null and handle that case (e.g., show an
error/toast informing the user their popup was blocked).

In `@src/components/Lexbox/tests/LexboxLogin.test.tsx`:
- Around line 26-41: Add a new test on LexboxLogin that simulates
getLexboxAuthStatus rejecting to exercise the loadStatus error path: have
mockGetLexboxAuthStatus.mockRejectedValueOnce(new Error("fail")) (or similar),
render <LexboxLogin /> inside act, wait for loadStatus to run, then assert the
component set status to undefined by checking the UI state (e.g., login button
is disabled or an error message/placeholder is shown) and ensure no unexpected
calls to mockGetLexboxLoginUrl or window.open occur; reference the loadStatus
function, mockGetLexboxAuthStatus, LexboxLogin component, and
mockGetLexboxLoginUrl when locating where to add the test.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1095cae and 8956f13.

📒 Files selected for processing (19)
  • .vscode/settings.json
  • Backend.Tests/Controllers/AuthControllerTests.cs
  • Backend.Tests/Mocks/AuthenticationServiceMock.cs
  • Backend/BackendFramework.csproj
  • Backend/Controllers/AuthController.cs
  • Backend/Models/AuthStatus.cs
  • Backend/Models/LexboxAuthUser.cs
  • Backend/Startup.cs
  • Backend/appsettings.json
  • src/api/.openapi-generator/FILES
  • src/api/api.ts
  • src/api/api/auth-api.ts
  • src/api/models/auth-status.ts
  • src/api/models/index.ts
  • src/backend/index.ts
  • src/components/Lexbox/LexboxLogin.tsx
  • src/components/Lexbox/tests/LexboxLogin.test.tsx
  • src/components/ProjectScreen/CreateProject.tsx
  • src/components/ProjectScreen/tests/CreateProject.test.tsx

@imnasnainaec
Copy link
Copy Markdown
Collaborator Author

Progress...
Screenshot 2026-03-25 170159
Lexbox trusts The Combine, but this pr can't yet handle the returned auth.
Screenshot 2026-03-25 170314

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

Labels

backend dependencies Pull requests that update a dependency file frontend project test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants