main
md 9.7 KB

name: smbcloud-auth

description: Use when working on authentication in the sigit-si Rails app — sign in, sign up, profile (me), sign out, or account removal. sigit-si is a platform client ("sigit") that consumes smbCloud Auth as an auth-as-a-service layer through the smbcloud-auth Ruby gem. Covers the SmbcloudAuthService wrapper, the local User upsert, env/credential config, the native-extension build caveat, and the planned /api/v1 JSON surface for the siGit Code & Deploy desktop app.

smbCloud Auth in sigit-si

Product map: siGit Code (local agent) · siGit Code Cloud (hosted chat + Cloud Sessions) · siGit Code Cloud Agent (autonomous task → PR; planning). sigit.si is Git hosting; code.sigit.si is the home of siGit Code. Full taxonomy: product-overview.

sigit-si does not own user identity. It is a platform client (client id sigit) that authenticates against the shared smbCloud Auth service. Treat smbCloud Auth as the source of truth for credentials; sigit-si keeps only a thin local mirror of the user.

For the full auth model (platform clients vs tenant auth apps, the Rust/WASM SDKs, the web console), see the authoritative skill in smbcloud-cli/.agents/skills/smbcloud-auth/SKILL.md. This file is the sigit-si-specific view.

Where things live

  • app/services/smbcloud_auth_service.rb — the only place that talks to the gem. Do not call SmbCloud::Auth directly from controllers; go through this service.
  • app/controllers/sessions_controller.rb — HTML sign in / sign out (/auth).
  • app/controllers/registrations_controller.rb — HTML sign up (/auth/signup).
  • app/controllers/oauth/provider_controller.rb — shared "Continue with " flow (/auth/<provider> → smbCloud authorize; /auth/<provider>/callback consumes the returned access_token). smbCloud brokers the provider OAuth flow; this app only starts it and reuses the same me → upsert → session path as sessions#create. Thin subclasses: oauth/github_controller.rb, oauth/google_controller.rb. The SPA (code.sigit.si) equivalents live under api/v1/oauth/ (brokered_controller.rb base + per-provider subclasses) with a redirect_uri allowlist. Adding a provider = two ~15-line subclasses + SmbcloudAuthService.<p>_authorize_url
    • routes + a button partial — provided smbcloud-api brokers it and the AuthApp has the provider's OAuth client configured (e.g. google_oauth_client_id/secret on the auth app; without it the broker returns "Google sign-in is not configured for this app").
  • app/models/user.rb — local mirror, upserted via User.find_or_create_from_smbcloud(profile.merge(access_token:)).
  • Gemfilegem "smbcloud-auth", "~> 0.4.5" (native Rust/Magnus extension).

SmbcloudAuthService surface

Class methods, all of which translate gem errors into local error types:

  • login(email:, password:)access_token (String)
  • me(access_token:) → profile hash { id:, email:, created_at:, updated_at: }
  • signup(email:, password:)
  • logout(access_token:)
  • remove(access_token:) — deletes the smbCloud account
  • client — memoized SmbCloud::Auth client built from env config

Error contract

  • SmbcloudAuthService::AuthenticationError — base; carries optional error_code
  • SmbcloudAuthService::AccountNotFoundError — no account for that email
  • SmbcloudAuthService::AccountIncompleteError — account exists but not verified; surface the "check your email" message, do not treat as bad credentials
  • Underlying gem failures arrive as SmbCloud::Auth::Error (with error_code) and must be caught inside the service, never leaked to controllers.

Controllers map these to flash + HTTP status (:unprocessable_entity for auth failures, :internal_server_error for config/unexpected). Preserve that mapping.

Local user mirror & identity reconciliation

app/models/user.rb mirrors the smbCloud account. The upsert keys on smbcloud_id (smbCloud's numeric auth_user.id) but reconciles by email — and that reconciliation is load-bearing, not a nicety:

smbCloud's auth_user.id is per-AuthApp, not a stable global identity. The same email is a different auth_user (different id) in every AuthApp, and the id also changes if the upstream account is deleted/recreated. So the moment sigit-si is repointed at a different AuthApp — e.g. moving the app to a different smbCloud account — every existing local User.smbcloud_id goes stale: it points at an auth_user that no longer backs that email.

Because User.email is locally unique, a smbcloud_id-only upsert then breaks: a login returns the email's current auth_user.id, no local row matches it, and inserting a new row collides on the unique email → ActiveRecord::RecordInvalid, which surfaces as a generic "An unexpected error occurred" on sign-in (this is exactly how the GitHub callback failed at launch — the GitHub login resolved to the email's current AuthApp auth_user, which differed from the id stored when the app pointed at an earlier AuthApp/account).

