main
md 192 lines 9.7 KB
Rendered Raw
1 ---
2 name: smbcloud-auth
3 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.
4 ---
5
6 # smbCloud Auth in sigit-si
7
8 > **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](../../../docs/product/product-overview.md).
9
10 sigit-si does **not** own user identity. It is a **platform client** (client id
11 `sigit`) that authenticates against the shared **smbCloud Auth** service. Treat
12 smbCloud Auth as the source of truth for credentials; sigit-si keeps only a thin
13 local mirror of the user.
14
15 For the full auth model (platform clients vs tenant auth apps, the Rust/WASM
16 SDKs, the web console), see the authoritative skill in
17 `smbcloud-cli/.agents/skills/smbcloud-auth/SKILL.md`. This file is the
18 **sigit-si-specific** view.
19
20 ## Where things live
21
22 - `app/services/smbcloud_auth_service.rb` — the only place that talks to the gem.
23 Do not call `SmbCloud::Auth` directly from controllers; go through this service.
24 - `app/controllers/sessions_controller.rb` — HTML sign in / sign out (`/auth`).
25 - `app/controllers/registrations_controller.rb` — HTML sign up (`/auth/signup`).
26 - `app/controllers/oauth/provider_controller.rb` — shared "Continue with
27 <provider>" flow (`/auth/<provider>` → smbCloud authorize;
28 `/auth/<provider>/callback` consumes the returned `access_token`). smbCloud
29 brokers the provider OAuth flow; this app only starts it and reuses the same
30 `me` → upsert → session path as `sessions#create`. Thin subclasses:
31 `oauth/github_controller.rb`, `oauth/google_controller.rb`. The SPA
32 (code.sigit.si) equivalents live under `api/v1/oauth/` (`brokered_controller.rb`
33 base + per-provider subclasses) with a redirect_uri allowlist.
34 Adding a provider = two ~15-line subclasses + `SmbcloudAuthService.<p>_authorize_url`
35 + routes + a button partial — provided smbcloud-api brokers it and the
36 AuthApp has the provider's OAuth client configured (e.g.
37 `google_oauth_client_id/secret` on the auth app; without it the broker
38 returns "Google sign-in is not configured for this app").
39 - `app/models/user.rb` — local mirror, upserted via
40 `User.find_or_create_from_smbcloud(profile.merge(access_token:))`.
41 - `Gemfile``gem "smbcloud-auth", "~> 0.4.5"` (native Rust/Magnus extension).
42
43 ## SmbcloudAuthService surface
44
45 Class methods, all of which translate gem errors into local error types:
46
47 - `login(email:, password:)``access_token` (String)
48 - `me(access_token:)` → profile hash `{ id:, email:, created_at:, updated_at: }`
49 - `signup(email:, password:)`
50 - `logout(access_token:)`
51 - `remove(access_token:)` — deletes the smbCloud account
52 - `client` — memoized `SmbCloud::Auth` client built from env config
53
54 ### Error contract
55
56 - `SmbcloudAuthService::AuthenticationError` — base; carries optional `error_code`
57 - `SmbcloudAuthService::AccountNotFoundError` — no account for that email
58 - `SmbcloudAuthService::AccountIncompleteError` — account exists but not verified;
59 surface the "check your email" message, do not treat as bad credentials
60 - Underlying gem failures arrive as `SmbCloud::Auth::Error` (with `error_code`)
61 and must be caught inside the service, never leaked to controllers.
62
63 Controllers map these to flash + HTTP status (`:unprocessable_entity` for auth
64 failures, `:internal_server_error` for config/unexpected). Preserve that mapping.
65
66 ## Local user mirror & identity reconciliation
67
68 `app/models/user.rb` mirrors the smbCloud account. The upsert keys on
69 `smbcloud_id` (smbCloud's numeric `auth_user.id`) but **reconciles by email**
70 and that reconciliation is load-bearing, not a nicety:
71
72 > **smbCloud's `auth_user.id` is per-AuthApp, not a stable global identity.**
73 > The same email is a *different* `auth_user` (different id) in every AuthApp,
74 > and the id also changes if the upstream account is deleted/recreated. So the
75 > moment sigit-si is repointed at a different AuthApp — e.g. moving the app to a
76 > different smbCloud **account** — every existing local `User.smbcloud_id` goes
77 > stale: it points at an `auth_user` that no longer backs that email.
78
79 Because `User.email` is locally unique, a `smbcloud_id`-only upsert then breaks:
80 a login returns the email's *current* `auth_user.id`, no local row matches it,
81 and inserting a new row collides on the unique email →
82 `ActiveRecord::RecordInvalid`, which surfaces as a generic **"An unexpected error
83 occurred"** on sign-in (this is exactly how the GitHub callback failed at
84 launch — the GitHub login resolved to the email's *current* AuthApp `auth_user`,
85 which differed from the id stored when the app pointed at an earlier
86 AuthApp/account).
87
88 `find_or_create_from_smbcloud` therefore does: look up by `smbcloud_id`; if that
89 misses **and** the authenticated email already has a local user, adopt that row
90 and move it onto the new `smbcloud_id`. Treat **email as the durable key** and
91 `smbcloud_id` as a pointer that can change. Never reintroduce a
92 `smbcloud_id`-only upsert. (Specs: `spec/models/user_smbcloud_spec.rb`.)
93
94 ## Credentials & environment
95
96 Required env vars (raise `KeyError` if missing — handle as a config error, not an
97 auth failure):
98
99 - `SIGITSI_SMBCLOUD_APP_ID`
100 - `SIGITSI_SMBCLOUD_APP_SECRET`
101
102 The smbCloud environment (dev vs production) is selected inside the service.
103 Keep dev and production app credentials separate; never hardcode either.
104
105 Security note: `sigit-si` is a **server-side confidential client**, so holding
106 `app_secret` here is fine. This is different from the desktop/browser clients —
107 see the public-client rule in the smbcloud-cli skill before reusing this pattern
108 in `sigit-app`.
109
110 ## Native extension build caveat
111
112 `smbcloud-auth` compiles a Rust extension via `magnus`/`rb_sys`. The pinned
113 `magnus` version does **not** build against **Ruby 4.0.x**:
114
115 ```
116 error[E0609]: no field `typed_flag` on type &rb_sys::RTypedData
117 error: could not compile `magnus`
118 ```
119
120 This affects every published version of the gem, not a specific one. If a
121 `bundle install` fails to build the native extension on Ruby 4.0, the fix is in
122 the **gem** (bump `magnus`/`rb_sys` in its `ext/auth/Cargo.toml` and republish)
123 or build under Ruby 3.3.x — not in this repo's Gemfile.
124
125 ### macOS 26/27: dlopen rejects the built bundle (empty LC_ID_DYLIB)
126
127 On macOS 26/27, a source-gem install of `smbcloud-auth` builds fine but fails
128 at runtime with:
129
130 ```
131 LoadError (dlopen(.../smbcloud-auth-X.Y.Z/lib/auth/auth.bundle):
132 load command #4 string extends beyond end of load command)
133 ```
134
135 Every request that touches `SmbcloudAuthService` then 500s (it does
136 `require "auth"` at file-eval). Root cause: rb-sys's generated Makefile
137 (`fixup_libnames` in `rb_sys/mkmf.rb`) ends the build with
138 `install_name_tool -id "" $(DLLIB)`, leaving an **empty `LC_ID_DYLIB` install
139 name**, which the stricter dyld on macOS 26/27 rejects. `gem pristine` does
140 NOT help — it rebuilds through the same broken step.
141
142 - **Durable fix** lives in the gem source (`smbcloud-cli` repo, merged to
143 `development` 2026-07-02): `sdk/gems/auth/ext/auth/extconf.rb` post-processes
144 the generated Makefile to stamp `-id "@rpath/auth.bundle"` (covers
145 `gem install`), and the gem's `Rakefile` does the same for `rake native gem`
146 builds. Ships in the next gem release after 0.4.5.
147 - **Local band-aid** if a broken bundle is already installed:
148
149 ```sh
150 install_name_tool -id "@rpath/auth.bundle" <path>/auth.bundle
151 codesign -f -s - <path>/auth.bundle
152 ```
153
154 - Diagnose with `otool -l auth.bundle | grep -A3 LC_ID_DYLIB` — an empty
155 `name (offset 24)` is the broken state.
156
157 ## Planned: /api/v1 JSON auth for the desktop app
158
159 The "siGit Code & Deploy" Tauri app (`sigit-app`) needs a JSON, token-based auth
160 surface — the current controllers are HTML + session-cookie only. When adding
161 `sigit.si/api/v1`:
162
163 - add an `Api::V1` controller namespace; reuse `SmbcloudAuthService`, do not
164 re-implement gem calls
165 - authenticate requests with the smbCloud `access_token` (bearer), not the Rails
166 session cookie
167 - return JSON shapes that match the desktop client's `account/command_*` set:
168 login, signup, me, logout, remove_account, resend confirmation, reset password,
169 check email/oauth
170 - keep the HTML controllers working; the API is additive (migrating the HTML
171 flows onto the API is optional, not required)
172 - keep `sigit-app` account features at parity with the sigit-si web account page
173
174 ## Validation
175
176 - `bundle exec rails routes | grep -E 'auth|api/v1'`
177 - `ruby -c` on touched controllers/services
178 - `bundle exec rails db:migrate` if the local `User` schema changed
179 - exercise sign in / sign out / sign up against the dev smbCloud environment
180
181 ## Common mistakes
182
183 - Calling `SmbCloud::Auth` directly from a controller instead of via the service
184 - Keying the local `User` upsert **only** on `smbcloud_id` — it is per-AuthApp
185 and changes when the app is repointed at a different AuthApp/account (or an
186 upstream account is recreated); reconcile by email
187 - Treating `AccountIncompleteError` as bad credentials instead of "verify email"
188 - Leaking `SmbCloud::Auth::Error` (or its `error_code`) to the view layer
189 - Using the Rails session cookie to authenticate desktop-app API requests
190 - Assuming a gem version bump caused a native build failure that is really the
191 Ruby 4.0 / magnus incompatibility
192 - Reusing the server-side `app_secret` pattern in a public client (desktop/browser)