* feat(desktop): scaffold Tauri 2 macOS shell with empty window
* feat(desktop): server store, URL normalization, and health-check helpers
* feat(desktop): IPC commands for server list/add/remove/health + active-server state
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* feat(desktop): native onboarding server picker with health check and remembered servers
* feat(desktop): vibrancy background, overlay titlebar, and inset traffic lights
* feat(desktop): native menu bar with standard shortcuts and menu events
* feat(desktop): webview→Rust bridge with native notifications
* fix(desktop): gate bridge injection on PageLoadEvent::Finished
Prevents double-injecting the bridge IIFE (once on Started, once on
Finished), which was duplicating every native notification.
* feat(desktop): Dock badge driven by webview attention count
* feat(desktop): launch-at-login autostart commands
* feat(desktop): sure:// deep link scheme with parse tests and navigation
* feat(desktop): preferences window with server switcher and launch-at-login
* docs(desktop): README for dev, release, signing/notarization, and deferred widget
* fix(desktop): remove dead New Window menu item
* fix(desktop): correct login route to /sessions/new
Rails uses `resources :sessions` (plural), so the login page is
/sessions/new, not the /session/new the plan assumed. Fixes an
immediate 404 when connecting to a server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* feat(desktop): Sure-styled onboarding, draggable titlebar, and app content offset
- Restyle onboarding + prefs to match Sure's auth page: solid surface
background, centered logomark, .form-field-style inputs, inverse primary
button; theme-aware via prefers-color-scheme (design-system tokens).
- Add a draggable titlebar strip on bundled pages and inject one into the
remote page so the window drags from the top everywhere.
- Inject a top offset on the logged-in app-layout root so the sidebar logo
clears the macOS traffic lights.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): de-dupe login navigation to prevent CSRF token/session race
connect() navigated to /sessions/new directly AND via the
active-server-changed event, which is also handled by a second listener
injected into the page by bridge.js. One connect fired multiple concurrent
GET /sessions/new requests, each minting a fresh session + CSRF token; the
form shown and the _sure_session finally stored could come from different
GETs, so the login POST failed 'Can't verify CSRF token authenticity'
intermittently. Route all navigation through a single window-level guard so
only the first request per server wins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): enable window drag permission; offset only the icon rail
- Add core:window:allow-start-dragging (+ show/set-focus, event emit/listen) to
capabilities so data-tauri-drag-region actually drags the window on macOS.
- Offset only the 84px left icon rail (logomark) to clear the traffic lights
instead of pushing the entire app-layout down; keep main content full-height.
- Drag strip z-index lowered below Sure's sticky headers so its controls stay
clickable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* feat(desktop): persist active server and resume session on launch
- Persist the active server to the Keychain in set_active_server; active_server
falls back to it so a relaunch knows where to go.
- On launch, auto-resume straight to the last server instead of showing the
picker every time.
- Navigate to the server root (not /sessions/new): Rails serves the dashboard
when the session cookie is still valid, or redirects to login when not — so a
persisted session no longer forces a re-login.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* feat: desktop SSO via system browser with PKCE code exchange
Passkeys/WebAuthn don't work in an embedded WKWebView, so SSO now runs in
the system browser and hands a session back to the app securely.
Server (Rails):
- GET /auth/desktop/:provider — stashes a PKCE S256 challenge, hands off to
OmniAuth (reusing the mobile auto-submit form). Passkeys work (real browser).
- openid_connect — for a linked identity in a desktop flow, mints a single-use,
2-min, PKCE-bound one-time code and redirects to sure://sso/callback?code=...
(unlinked identities are sent back with an error).
- GET /sessions/desktop_exchange — verifies the code + PKCE verifier
(secure_compare), single-use (cache delete), then create_session_for; MFA is
enforced at exchange time. Sets the normal web session cookie in the webview.
- Tests: happy path + single-use, wrong-verifier rejection, missing challenge.
Desktop (Tauri):
- start_sso command: generates PKCE, opens the browser, stores the verifier.
- sure://sso/callback deep link -> webview navigates to desktop_exchange with
the verifier (never sent through the deep link, so an intercepted code is
useless).
- bridge.ts intercepts SSO provider form submits and routes them to start_sso;
password login stays in the webview.
- remote.json capability: minimal IPC (drag, event bridge, prefs window,
start_sso) for the remote Sure origin — no fs/shell/http.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): correct remote IPC capability + handle menu in Rust + drag fallback
Root cause of prefs/switch-server/SSO/drag doing nothing on the logged-in
page: the remote-bridge capability's remote.urls ('https://*') did not match
the server origin, so all IPC (event listen, invoke, drag command) was denied.
Per Tauri v2, window.__TAURI__ is injected on remote pages only with
withGlobalTauri (set) AND a matching remote.urls; patterns need a path
wildcard.
- remote.json: urls -> https://*/**, http://*/** (+ bare host) so any server
origin matches.
- menu.rs: Preferences and Switch Server now show the prefs window directly in
Rust (no dependency on remote-page IPC); Switch Server moved from Window to
the App menu.
- bridge.ts: drops the menu-event listeners (Rust owns them), adds a
startDragging mousedown fallback for the drag strip, logs diagnostics, and
reports start_sso success/failure to the console.
- main.ts: drops the now-unused menu listeners.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): SSO via event (remote can't invoke commands), disk-backed server store
Diagnostics confirmed window.__TAURI__ + IPC work on the remote page, but a
remote origin cannot invoke custom commands ('start_sso not allowed. Plugin
not found'). Events are permitted, so SSO now goes through an event.
- SSO: bridge emits 'sure://start-sso'; Rust listens and runs begin_sso
(opens the system browser). start_sso command kept for local use.
- servers: mirror the server list + active server to a JSON file in
Application Support as a fallback — Keychain items don't persist for
unsigned builds, which was wiping the saved server on relaunch.
- remote.json: add notification:default (Sure's PWA was requesting it and
erroring).
- menu: log whether the prefs window is present when Preferences/Switch
Server fire, to diagnose the no-op.
- main: log the persisted active server on boot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): drag the top band on every page via mousedown, not a z-indexed strip
The fixed drag strip sat below Sure's sticky headers (z-10) so it worked only
on pages without a top header. Replace it with a document-level mousedown in the
top ~34px that starts a window drag unless the target is an interactive element
— so dragging works on all pages, Sure's titlebar controls stay clickable, and
main content isn't pushed down.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* ci(desktop): tag-triggered GitHub Actions release for universal unsigned .dmg
- .github/workflows/desktop-release.yml: on a 'desktop-v*' tag, build the
universal (Apple Silicon + Intel) .dmg on a macOS runner via tauri-action and
publish it to a GitHub Release with unsigned-install instructions.
- README: universal build command, the tag-based release process, and the
Gatekeeper 'Open Anyway' / xattr steps for end users.
- Drop the unused iOS/Android icon sets (macOS build only needs icon.icns).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* ci(desktop): rolling desktop-latest build on desktop/ changes, not manual tags
Replaces the manual desktop-v* tag release with a path-filtered workflow that
builds only when desktop/ changes on main and publishes to a single rolling
'desktop-latest' prerelease with a stable Sure.dmg filename — one permanent
download URL, and the file changes only when the desktop code does.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* ci(desktop): tag-driven versioned releases; tag is the single source of version
Revert to manual version tags (desktop-v*) for explicit version control, but
derive the app/.dmg version from the tag so package.json + tauri.conf.json are
synced automatically in CI — no manual version-file edits. Each tag produces its
own versioned GitHub Release with the universal unsigned .dmg.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* ci(desktop): release entirely from GitHub via workflow_dispatch version input
Make the GitHub Action the single tool to version + deploy the desktop app:
Run workflow -> enter a version -> it syncs the version, builds the universal
unsigned .dmg, and creates the desktop-v<version> tag + Release. Refuses to
re-release an existing version; marks pre-release versions accordingly. Tag
push (desktop-v*) still works as a secondary trigger.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* ci(desktop): publish releases with make_latest:false so they don't hijack the repo's Latest badge
Desktop is a secondary artifact, not the main product. Build with tauri-action,
then publish via action-gh-release with make_latest:false so the repo's 'Latest
release' badge stays on the main app's v* release. Separate desktop-v* tag
namespace already keeps it out of the v* publish workflow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): address PR review feedback (security + correctness)
Security:
- workflow: pass workflow_dispatch version via env (no shell injection); pin all
third-party actions to commit SHAs.
- SSO: gate deep-link navigation and begin_sso to servers the user has saved
(is_known_server), so a rogue page/deep link can't drive them.
- desktop_exchange is now POST (verifier in body, not URL/logs); CSRF skipped
since the single-use PKCE code is the protection.
- desktop_sso_start validates the code_challenge is a 43-char base64url digest.
- desktop_exchange claims the one-time code atomically (delete-and-check) to
close the read/delete TOCTOU.
- failure: return desktop SSO errors to the app via sure://sso/callback?error.
Correctness / stability:
- prefs window hides on close instead of being destroyed, so the menu can
reopen it.
- servers.rs: on-disk store is authoritative (file-first read), atomic writes
(temp + rename).
- main.ts/prefs.ts: try/catch around add/set/remove/active_server and boot; add
a shared serverErrorMessage helper (no duplicated substring checks).
- vite.config.ts: derive dir from import.meta.url (ESM has no __dirname).
- bridge.ts: coalesce MutationObserver scans to one per frame.
- README: notarization example uses the universal .dmg name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): scope remote IPC to server origins at runtime, drop wildcard capability
Resolves the remaining security finding: the static remote.json granted Tauri
IPC to any http(s) origin (https://*). Remove it and instead add a capability
scoped to each server's exact origin at runtime (CapabilityBuilder +
add_capability), granting only the minimal permissions the bridge needs, for
saved/active servers on startup and for the target in set_active_server. No
origin outside the user's configured servers can access IPC.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): add runtime per-origin IPC capability (grant_server_capability)
Implements the runtime-scoped capability that replaces the removed wildcard
remote.json: CapabilityBuilder scoped to each server's exact origin, added via
add_capability for saved/active servers at startup and in set_active_server.
(Split from the previous commit, which only recorded the remote.json removal.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* ci(desktop): harden release workflow (no shared caches, environment gate)
Address two release-workflow security findings:
- Remove cache: npm and the swatinem/rust-cache step so a poisoned Actions
cache written by another workflow can't flow into a published .dmg (P0).
- Add 'environment: release' to the build job so publishing can require manual
approval and scope secrets to release runs (P1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): remove transparency code and fix relaunch behavior
* fix(desktop): fix PR review findings; adjust app notarization path, use Sure theme tokens instead of hardcoding values
* ci(desktop): switch release from independant versioning to using Sure's publishing workflow, releasing and versioning with every main app release
* fix(desktop): restrict CSP as much as possible while maintaining functionality; allow bundled scripts, Tauri IPC, inline styles; deny wildcards
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Check for pending invitations before creating new Family during SSO account creation
When a user signs in via Google SSO and doesn't have an account yet, the
system now checks for pending invitations before creating a new Family.
If an invitation exists, the user joins the invited family instead.
- OidcAccountsController: check Invitation.pending in link/create_user
- API AuthController: check pending invitations in sso_create_account
- SessionsController: pass has_pending_invitation to mobile SSO callback
- Web view: show "Accept Invitation" button when invitation exists
- Flutter: show "Accept Invitation" tab/button when invitation pending
https://claude.ai/code/session_019Tr6edJa496V1ErGmsbqFU
* Fix external assistant tests: clear Settings cache to prevent test pollution
The tests relied solely on with_env_overrides to clear configuration, but
rails-settings-cached may retain stale Setting values across tests when
the cache isn't explicitly invalidated. Ensure both ENV vars AND Setting
values are cleared with Setting.clear_cache before assertions.
https://claude.ai/code/session_019Tr6edJa496V1ErGmsbqFU
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add Google SSO onboarding flow for Flutter mobile app
Previously, mobile users attempting Google SSO without a linked OIDC
identity received an error telling them to link from the web app first.
This adds the same account linking/creation flow that exists on the PWA.
Backend changes:
- sessions_controller: Cache pending OIDC auth with a linking code and
redirect back to the app instead of returning an error
- api/v1/auth_controller: Add sso_link endpoint to link Google identity
to an existing account via email/password, and sso_create_account
endpoint to create a new SSO-only account (respects JIT config)
- routes: Add POST auth/sso_link and auth/sso_create_account
Flutter changes:
- auth_service: Detect account_not_linked callback status, add ssoLink
and ssoCreateAccount API methods
- auth_provider: Track SSO onboarding state, expose linking/creation
methods and cancelSsoOnboarding
- sso_onboarding_screen: New screen with tabs to link existing account
or create new account, pre-filled with Google profile data
- main.dart: Show SsoOnboardingScreen when ssoOnboardingPending is true
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Fix broken SSO tests: use MemoryStore cache and correct redirect param
- Sessions test: check `status` param instead of `error` since
handle_mobile_sso_onboarding sends linking info with status key
- API auth tests: swap null_store for MemoryStore so cache-based
linking code validation works in test environment
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Delay linking-code consumption until SSO link/create succeeds
Split validate_and_consume_linking_code into validate_linking_code
(read-only) and consume_linking_code! (delete). The code is now only
consumed after password verification (sso_link) or successful user
save (sso_create_account), so recoverable errors no longer burn the
one-time code and force a full Google SSO roundtrip.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Make linking-code consumption atomic to prevent race conditions
Move consume_linking_code! (backed by Rails.cache.delete) to after
recoverable checks (bad password, policy rejection) but before
side-effecting operations (identity/user creation). Only the first
caller to delete the cache key gets true, so concurrent requests
with the same code cannot both succeed.
- sso_link: consume after password auth, before OidcIdentity creation
- sso_create_account: consume after allow_account_creation check,
before User creation
- Bad password still preserves the code for retry
- Add single-use regression tests for both endpoints
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Add missing sso_create_account test coverage for blank code and validation failure
- Test blank linking_code returns 400 (bad_request) with proper error
- Test duplicate email triggers user.save failure → 422 with validation errors
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Verify cache payload in mobile SSO onboarding test with MemoryStore
The test environment uses :null_store which silently discards cache
writes, so handle_mobile_sso_onboarding's Rails.cache.write was never
verified. Swap in a MemoryStore for this test and assert the full
cached payload (provider, uid, email, name, device_info,
allow_account_creation) at the linking_code key from the redirect URL.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Add rswag/OpenAPI specs for sso_link and sso_create_account endpoints
POST /api/v1/auth/sso_link: documents linking_code + email/password
params, 200 (tokens), 400 (missing code), 401 (invalid creds/expired).
POST /api/v1/auth/sso_create_account: documents linking_code +
optional first_name/last_name params, 200 (tokens), 400 (missing code),
401 (expired code), 403 (creation disabled), 422 (validation errors).
Note: RAILS_ENV=test bundle exec rake rswag:specs:swaggerize should be
run to regenerate docs/api/openapi.yaml once the runtime environment
matches the Gemfile Ruby version.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Preserve OIDC issuer through mobile SSO onboarding flow
handle_mobile_sso_onboarding now caches the issuer from
auth.extra.raw_info.iss so it survives the linking-code round trip.
build_omniauth_hash populates extra.raw_info.iss from the cached
issuer so OidcIdentity.create_from_omniauth stores it correctly.
Previously the issuer was always nil for mobile SSO-created identities
because build_omniauth_hash passed an empty raw_info OpenStruct.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Block MFA users from bypassing second factor via sso_link
sso_link authenticated with email/password but never checked
user.otp_required?, allowing MFA users to obtain tokens without
a second factor. The mobile SSO callback already rejects MFA users
with "mfa_not_supported"; apply the same guard in sso_link before
consuming the linking code or creating an identity.
Returns 401 with mfa_required: true, consistent with the login
action's MFA response shape.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Fix NoMethodError in SSO link MFA test
Replace non-existent User.generate_otp_secret class method with
ROTP::Base32.random(32), matching the pattern used in User#setup_mfa!.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Assert linking code survives rejected SSO create account
Add cache persistence assertion to "should reject SSO create account
when not allowed" test, verifying the linking code is not consumed on
the 403 path. This mirrors the pattern used in the invalid-password
sso_link test.
The other rejection tests (expired/missing linking code) don't have a
valid cached code to check, so no assertion is needed there.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Wire ui layout and AI flags into mobile auth
Include ui_layout and ai_enabled in mobile login/signup/SSO payloads,
add an authenticated endpoint to enable AI from Flutter, and gate
mobile navigation based on intro layout and AI consent flow.
* Linter
* Ensure write scope on enable_ai
* Make sure AI is available before enabling it
* Test improvements
* PR comment
* Fix review issues: test assertion bug, missing coverage, and Dart defaults (#985)
- Fix login test to use ai_enabled? (method) instead of ai_enabled (column)
to match what mobile_user_payload actually serializes
- Add test for enable_ai when ai_available? returns false (403 path)
- Default aiEnabled to false when user is null in AuthProvider to avoid
showing AI as available before authentication completes
- Remove extra blank lines in auth_provider.dart and auth_service.dart
https://claude.ai/code/session_01LEYYmtsDBoqizyihFtkye4
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add SSL_CA_FILE and SSL_VERIFY environment variables to support self-signed certificates in self-hosted environments
* fix: NoMethodError by defining SSL helper methods before configure block executes
* refactor: Refactor SessionsController to use shared SslConfigurable module and simplify SSL initializer redundant checks
* refactor: improve SSL configuration robustness and error detection accuracy
* fix:HTTParty SSL options, add file validation guards, prevent Tempfile GC, and redact URLs in error logs
* fix: Fix SSL concern indentation and stub Simplefin POST correctly in tests
* fix: normalize ssl_verify to always return boolean instead of nil
* fix: solve failing SimpleFin test
* refactor: trim unused error-handling code from SslConfigurable, replace Tempfile with fixed-path CA bundle, fix namespace pollution in initializers, and add unit tests for core SSL configuration and Langfuse CRL callback.
* fix: added require ileutils in the initializer and require ostruct in the test file.
* fix: solve autoload conflict that broke provider loading, validate all certs in PEM bundles, and add missing requires.
* Fix OIDC household invitation (issue #900)
- Auto-add existing user when inviting by email (no invite email sent)
- Accept page: choose 'Create account' or 'Sign in' (supports OIDC)
- Store invitation token in session on sign-in; accept after login (password,
OIDC, OIDC link, OIDC JIT, MFA)
- Invitation#accept_for!(user): add user to household and mark accepted
- Defensive guards: nil/blank user, token normalization, accept_for! return check
* Address PR review: rename accept_for! to accept_for, i18n OIDC notice, test fixes, stub Rails.application.config
* Fix flaky system test: assert only configure step, not flash message
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: mkdev11 <jaysmth689+github@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add mobile SSO support to sessions controller
Add /auth/mobile/:provider route and mobile_sso_start action that
captures device params in session and renders an auto-submitting POST
form to OmniAuth (required by omniauth-rails_csrf_protection).
Modify openid_connect callback to detect mobile_sso session, issue
Doorkeeper tokens via MobileDevice, and redirect to sureapp://oauth/callback
with tokens. Handles MFA users and unlinked accounts with error redirects.
Validates provider name against configured SSO providers and device info
before proceeding.
* Add SSO auth flow to Flutter service and provider
Add buildSsoUrl() and handleSsoCallback() to AuthService for
constructing the mobile SSO URL and parsing tokens from the deep
link callback.
Add startSsoLogin() and handleSsoCallback() to AuthProvider for
launching browser-based SSO and processing the redirect.
* Register deep link listener for SSO callback
Listen for sureapp://oauth/* deep links via app_links package,
handling both cold start (getInitialLink) and warm (uriLinkStream)
scenarios. Routes callbacks to AuthProvider.handleSsoCallback().
* Add Google Sign-In button to Flutter login screen
Add "or" divider and outlined Google Sign-In button that triggers
browser-based SSO via startSsoLogin('google_oauth2').
Add app_links and url_launcher dependencies to pubspec.yaml.
* Fix mobile SSO failure handling to redirect back to app
When OmniAuth fails during mobile SSO flow, redirect to
sureapp://oauth/callback with the error instead of the web login page.
Cleans up mobile_sso session data on failure.
* Address PR review feedback for mobile SSO flow
- Use strong params for device info in mobile_sso_start
- Guard against nil session data in handle_mobile_sso_callback
- Add error handling for AppLinks initialization and stream
- Handle launchUrl false return value in SSO login
- Use user-friendly error messages instead of exposing exceptions
- Reject empty token strings in SSO callback validation
* Consolidate mobile device token logic into MobileDevice model
Extract duplicated device upsert and token issuance code from
AuthController and SessionsController into MobileDevice. Add
CALLBACK_URL constant and URL builder helpers to eliminate repeated
deep-link strings. Add mobile SSO integration tests covering the
full flow, MFA rejection, unlinked accounts, and failure handling.
* Fix CI: resolve Brakeman redirect warnings and rubocop empty line
Move mobile SSO redirect into a private controller method with an
inline string literal so Brakeman can statically verify the target.
Remove unused URL builder helpers from MobileDevice. Fix extra empty
line at end of AuthController class body.
* Use authorization code exchange for mobile SSO and add signup error handling
Replace passing plaintext tokens in mobile SSO redirect URLs with a
one-time authorization code pattern. Tokens are now stored server-side
in Rails.cache (5min TTL) and exchanged via a secure POST to
/api/v1/auth/sso_exchange. Also wraps device/token creation in the
signup action with error handling and sanitizes device error messages.
* Add error handling for login device registration and blank SSO code guard
* Address PR #860 review: fix SSO race condition, add OpenAPI spec, and cleanup
- Fix race condition in sso_exchange by checking Rails.cache.delete return
value to ensure only one request can consume an authorization code
- Use strong parameters (params.require) for sso_exchange code param
- Move inline HTML from mobile_sso_start to a proper view template
- Clear stale session[:mobile_sso] flag on web login paths to prevent
abandoned mobile flows from hijacking subsequent web SSO logins
- Add OpenAPI/rswag spec for all auth API endpoints
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix mobile SSO test to match authorization code exchange pattern
The test was asserting tokens directly in the callback URL, but the code
uses an authorization code exchange pattern. Updated to exchange the code
via the sso_exchange API endpoint. Also swaps in a MemoryStore for this
test since the test environment uses null_store which discards writes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor mobile OAuth to use single shared application
Replace per-device Doorkeeper::Application creation with a shared
"Sure Mobile" OAuth app. Device tracking uses mobile_device_id on
access tokens instead of oauth_application_id on mobile_devices.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Simplified `name_id_format` selection logic in SSO provider form.
- Switched raw database query to sanitized SQL in client secret tests.
- Added condition to log JIT account creation only when identity persists.
- Sanitized failure reasons in SSO login failure handling.
- Added SSO provider connection test policy tests for super admin and regular users.
Multi-provider SSO support:
- Database-backed SSO provider management with admin UI
- Support for OpenID Connect, Google OAuth2, GitHub, and SAML 2.0
- Flipper feature flag (db_sso_providers) for dynamic provider loading
- ProviderLoader service for YAML or database configuration
Admin functionality:
- Admin::SsoProvidersController for CRUD operations
- Admin::UsersController for super_admin role management
- Pundit policies for authorization
- Test connection endpoint for validating provider config
User provisioning improvements:
- JIT (just-in-time) account creation with configurable default role
- Changed default JIT role from admin to member (security)
- User attribute sync on each SSO login
- Group/role mapping from IdP claims
SSO identity management:
- Settings::SsoIdentitiesController for users to manage connected accounts
- Issuer validation for OIDC identities
- Unlink protection when no password set
Audit logging:
- SsoAuditLog model tracking login, logout, link, unlink, JIT creation
- Captures IP address, user agent, and metadata
Advanced OIDC features:
- Custom scopes per provider
- Configurable prompt parameter (login, consent, select_account, none)
- RP-initiated logout (federated logout to IdP)
- id_token storage for logout
SAML 2.0 support:
- omniauth-saml gem integration
- IdP metadata URL or manual configuration
- Certificate and fingerprint validation
- NameID format configuration
* Add configuration and logic for dynamic SSO provider support and stricter JIT account creation
- Introduced `config/auth.yml` for centralized auth configuration and documentation.
- Added support for multiple SSO providers, including Google, GitHub, and OpenID Connect.
- Implemented stricter JIT SSO account creation modes (`create_and_link` vs `link_only`).
- Enabled optional restriction of JIT creation by allowed email domains.
- Enhanced OmniAuth initializer for dynamic provider setup and better configurability.
- Refined login UI to handle local login disabling and emergency super-admin override.
- Updated account creation flow to respect JIT mode and domain checks.
- Added tests for SSO account creation, login form visibility, and emergency overrides.
# Conflicts:
# app/controllers/sessions_controller.rb
* remove non-translation
* Refactor authentication views to use translation keys and update locale files
- Extracted hardcoded strings in `oidc_accounts/link.html.erb` and `sessions/new.html.erb` into translation keys for better localization support.
- Added missing translations for English and Spanish in `sessions` and `oidc_accounts` locale files.
* Enhance OmniAuth provider configuration and refine local login override logic
- Updated OmniAuth initializer to support dynamic provider configuration with `name` and scoped parameters for Google and GitHub.
- Improved local login logic to enforce stricter handling of super-admin override when local login is disabled.
- Added test for invalid super-admin override credentials.
* Document Google sign-in configuration for local development and self-hosted environments
---------
Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
* Add OpenID Connect login support
* Add docs for OIDC config with Google Auth
* Use Google styles for log in
- Add support for linking existing account
- Force users to sign-in with passoword first, when linking existing accounts
- Add support to create new user when using OIDC
- Add identities to user to prevent account take-ver
- Make tests mocking instead of being integration tests
- Manage session handling correctly
- use OmniAuth.config.mock_auth instead of passing auth data via request env
* Conditionally render Oauth button
- Set a config item `configuration.x.auth.oidc_enabled`
- Hide button if disabled
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Signed-off-by: soky srm <sokysrm@gmail.com>
Co-authored-by: sokie <sokysrm@gmail.com>