shakha 0.3.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f2fb69e4f483c981312b4211c9205e9ae5d0d100a0d3d8f0d61f693478c8369
4
- data.tar.gz: 6dfb50d826c90ea54ed5453d7eebad7a94bf93cea02a1bb407fbd69a61da421e
3
+ metadata.gz: d366a8476de38c1c5ba4a9a1b5cc286b96ef0f99c20e08101c94a4048621a63c
4
+ data.tar.gz: 7a79e8f4493e93a4b1d6e2d9711ad0bbbd9eede57fede0de68ce667d184d82a9
5
5
  SHA512:
6
- metadata.gz: 1bac5c8e1d92fb2997713d5ad19d7b16327fc21ec7e7520adf09d736bbe2c15c1cb72ef7c859798cba62a8a012819a1fb4863bd6c49830cd10d98f3082d4afc9
7
- data.tar.gz: 8e9ae9652bb51b48bc30623305113e75c2a1c56676ec17a4e9503f4dc9694f55cf87d1d58fa9a2aaee956e47fa8ee1ecdc35024f3500c616e228942f42efbfac
6
+ metadata.gz: 1da077fb10311c211402e86cc09525bc1ac6aabf0d78000952d493314f4b5f47e54d77b7ee00615483f5aefb6c29e1f0732997fa363fcfe84b4856466b0f4234
7
+ data.tar.gz: 17825d89cd29b0a4e63ed1b4a504e4a4c2c5fa91696d77dfccbbedbde1e0ff80ba2ee739f9916f43671a9b71f5345244c73956bfa464087a97eec58a07626ed8
data/CHANGELOG.md ADDED
@@ -0,0 +1,128 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres
5
+ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Until 1.0.0, minor
6
+ versions may include breaking changes; these are called out below.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.9.0] — 2026-10-03
11
+
12
+ Developer experience and adoption.
13
+
14
+ ### Added
15
+ - `ActiveSupport::Notifications` events: `shakha.sign_in`, `shakha.auth_failure`,
16
+ and `shakha.sign_out`, documented in `docs/instrumentation.md`.
17
+ - `Shakha::Providers.register(name, klass)` for third-party providers, plus a
18
+ provider-writing guide (`docs/providers.md`) with a worked GitLab example.
19
+ - `GET /sessions` lists the current user's active sessions (flagging the
20
+ requesting one) and `DELETE /sessions/:id` revokes one of them.
21
+ `Shakha::Session.for(user)` returns a user's active sessions newest first.
22
+ - Installer `--providers=google,github` option; the initializer only includes
23
+ the chosen providers.
24
+ - `examples/rails-api-react`: a runnable Rails API + Vite/React demo, booted in CI.
25
+
26
+ ### Changed
27
+ - Re-running `rails generate shakha:install` no longer creates a second
28
+ migration, overwrites an existing initializer, or re-adds the cookie
29
+ middleware.
30
+ - Configuration validation now requires credentials only for enabled built-in
31
+ providers. A GitHub-only app no longer needs `GOOGLE_CLIENT_ID`.
32
+
33
+ ## [0.8.0] — 2026-07-27
34
+
35
+ First public release of the reworked gem.
36
+
37
+ ### Added
38
+ - Rewritten README, `CHANGELOG.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`,
39
+ issue/PR templates, and Dependabot.
40
+
41
+ ### Changed
42
+ - Gemspec: accurate summary/description, GitHub homepage, `source_code_uri` /
43
+ `changelog_uri` / `bug_tracker_uri`, `rubygems_mfa_required`, and an explicit
44
+ packaged-file manifest.
45
+ - Removed superseded planning directories from the repo (kept in git history).
46
+
47
+ ## [0.7.0] — 2026-07-26
48
+
49
+ Security hardening. Several changes are breaking; there are no production
50
+ installs of the intermediate 0.6.0 to migrate.
51
+
52
+ ### Security
53
+ - Verify the Google ID token: the OIDC `nonce` is now threaded through the
54
+ authorize step and checked on callback, and the `iss`, `aud`, and `exp` claims
55
+ are validated. The `aud` check rejects tokens minted for other clients.
56
+ - Deliver the session token via a single-use, 60-second exchange code by default
57
+ instead of placing it in the redirect URL, where it could leak into history,
58
+ `Referer` headers, and logs.
59
+ - Validate `return_to` against the app origin and `allowed_redirect_origins`
60
+ before it is stored, closing an open-redirect path in the OAuth flow.
61
+ - Run `bundler-audit` in CI; raise the `jwt` floor to `>= 2.10.3` and patch
62
+ `rails-html-sanitizer`, `json`, and `concurrent-ruby`.
63
+ - Add `SECURITY.md` with a private disclosure process and threat model.
64
+
65
+ ### Changed
66
+ - **Breaking:** the OAuth callback now redirects with `?code=` (a one-time
67
+ exchange code). Set `config.redirect_token_delivery = :token` to restore the
68
+ previous `?token=` behavior for one release.
69
+ - **Breaking:** removed the `shakha_clients` table and the `Shakha::Client`
70
+ model. Users and sessions no longer belong to a client. The sign-in page shows
71
+ `config.app_name` (default `"Shakha"`).
72
+ - Returning users' `email`/`name`/`picture` are refreshed on each sign-in.
73
+ - The OAuth `redirect_uri` is derived from the engine's actual mount point, so
74
+ Shakha works when mounted anywhere.
75
+ - An unknown or disabled provider returns `404` instead of raising a `500`.
76
+
77
+ ### Added
78
+ - `config.redirect_token_delivery`, `config.app_name`.
79
+ - `POST /auth/shakha/session/exchange` — swap a one-time code for the token.
80
+ - The install generator injects `ActionDispatch::Cookies` into API-only hosts.
81
+
82
+ ## [0.6.0] — 2026-07-26
83
+
84
+ ### Added
85
+ - A real test suite: an in-process dummy Rails app harness with full coverage of
86
+ the OAuth flow, session/bearer auth, providers, PKCE, models, and the
87
+ generator. Previously the suite could not load.
88
+ - GitHub Actions CI across Ruby 3.1–3.4 × Rails 7.1/8.0/8.1, plus RuboCop
89
+ (`rubocop-rails-omakase`).
90
+
91
+ ### Fixed
92
+ - PKCE code verifier no longer carries base64 padding (`SecureRandom.urlsafe_base64`
93
+ takes `padding` positionally, so `padding: false` had passed a truthy hash).
94
+ - The OAuth callback no longer trips Rails' open-redirect protection.
95
+ - The install generator injects into `ApplicationController` using the
96
+ generator's `destination_root` rather than a CWD-relative path.
97
+
98
+ ### Changed
99
+ - **Breaking (API-mode hosts):** the engine no longer force-inserts cookie and
100
+ session middleware into the host app. Re-run `rails generate shakha:install`,
101
+ or add `ActionDispatch::Cookies` to your middleware stack.
102
+
103
+ ## [0.5.0] — 2026-05-20
104
+ - End-to-end verification of the SPA flow in a Rails 8.1 API app; runtime fixes
105
+ found during that testing; API-mode cookie/session compatibility.
106
+
107
+ ## [0.3.0] — 2026-05-19
108
+ - Multi-provider system (Google and GitHub) wired into the engine; simplified
109
+ provider-scoped URLs; `rails generate shakha:install` generator.
110
+
111
+ ## [0.2.0] — 2026-05-19
112
+ - Rework toward an SPA-first broker: fixed six fatal bugs and several security
113
+ issues; rewrote the auth flow and controllers; simplified models and config;
114
+ removed the JWT/JWKS/OIDC and pairwise-subject service infrastructure.
115
+
116
+ ## [0.1.0] — 2026-05
117
+ - Initial prototype: Google OAuth broker with PKCE, database sessions, and a
118
+ sign-in page.
119
+
120
+ [Unreleased]: https://github.com/Asrat77/shakha/compare/v0.9.0...HEAD
121
+ [0.9.0]: https://github.com/Asrat77/shakha/compare/v0.8.0...v0.9.0
122
+ [0.8.0]: https://github.com/Asrat77/shakha/compare/v0.7.0...v0.8.0
123
+ [0.7.0]: https://github.com/Asrat77/shakha/compare/v0.6.0...v0.7.0
124
+ [0.6.0]: https://github.com/Asrat77/shakha/compare/v0.5.0...v0.6.0
125
+ [0.5.0]: https://github.com/Asrat77/shakha/compare/v0.3.0...v0.5.0
126
+ [0.3.0]: https://github.com/Asrat77/shakha/compare/v0.2.0...v0.3.0
127
+ [0.2.0]: https://github.com/Asrat77/shakha/compare/v0.1.0...v0.2.0
128
+ [0.1.0]: https://github.com/Asrat77/shakha/releases/tag/v0.1.0
data/README.md CHANGED
@@ -1,93 +1,123 @@
1
1
  # Shakha