find_or_create_from_smbcloud therefore does: look up by smbcloud_id; if that misses and the authenticated email already has a local user, adopt that row and move it onto the new smbcloud_id. Treat email as the durable key and smbcloud_id as a pointer that can change. Never reintroduce a smbcloud_id-only upsert. (Specs: spec/models/user_smbcloud_spec.rb.)

Credentials & environment

Required env vars (raise KeyError if missing — handle as a config error, not an auth failure):

  • SIGITSI_SMBCLOUD_APP_ID
  • SIGITSI_SMBCLOUD_APP_SECRET

The smbCloud environment (dev vs production) is selected inside the service. Keep dev and production app credentials separate; never hardcode either.

Security note: sigit-si is a server-side confidential client, so holding app_secret here is fine. This is different from the desktop/browser clients — see the public-client rule in the smbcloud-cli skill before reusing this pattern in sigit-app.

Native extension build caveat

smbcloud-auth compiles a Rust extension via magnus/rb_sys. The pinned magnus version does not build against Ruby 4.0.x:

error[E0609]: no field `typed_flag` on type &rb_sys::RTypedData
error: could not compile `magnus`

This affects every published version of the gem, not a specific one. If a bundle install fails to build the native extension on Ruby 4.0, the fix is in the gem (bump magnus/rb_sys in its ext/auth/Cargo.toml and republish) or build under Ruby 3.3.x — not in this repo's Gemfile.

macOS 26/27: dlopen rejects the built bundle (empty LC_ID_DYLIB)

On macOS 26/27, a source-gem install of smbcloud-auth builds fine but fails at runtime with:

LoadError (dlopen(.../smbcloud-auth-X.Y.Z/lib/auth/auth.bundle):
  load command #4 string extends beyond end of load command)

Every request that touches SmbcloudAuthService then 500s (it does require "auth" at file-eval). Root cause: rb-sys's generated Makefile (fixup_libnames in rb_sys/mkmf.rb) ends the build with install_name_tool -id "" $(DLLIB), leaving an empty LC_ID_DYLIB install name, which the stricter dyld on macOS 26/27 rejects. gem pristine does NOT help — it rebuilds through the same broken step.

  • Durable fix lives in the gem source (smbcloud-cli repo, merged to development 2026-07-02): sdk/gems/auth/ext/auth/extconf.rb post-processes the generated Makefile to stamp -id "@rpath/auth.bundle" (covers gem install), and the gem's Rakefile does the same for rake native gem builds. Ships in the next gem release after 0.4.5.
  • Local band-aid if a broken bundle is already installed:
  install_name_tool -id "@rpath/auth.bundle" <path>/auth.bundle
  codesign -f -s - <path>/auth.bundle
  • Diagnose with otool -l auth.bundle | grep -A3 LC_ID_DYLIB — an empty name (offset 24) is the broken state.

Planned: /api/v1 JSON auth for the desktop app

The "siGit Code & Deploy" Tauri app (sigit-app) needs a JSON, token-based auth surface — the current controllers are HTML + session-cookie only. When adding sigit.si/api/v1:

  • add an Api::V1 controller namespace; reuse SmbcloudAuthService, do not re-implement gem calls
  • authenticate requests with the smbCloud access_token (bearer), not the Rails session cookie
  • return JSON shapes that match the desktop client's account/command_* set: login, signup, me, logout, remove_account, resend confirmation, reset password, check email/oauth
  • keep the HTML controllers working; the API is additive (migrating the HTML flows onto the API is optional, not required)
  • keep sigit-app account features at parity with the sigit-si web account page

Validation

  • bundle exec rails routes | grep -E 'auth|api/v1'
  • ruby -c on touched controllers/services
  • bundle exec rails db:migrate if the local User schema changed
  • exercise sign in / sign out / sign up against the dev smbCloud environment

Common mistakes

  • Calling SmbCloud::Auth directly from a controller instead of via the service
  • Keying the local User upsert only on smbcloud_id — it is per-AuthApp and changes when the app is repointed at a different AuthApp/account (or an upstream account is recreated); reconcile by email
  • Treating AccountIncompleteError as bad credentials instead of "verify email"
  • Leaking SmbCloud::Auth::Error (or its error_code) to the view layer
  • Using the Rails session cookie to authenticate desktop-app API requests
  • Assuming a gem version bump caused a native build failure that is really the Ruby 4.0 / magnus incompatibility
  • Reusing the server-side app_secret pattern in a public client (desktop/browser)