2
2
 
3
- Headless Google OAuth broker for Rails — PKCE, pairwise subjects, zero JavaScript.
3
+ **SPA-first OAuth for Rails. One redirect. One token. Done.**
4
4
 
5
- Same Google account, different IDs per application. Built DHH-style: database sessions, Turbo native, single "Continue with Google" button.
5
+ [![CI](https://github.com/Asrat77/shakha/actions/workflows/ci.yml/badge.svg)](https://github.com/Asrat77/shakha/actions/workflows/ci.yml)
6
+ [![Gem Version](https://badge.fury.io/rb/shakha.svg)](https://rubygems.org/gems/shakha)
7
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
6
8
 
7
- ## Installation
9
+ Shakha is a headless OAuth broker engine for Rails APIs and monoliths. Your
10
+ frontend does a single redirect; Shakha runs the OAuth dance — PKCE, `state`,
11
+ and (for Google) `nonce` — against Google or GitHub, stores a revocable
12
+ database-backed session, and hands your frontend a session token it uses as a
13
+ cookie or `Authorization: Bearer` header.
8
14
 
9
- ```ruby
10
- gem "shakha"
11
- ```
15
+ No JWTs to issue or rotate. No Redis. No frontend SDK.
12
16
 
13
- Run the migration:
17
+ ## The flow
14
18
 
15
- ```bash
16
- bin/rails generate migration CreateShakhaTables
17
19
  ```
20
+ 1. Frontend: window.location = "https://api.example.com/auth/shakha/google?return_to=https://app.example.com/login"
21
+ 2. Shakha: redirects to accounts.google.com (PKCE, state, nonce — handled)
22
+ 3. Google: redirects back to /auth/shakha/google/callback?code=...
23
+ 4. Shakha: exchanges the code, verifies the ID token, creates the User + Session
24
+ 5. Shakha: redirects to https://app.example.com/login?code=<one-time-code>
25
+ 6. Frontend: POST /auth/shakha/session/exchange { code } -> { token, expires_at }
26
+ 7. Frontend: send "Authorization: Bearer <token>" on every API call
27
+ ```
28
+
29
+ The one-time code in step 5 keeps the session token out of the redirect URL (and
30
+ therefore out of browser history, `Referer` headers, and logs). It is single-use
31
+ and expires in 60 seconds.
32
+
33
+ ## Installation
34
+
35
+ Add the gem:
18
36
 
19
37
  ```ruby
20
- class CreateShakhaTables < ActiveRecord::Migration[7.1]
21
- def change
22
- create_table :shakha_clients do |t|
23
- t.string :name, null: false
24
- t.string :origin, null: false
25
- t.timestamps
26
- t.index :origin, unique: true
27
- end
28
-
29
- create_table :shakha_users do |t|
30
- t.references :client, null: false, foreign_key: { to_table: :shakha_clients }
31
- t.string :provider, null: false
32
- t.string :uid, null: false
33
- t.string :email
34
- t.string :name
35
- t.string :picture
36
- t.timestamps
37
- t.index [:provider, :uid], unique: true
38
- t.index :email
39
- end
40
-
41
- create_table :shakha_sessions do |t|
42
- t.references :user, foreign_key: { to_table: :shakha_users }
43
- t.references :client, null: false, foreign_key: { to_table: :shakha_clients }
44
- t.string :token, null: false
45
- t.string :ip_address
46
- t.string :user_agent
47
- t.timestamps
48
- t.index :token, unique: true
49
- t.index :created_at
50
- end
51
- end
52
- end
38
+ gem "shakha"
53
39
  ```
54
40
 
55
- ## Configuration
56
-
57
- Create `config/initializers/shakha.rb`:
41
+ Mount the engine in `config/routes.rb`:
58
42
 
59
43
  ```ruby
60
- Shakha.setup do |config|
61
- config.app_origin = ENV.fetch("SHAKHA_APP_ORIGIN", "http://localhost:3000")
62
- config.service_url = ENV["SHAKHA_SERVICE_URL"] # omit for embedded mode
63
- config.service_secret = ENV["SHAKHA_SERVICE_SECRET"]
64
- config.google_client_id = ENV["GOOGLE_CLIENT_ID"]
65
- config.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
66
- config.session_lifetime = 30.days
67
- end
44
+ mount Shakha::Engine => "/auth/shakha"
68
45
  ```
69
46
 
70
- Environment variables:
47
+ Run the installer and migrate:
71
48
 
72
49
  ```bash
73
- export SHAKHA_APP_ORIGIN="https://yourapp.com"
74
- export SHAKHA_SERVICE_SECRET="your-secret-key"
75
- export GOOGLE_CLIENT_ID="your-google-client-id"
76
- export GOOGLE_CLIENT_SECRET="your-google-client-secret"
50
+ bin/rails generate shakha:install
51
+ bin/rails db:migrate
52
+ ```
53
+
54
+ The generator writes a migration and `config/initializers/shakha.rb`, includes
55
+ `Shakha::ControllerHelpers` in your `ApplicationController`, and — for API-only
56
+ apps — adds the cookie middleware Shakha needs. It configures Google and GitHub
57
+ by default; pass `--providers=google` (or `github`) to set up just one. Re-running
58
+ the generator is safe: it won't add a second migration or overwrite your
59
+ initializer.
60
+
61
+ ### Configuration
62
+
63
+ Set these environment variables (the generated initializer reads them):
64
+
65
+ | Variable | Required | Purpose |
66
+ |---|---|---|
67
+ | `APP_ORIGIN` | yes | Your Rails app's origin, e.g. `https://api.example.com` |
68
+ | `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | for Google | Google OAuth credentials |
69
+ | `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` | for GitHub | GitHub OAuth credentials |
70
+ | `ALLOWED_REDIRECT_ORIGINS` | for a cross-origin SPA | Comma-separated frontend origins allowed as `return_to`, e.g. `https://app.example.com` |
71
+
72
+ Provider console redirect URIs (derived from `APP_ORIGIN` and the mount point):
73
+
74
+ - Google: `https://api.example.com/auth/shakha/google/callback`
75
+ - GitHub: `https://api.example.com/auth/shakha/github/callback`
76
+
77
+ ## Frontend (React) example
78
+
79
+ ```jsx
80
+ // Start sign-in
81
+ const signIn = () => {
82
+ const returnTo = "https://app.example.com/login";
83
+ window.location =
84
+ `https://api.example.com/auth/shakha/google?return_to=${encodeURIComponent(returnTo)}`;
85
+ };
86
+
87
+ // On the return page (/login), swap the one-time code for a token
88
+ useEffect(() => {
89
+ const code = new URLSearchParams(window.location.search).get("code");
90
+ if (!code) return;
91
+
92
+ fetch("https://api.example.com/auth/shakha/session/exchange", {
93
+ method: "POST",
94
+ headers: { "Content-Type": "application/json" },
95
+ body: JSON.stringify({ code }),
96
+ })
97
+ .then((r) => r.json())
98
+ .then(({ token }) => {
99
+ localStorage.setItem("token", token);
100
+ return fetch("https://api.example.com/auth/shakha/session", {
101
+ headers: { Authorization: `Bearer ${token}` },
102
+ });
103
+ })
104
+ .then((r) => r.json())
105
+ .then(({ user }) => setUser(user));
106
+ }, []);
77
107
  ```
78
108
 
79
- Google Cloud Console redirect URI: `https://yourapp.com/auth/shakha/callback`
109
+ For a complete runnable version (Rails API + Vite/React), see
110
+ [examples/rails-api-react](examples/rails-api-react).
80
111
 
81
- ## Usage
112
+ ## Rails monolith usage
82
113
 
83
- ### Sign In
114
+ Shakha ships a minimal sign-in page and sets an encrypted session cookie, so a
115
+ classic server-rendered app needs no JavaScript.
84
116
 
85
117
  ```erb
86
- <%= link_to "Sign in with Google", shakha.new_auth_path %>
118
+ <%= link_to "Sign in", "/auth/shakha/google" %>
87
119
  ```
88
120
 
89
- ### Protect Routes
90
-
91
121
  ```ruby
92
122
  class ApplicationController < ActionController::Base
93
123
  include Shakha::ControllerHelpers
@@ -95,48 +125,96 @@ class ApplicationController < ActionController::Base
95
125
  end
96
126
  ```
97
127
 
98
- ### Current User
99
-
100
128
  ```ruby
101
- current_user # Shakha::User or nil
102
- current_session # Shakha::Session or nil
103
- signed_in? # boolean
104
- authenticate! # redirects to login if not signed in
129
+ current_user # Shakha::User or nil
130
+ current_session # Shakha::Session or nil
131
+ signed_in? # boolean
132
+ authenticate! # 401 JSON for API requests, redirect to sign-in for HTML
105
133
  ```
106
134
 
107
- ### Sign Out
135
+ Sign out:
108
136
 
109
137
  ```erb
110
- <%= link_to "Sign out", shakha.session_path, data: { turbo_method: :delete } %>
138
+ <%= button_to "Sign out", "/auth/shakha/sign_out", method: :delete %>
111
139
  ```
112
140
 
113
- ### JWT Verification (API Mode)
141
+ ## Endpoints
142
+
143
+ All paths are relative to the mount point (`/auth/shakha` above).
144
+
145
+ | Method | Path | Auth | Purpose |
146
+ |---|---|---|---|
147
+ | GET | `/` | — | Built-in sign-in page (monolith) |
148
+ | GET | `/:provider` | — | Start OAuth (`google`, `github`) |
149
+ | GET | `/:provider/callback` | — | OAuth callback (provider redirects here) |
150
+ | POST | `/session/exchange` | one-time code | Swap the code for `{ token, expires_at }` |
151
+ | GET | `/session` | cookie or bearer | Current user: `{ user: {...}, session: {...} }` |
152
+ | GET | `/session/check` | cookie or bearer | Lightweight `{ status: "active" }` / `401 { status: "expired" }` |
153
+ | DELETE | `/sign_out` | cookie or bearer | Destroy the session |
154
+ | GET | `/sessions` | cookie or bearer | Your active sessions, newest first: `{ sessions: [{ id, ip_address, user_agent, created_at, current }] }` |
155
+ | DELETE | `/sessions/:id` | cookie or bearer | Revoke one of your sessions (`404` if it isn't yours) |
156
+
157
+ ## Configuration reference
158
+
159
+ Set in `config/initializers/shakha.rb` via `Shakha.setup { |config| ... }`.
160
+
161
+ | Option | Default | Description |
162
+ |---|---|---|
163
+ | `app_origin` | — | Your Rails app's origin; used to build the OAuth `redirect_uri` |
164
+ | `app_name` | `"Shakha"` | Name shown on the built-in sign-in page |
165
+ | `google_client_id` / `google_client_secret` | — | Google OAuth credentials |
166
+ | `github_client_id` / `github_client_secret` | — | GitHub OAuth credentials |
167
+ | `providers` | `[:google]` | Enabled providers |
168
+ | `allowed_redirect_origins` | `nil` | Extra origins permitted as `return_to` targets |
169
+ | `redirect_token_delivery` | `:exchange_code` | `:exchange_code` (one-time code) or `:token` (token in the URL) |
170
+ | `session_lifetime` | `30.days` | How long a session stays valid |
171
+ | `rate_limiting_enabled` | `false` | Rate-limit `authorize` (20/min) and `callback` (10/min) |
172
+
173
+ ## Security
174
+
175
+ Shakha uses PKCE (S256), a timing-safe `state` check, and Google `nonce` +
176
+ ID-token claim (`iss`/`aud`/`exp`) verification. Session tokens are random
177
+ strings in the database — deleting the row revokes access immediately. See
178
+ [SECURITY.md](SECURITY.md) for the full threat model and how to report a
179
+ vulnerability.
180
+
181
+ ## Instrumentation
182
+
183
+ Shakha emits `shakha.sign_in`, `shakha.auth_failure`, and `shakha.sign_out`
184
+ through `ActiveSupport::Notifications`, so you can build an audit log without
185
+ patching the engine. See [docs/instrumentation.md](docs/instrumentation.md) for
186
+ payloads and examples.
187
+
188
+ ## How it compares
189
+
190
+ - **OmniAuth** — a Rack middleware for the OAuth handshake; you still build
191
+ sessions, endpoints, and the SPA token hand-off yourself. Shakha ships the
192
+ whole path.
193
+ - **Devise** — excellent for password auth in a monolith; OAuth and API/SPA
194
+ token flows are add-ons. Shakha is OAuth-only and API-first.
195
+ - **Rodauth** — broader and more configurable, with its own conventions. Shakha
196
+ does one thing: an OAuth session broker for the API + SPA era.
197
+
198
+ ## Writing your own provider
199
+
200
+ A provider implements five methods (`provider_name`, `scopes`, `authorize_url`,
201
+ `exchange_code`, `identity_from_response`) returning an identity hash of
202
+ `{ provider:, uid:, email:, name:, picture: }`, and is registered with
203
+ `Shakha::Providers.register(:gitlab, "GitLabProvider")`. See
204
+ [docs/providers.md](docs/providers.md) for the full contract, a worked GitLab
205
+ example, and a testing pattern.
206
+
207
+ ## Development
114
208
 
115
- ```ruby
116
- payload = Shakha.verify_token(id_token)
117
- user_id = payload[:pairwise_sub]
209
+ ```bash
210
+ bundle install
211
+ bundle exec rake test
212
+ bundle exec rubocop
118
213
  ```
119
214
 
120
- ## Architecture
121
-
122
- - **PKCE** — S256 code challenges on every flow
123
- - **Pairwise subjects** — domain-scoped user identifiers (HMAC-SHA256)
124
- - **ES256 JWTs** — signed with JWKS endpoint at `.well-known/jwks.json`
125
- - **OpenID Connect** — `.well-known/openid-configuration` endpoint
126
- - **Database sessions** — DHH-style, no Redis
127
- - **Turbo native** — zero JavaScript needed
128
- - **Embedded or standalone** — runs as Rails engine or headless service
129
-
130
- ## Modes
131
-
132
- ### Embedded (default)
133
-
134
- Mount in your Rails app. Routes served at `/auth/shakha`. Uses the app's own `shakha_clients` table with a single client auto-created on first request.
135
-
136
- ### Service (multi-tenant)
137
-
138
- Set `SHAKHA_SERVICE_URL` and register each app's origin in `shakha_clients`. Each app gets different pairwise subjects for the same Google user.
215
+ Test against another Rails version with `BUNDLE_GEMFILE=gemfiles/rails_71.gemfile`.
216
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
139
217
 
140
218
  ## License
141
219
 
142
- MIT
220
+ MIT — see [LICENSE.txt](LICENSE.txt).
data/SECURITY.md ADDED
@@ -0,0 +1,56 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Shakha is pre-1.0. Security fixes are released only for the latest minor
6
+ version. Please stay current until 1.0 establishes a support window.
7
+
8
+ ## Reporting a vulnerability
9
+
10
+ **Do not open a public issue for security problems.**
11
+
12
+ Report privately through GitHub Security Advisories — the "Report a
13
+ vulnerability" button on the repository's Security tab. Include a description,
14
+ affected versions, and a reproduction if you have one. We aim to acknowledge
15
+ within 7 days and to coordinate disclosure once a fix is available.
16
+
17
+ ## Threat model
18
+
19
+ Shakha is an OAuth session broker. It is responsible for the integrity of the
20
+ OAuth redirect flow and for session lifecycle; it deliberately delegates the
21
+ rest to the host application.
22
+
23
+ ### What Shakha protects
24
+
25
+ - **Authorization-code interception** — PKCE (S256) on every flow.
26
+ - **CSRF on the OAuth round-trip** — a random `state`, stored in an encrypted,
27
+ http-only cookie and compared timing-safely on callback.
28
+ - **ID token replay / substitution (Google)** — the OIDC `nonce` is verified
29
+ against the value minted at authorize time, and the `iss`, `aud`, and `exp`
30
+ claims are validated. The `aud` check rejects tokens minted for other clients.
31
+ - **Open redirects** — `return_to` is validated against the app origin and an
32
+ explicit `allowed_redirect_origins` allowlist before it is stored, so the
33
+ post-login redirect can only reach approved origins.
34
+ - **Token exposure in URLs** — by default the callback redirects with a
35
+ single-use, 60-second exchange code rather than the session token itself; the
36
+ frontend swaps it for the token over a POST.
37
+ - **Session revocation** — sessions are random tokens stored in the database;
38
+ deleting the row revokes access immediately. No JWTs are issued, so there is
39
+ no standalone-token revocation gap.
40
+ - **Brute force on the auth endpoints** — optional rate limiting on `authorize`
41
+ and `callback`.
42
+
43
+ ### What the host application owns
44
+
45
+ - **TLS** — Shakha assumes it runs behind HTTPS in production; the Google ID
46
+ token's signature is trusted on the basis of the TLS connection to Google's
47
+ token endpoint rather than re-verified.
48
+ - **CORS** — the host configures which origins may call its API.
49
+ - **Frontend XSS** — any script that runs in the frontend can read a bearer
50
+ token the SPA stores; Shakha cannot defend a compromised frontend.
51
+ - **Secret management** — `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_SECRET`, and
52
+ the Rails `secret_key_base` (which encrypts Shakha's cookies) are the host's
53
+ responsibility.
54
+ - **Rate-limit backing store** — atomic rate limiting requires a shared,
55
+ atomic cache (Redis/Memcached) in multi-process deployments; the default
56
+ in-memory store does not coordinate across processes.
@@ -4,9 +4,8 @@ module Shakha
4
4
  class ApplicationController < ActionController::Base
5
5
  include ErrorHandler
6
6
  include ControllerHelpers
7
- include RateLimiter
8
7
 
9
- protect_from_forgery with: :exception
8
+ protect_from_forgery with: :exception, unless: -> { request.format.json? }
10
9
 
11
10
  layout -> { false if request.format == :json }
12
11
 
@@ -18,4 +17,4 @@ module Shakha
18
17
  render json: { error: "Invalid CSRF token" }, status: :unprocessable_entity
19
18
  end
20
19
  end
21
- end
20
+ end